Shell's Home

Nov 15, 2011 - 2 minute read - Comments

语言的易读性

何谓语言的易读性,简单来说,就是看到一段代码的时候,能够了解其意思。易读性最差的典型代表作是汇编语言和机器语言,因为在读这两种语言的时候,其实是你的大脑在替代模拟CPU的功效。说起来,自从汇编以后,每种语言多多少少都注重了人类阅读的习惯,像brainfuck这种特例是万难一见的。例如下面的例子。

printf("hello, worldn");

即使没有任何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:testmyfiles.zip");
            ZipOutputStream out=new ZipOutputStream(
                new BufferedOutputStream(dest));

            byte data[]=new byte[BUFFER];
            Filef=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);
                ZipEntryentry=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(Exceptione){
            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;
    };
}
}

#endif//RFB_MAINTHREAD

OK,这段代码,就非常晦涩。其实老C++程序员都习惯这种晦涩的风格了,但是一般人还是会不知所云,必须去查手册。我们数数,这段代码用了多少奇怪的特性。

#pragma once // 这个是MSVC的一个宏,指头文件只被引用一次
#pragma lib("curl") // MSVC的宏,表示引入库

using namespace std; // 引入STL的标准命名空间
#ifndef RFB_MAINTHREAD//

虽然上面说只引用一次,但是为了兼容老的编译器,还是得定义一下,以防重复引用。

#define RFB_MAINTHREAD
namespace rbf{ // 自定义命名空间
extern "C" { // 虽然自己是C++,但是名称输出要符合C
    //多重继承,而且有点歧义的是,EventHandler按照默认规则,使用了private继承,你看出来了么?
    class MainThread: public Thread, EventHandler {
        MainThread (explicit HANDLE hFD); //explicit关键字,阻止内隐转换
        //虚析构函数,在多态重载的情况下,谨防某些编译器(例如Borland C++)上会调用不正确的析构函数,从而丢失内存
        virtual \~MainThread();
        // inline static,类静态函数可以被当作C函数等态启用,而C++类成员函数不可以
        inline static int run\_wrapper() { return run(); }
        virtual run() = 0; // 纯虚函数,必须被继承和重载
    };
}
}

#endif//RFB_MAINTHREAD

这已经算是把C++当C在写,只使用类,从而尽量避免引入高级特性了。纯正的C++程序员写出来的代码,我从effective STL摘一个例子出来看看。

v.erase(
    remove_if(
        find_if(v.rbegin(), v.rend(),
            bind2nd(greater_equal<int>(), y)
            ).base(),
        v.end(),
        bind2nd(less<int>(), x)
        ),
    v.end()
    );

看懂了么?实话说,我糊涂了。这一大段程序写的,怎么看怎么像lisp,偏偏又不像lisp那样,能把精力集中在问题本身上面,而要关注过多实现细节。

实话说,在编写和阅读时,必须注意如此多高级特性的语言,已经陷入为了解决一个问题不惜制造另两个问题的怪圈了。

总结 在说这些语言问题的时候,应当注意,很多时候语言的问题并不是语言内在造成一定会出问题,而是现有的语言风格和社区,形成了不良的语言氛围。例如Java哪种绕口令般的架构风格,已经催生了架构师这个特殊职业。这个职业的主要工作,就是按照要求,产生很多类,以及描述,给其他人实现。

语言本身在设计时,一定会考虑简洁易读的问题。然而出于种种理由,简洁易读并不容易。主要原因就是易读和其他特性之间往往冲突。例如,库函数越细,就越容易重用,然而使用上就越罗嗦,而且实现分散,不易阅读。为了使得阅读流畅,因此允许对象重载算符,这又引入了算符优先级不定性问题。通常来说,一个语言的设计,总是基于某个特定想法,试图解决某类问题。因此在冲突的目标间,需要做出抉择,例如python之禅。

语言自身产生不易阅读叫做无可奈何,然而人为的编写不易阅读的代码,而且从未意识到这样做的错误,这就属于不思进取了。

参考:

1.http://www.blogjava.net/dreamstone/archive/2007/08/09/134986.html

2.http://imlsb.blogbus.com/logs/97126472.html