2011年11月15日星期二

语言的易读性

    何谓语言的易读性,简单来说,就是看到一段代码的时候,能够了解其意思。易读性最差的典型代表作是汇编语言和机器语言,因为在读这两种语言的时候,其实是你的大脑在替代模拟CPU的功效。说起来,自从汇编以后,每种语言多多少少都注重了人类阅读的习惯,像brainfuck这种特例是万难一见的。例如下面的例子。
    printf("hello, world\n");
    即使没有任何C基础的人,也能够看懂这是在做一个字符串打印。
    语言的易读性其实是语言非常重要的特征,比其他特征都重要。因为人类的大脑不可能记得所有的代码细节,并且能够直观的反应出如何修改。往往我们需要阅读一下代码,搞明白每段的意思,然后才能动手——哪怕这段代码出自自己手笔,只要过得一两个月,还是要重读一下的。正是因为读这个技能的使用频率非常高,所以语言的易读性非常直观的影响到语言的易用性。而易读性差的语言和习惯,目前来看有以下几个典型例子。

1.罗嗦
    典型代表是Java。下面是一个Java解压Zip的代码,引用自参考1。
public class Zip {
    
static final int BUFFER = 2048;

    
public static void main(String argv[]) {
        
try {
            BufferedInputStream origin 
= null;
            FileOutputStream dest 
= new FileOutputStream("E:\\test\\myfiles.zip");
            ZipOutputStream out 
= new ZipOutputStream(new BufferedOutputStream(
                    dest));
            
byte data[] = new byte[BUFFER];
            File f 
= new File("e:\\test\\a\\");
            File files[] 
= f.listFiles();

            
for (int i = 0; i < files.length; i++{
                FileInputStream fi 
= new FileInputStream(files[i]);
                origin 
= new BufferedInputStream(fi, BUFFER);
                ZipEntry entry 
= new ZipEntry(files[i].getName());
                out.putNextEntry(entry);
                
int count;
                
while ((count = origin.read(data, 0, BUFFER)) != -1{
                    out.write(data, 
0, count);
                }

                origin.close();
            }

            out.close();
        }
 catch (Exception e) {
            e.printStackTrace();
        }

    }

}
    我下面给出python版本。
import os, zipfile
with zipfile.ZipFile('filename.zip', 'w' ,zipfile.ZIP_DEFLATED) as zf:
    for name in os.listdir('.'): zf.write(name)
    罗嗦有什么坏处?当你需要理解一段代码的时候,需要上上下下翻动屏幕,并且仔细对比每个细节,才能理解这个代码的目的。这对于阅读来说是非常不友好的。

2.歧义
    典型例子是++,我给出这么一个例子。
i = (++j) + (j++) + i---i
    i是多少?脑子一团糨糊吧?关于自增自减的歧义,具体可以看参考2。当然,这并不是说C++设计的有问题,只是这个用法不可取而已。
    歧义的最大问题是,不借助具体的实现运行一下,基本没有希望了解这个代码是什么意思。这哪里叫可读,这叫不可读。歧义是不可读中最差劲的一种,一切产生歧义的代码都是坏的代码,例如我们下面的这个例子:
import os
def os(os): return os
    这个,return回去的到底是谁?os module?function?variable?运行一下我们知道,其实是返回了参数。但是这种代码骤然看到,鬼才能够反应的过来,写出这种代码的,上辈子都是非洲丛林里面的守林人,想bug想疯了吧。
    还有一种是变量名类似,例如只以大小写区分,或者以下这个例子:
def sl(s1): return sl
    您看出问题了么?没看出来?这到底要多脑残才会把变量弄的那么像函数名,导致return的时候把自己的函数给return回去阿?

3.依赖上下文
    什么叫依赖上下文?其实这并不是一个很好界定的问题。无论代码多么简洁,我们都需要调用其他函数。这个函数就是所谓的上下文。在拥有一定知识的前提下,我们的代码越上下文无关越好。如果一定要上下文有关,这个相关部分越确定越好。例如C++中的一个例子:
DynamicFn<WM_Hooks_WMVAL_proto> WM_Hooks_WindowChanged(_T("wm_hooks.dll"), "WM_Hooks_WindowChanged");
    谁能告诉我,为了看懂这个代码,我需要查看多少内容?首先,我需要查看DynamicFn和WM_Hooks_WMVAL_proto的定义,然后去检查DynamicFn的构造函数。如果只有一个构造函数,并且参数类型匹配,那么很幸运,事情就到此为止。如果不匹配,我还得查看是否可以编译通过,如果可以,是匹配了哪个构造函数。如果都不匹配,那么肯定发生了内隐转换(implicit cast),如果有两个函数都可以通过内隐转换进行匹配,例如下面这种:
template <typename T>
DynamicFn::DynamicFn(wstring t, char * c);
DynamicFn::DynamicFn(TCHAR * t, string c);
    天呐,这个不但依赖上下文,而且歧义了。更郁闷的是,随着UNICODE宏的变化,这两个函数的匹配行为还会产生变化。即使上面一切都没问题,您能够直观的从刚刚的一行代码中看出代码所要达到的目的么?从老程序员的习惯来猜测,好像是wm-hooks这个dll的WM_Hooks_WindowChanged函数进行SetHook,是不是,我懒得验证了。
    比较好的解决这个问题的方法叫做代码自描述性。例如上文,这种文法是比较容易理解的。(但是不保证意义一致,因为上文我还不确定是安装Hook还是仅仅生成对象包装,或者两个行为同时实施了?下文也只是伪代码)
create_function(u"wm_hooks.dll", "WM_Hooks_WindowChanged")

4.晦涩
    何谓晦涩?高级特性过多。典型代表C++,谁来读一下这个代码?
#pragma once
#pragma lib("curl")
using namespace std;

#ifndef RFB_MAINTHREAD
#define RFB_MAINTHREAD
namespace rbf{
extern "C" {
  class MainThread: public Thread, EventHandler {
    MainThread (explicit HANDLE hFD);
    virtual ~MainThread();
    inline static int run_wrapper() { return run(); }
    virtual run() = 0;
  };
}
}

没有评论: