关于编码的思考

[原创] 关于编码的思考

  摘要:代码也分种类?哪种代码能够自动生成?自动生成代码会不会让程序员没有饭吃?或者会颠覆现有的编程模式?

写在前面

  学习编程,再加上实际开发,写代码也有7个年头了。虽然不敢说有多少多少经验,但思考总是有一些的。这两年慢慢发现,原来代码和代码也是不同的。

  编程越来越趋于自动化,尤其在微软的产品里,程序员总是可以很懒惰。但懒惰之余也有顾虑,35岁的年限让很多人从25岁就开始焦虑,就开始“寻求出路”。而我却越来越恋上编程,也曾想过将来做管理。但德鲁克告诉我们,每个人都是自己的管理者,无论从事什么工作。

  现在的工作离编程好像越来越远了,但从事教育,在给别人澄清问题的时候也让自己对很多知识产生了更深入的理解,更重要的是,产生了更深入的思考。

  本文就是很长一段日子以来思考的结果,主要是对代码的一些认识。今后也还将就这些问题继续思考,但决不会停止编程的步伐。

声明性代码和行为性代码

  我认为自己最有价值的思考就是将代码分成了这两种类别——声明性代码和行为性代码。声明性代码这个名词忘了在哪里看到过的(Declarative Codes),而行为性代码这个名词则是我根据这个词的对立面自编出来的。

  声明性代码指的是本身不完成任何实际工作,只是为编译器和运行环境提供与程序相关信息的代码。

  行为性代码指的是真正用来完成实际工作中某一步骤任务的代码。

  就以Hello World为例。

01: using System;
02:
03: namespace AndersLiu.Samples
04: {
05:    public class Hello
06:    {
07:        static void Main()
08:        {
09:            Console.WriteLine("Hello, world!");
10:        }
11:    }
12: }

  除了第09行属于行为性代码以外,其他代码都是声明性代码。例如第01行,其作用仅仅是告诉编译器,对于System命名空间中的类型,程序中可能仅会提到类型的名字,而不会给出完全限定的名字,需要编译器自己进行解析。又如第03、05、07行,分别告诉编译器,这里有个名为AndersLiu.Samples的命名空间、当前命名空间中有个名为Hello的公共类型、当前类型中有一个名为Main静态私有方法,当然最终这些信息会保留在程序集中,指导CLR的运作。

  而第09行,才真正完成程序的功能,向屏幕上写一个字符串。

  现在就可以明确一个问题了。众所周知,一个C#源文件被编译后将生成一个程序集,而程序集中存放的是IL代码和元数据。那么,究竟哪些代码经过编译后会生成IL代码,哪些代码经过编译后会生成元数据呢?答案不言而喻,声明性代码经过编译后要么被丢弃,要么形成元数据;而行为性代码经过编译后,会变成IL代码。声明性代码中,用来指导编译器进行编译的代码将会被丢弃,而用来指导CLR运作的代码,被保留在程序集中形成元数据。

  这是在.NET世界中,那么对于其他编程技术或语言呢?不难发现,任何编程语言的构成结构中都会有这样两种代码存在。而在编程语言之外,有很多纯声明性的语言存在,比如各种标记语言(HTML、XML、VRML等)。不过纯行为性语言的存在值得质疑,目前没有那种语言中仅存在行为性代码,这是因为任何时候编译器都要从代码中获取一些信息,而这些信息几乎总是通过声明性代码来传达的。

  在编程语言之中,声明性语言的最大特征就是没有明显的先后顺序性。例如下面两个变量(或域)的声明:

int i;
int j;

  交换上面这两条语句的顺序,不会对程序产生任何影响。

  而行为性代码就具有很强烈的顺序性,如果两条代码引用了相关的实体,那么调换其顺序必然会导致程序行为的异常。像i++; j++;这样的语句交换顺序没问题,这仅仅是因为两条语句没有什么相关性而已。

  另外,声明性代码和行为性代码并不是以语句作为划分单位的。例如int i = 5;,虽然这是一个完整的语句,但称它是声明性代码或是行为性代码是没有意义的,因为它实际上包含两条代码:int i;(变量声明,声明性代码)和i = 5;(赋值,行为性代码)。不过对于常数声明来说,例如const int c = 5;,尽管其中出现了赋值运算符=,但仍属于声明性代码。

  对于C#语言来说,行为性代码只能出现在方法体、属性/索引器的get/set访问器中。而类型的成员(域、属性、方法和事件)声明,都是声明性代码。那么,为什么上面提到的int i = 5;在C#中可以作为一个类型的域声明呢?来看一个例子,例如有下面这样一个简单的类定义:

namespace AndersLiu.Test
{
    class CSample
    {
        int i = 10;
        static int si = 5;
    }
}

  使用/t:library参数将其编译为一个dll,并使用ILDASM对其进行Dump,可以从返回编出来的代码中看到这样的类定义(其中的中文注释是我自己添加的):

.class private auto ansi beforefieldinit AndersLiu.Test.CSample
extends [mscorlib]System.Object
{
// 两个私有域的定义,一个实例域i,一个静态域si
.field private int32 i
.field private static int32 si

// 编译器自动生成的实例默认构造器 .ctor
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
    // Code size 16 (0x10)
    .maxstack 8
    IL_0000: ldarg.0

    // 为实例域i赋初值10
    IL_0001: ldc.i4.s 10
    IL_0003: stfld int32 AndersLiu.Test.CSample::i

    // 赋值完毕,调用父类实例默认构造器
    IL_0008: ldarg.0
    IL_0009: call instance void [mscorlib]System.Object::.ctor()
    IL_000e: nop
    IL_000f: ret
} // end of method CSample::.ctor

// 编译器自动生成的静态构造器 .cctor
.method private hidebysig specialname rtspecialname static
void .cctor() cil managed
{
    // Code size 7 (0x7)
    .maxstack 8

    // 为静态域赋si初值5
    IL_0000: ldc.i4.5
    IL_0001: stsfld int32 AndersLiu.Test.CSample::si

    // 赋值完毕,返回
    IL_0006: ret
} // end of method CSample::.cctor

  从这里可以看出,编译器自动将带有初始化器的域声明代码拆成了两个部分,域的声明保留在类型(元数据)中,而对域的赋值则被插入到了自动生成的构造器代码的最前面 (形成IL代码)。

  而下面的代码就没有那么幸运了,它们只能作为局部变量,如果将它们作为类型的域定义,就会发生编译错误。

int i = 5;
int j = i + 5;

  这是因为C#编译器只会将无顺序性的(不相关的)行为性代码转移到构造器中,上面的代码明显存在强烈的顺序性,因此编译器为了确保程序行为的一致性,就会拒绝翻译这段代码。

小议代码自动生成

  各大软件公司(姑且这么称呼他们)和各种计算机方面的研究机构都一直在研究代码自动生成的技术。编译器其实是代码自动生成的最好例子,我们编写程序的行为可以认为是在干涉编译器,影响它生成代码的行为。

  不过提到代码自动生成,更多人的直观认识是“源代码的自动生成”,其实本文要讨论的,也正是源代码的自动生成。因此在本文中,如果不是特殊强调,就认为代码自动生成等于源代码自动生成。

  研究机构我不清楚,但在商业领域,在代码自动生成技术上走得最远的恐怕就是微软了。从很久很久以前,微软的各种开发工具(甚至包括像Office这类非严格意义上的开发工具)就都提供了丰富的代码自动生成功能。这些功能体现在各种各样的开发向导上,如著名的MFC应用程序框架、Visual Basic的“所见即所得”开发环境等等。

  过去的代码自动生成功能往往有限,虽然生成了大量的代码,但程序员的工作依然是比较繁重的。且不说很多工作要靠程序员手工完成,遇到生成的代码难以满足系统需求时,修改自动生成的代码的任务量就足以让一般开发爱好者望而却步。甚至,有时自动生成的代码还会出现BUG,要靠程序员手工fix。

  但自动生成代码的功能还是或多或少会在程序员中引发一些焦虑。随着.NET的出现(尤其是Visual Studio 2005的出现),很多代码是不需要程序员来完成的,甚至程序员都可以忽视它们的存在。比如Typed DataSet、Web Services Proxy,有多少一线的程序员有空或者是愿意去看看它们的代码是什么样子的?

  前不久参加了Vista的开发培训,看到无论是WPF、WCF还是WWF,开发起来都不用编写太多的代码,无非就是改改配置文件,或是写上一两句“胶水代码”。难道,程序员的“末日”真的来临了?

  在解决这个问题之前,还是看看究竟哪些代码能够自动生成吧。

  很简单,一句话可以概括:声明性代码和形成了模式的行为性代码可以自动生成。

  以Visual Studio 2005的Class Diagram为例,Class Diagram除了可以将项目中现有代码中的类结构表示出来以外,还可以通过将Toolbox中表示各种类型和类型间关系的图示拖到类图中来完成系统的设计。通过图形化方式进行编程的时代终于正式到来了。

  但是对Class Diagram自动生成的代码稍作观察就能发现,这些代码只是类型的一个大致的“框架”,只是类型及其成员的声明而已——这些不都是声明性代码么。真的遇到方法体或者属性/索引器的get/set访问器,还不是要抛一个NotImplementedException异常出来?

  另外一类能够自动生成的代码是已经形成了模式的行为性代码。例如前面提到的Typed DataSet和Web Services Proxy,其中都有一些行为性代码存在,这些代码很简单,任谁都会编写这样的代码去完成相应的工作,因此可以自动生成。还有一类形成了模式的行为性代码,它们以“语法糖”的形式出现,由编译器在真正的编译开始之前完成代码自动生成工作,例如C#中的foreach语句、using语句(并非“using指令”)和lock语句等。

  此外,Visual Studio 2005中还强调了Code Snippets的概念,这从某些角度来看,也是在自动生成形成了模式的行为性代码。

程序员的出路

  文章进行到这里,结论不言而喻了吧?

  不过这一部分的标题起得不好,仅作为标题使用,请不要就这个标题对我的阐述进行攻击。首先就微软的作风来讲,不会把程序员逼到“找出路”的境地。微软很懂得“生态环境”的概念,明白没有更多的开发者就没有更多的Windows应用,没有更多的Windows应用就没有更多的Windows市场,没有更多的Windows市场,微软也就失去了生机。因此微软“自古以来”就一直给开发者提供者最好的资源和广泛的生存空间,无论是初出茅庐的编程新手,还是深谙理论和实践的技术大师,在微软的平台上都有自己的舞台。今天是这样,今后也是这样。其次,就算程序员真的要“找出路”,这里一没给出最佳选择,二并没有提到所有可供参考的选择,只是说一些自己的想法而已。说到这里不得不申明以下,本文中提到的“程序员”,都特指“微软平台上的程序员”。

  因此,对于想一直搞开发,而不想转向管理者或转向其他领域的程序员来说,这部分内容可能会有一些参考意义。

  综合前几部分的论述,程序员要想找到“出路”,或者想使自己的工作更有意义,无外乎向两个方向发展——一是向宏观发展,做系统架构师,或者至少是做设计者;二就是向微观发展,在没有模式的行为性代码上下功夫。

  当代的开发工具尽管可以极大程度地便利设计工作,但毕竟只是工具,真正的设计思想和设计实践依然是开发者自己的财富。我不敢说设计工作在将来能不能实现自动化(实际上有些已经实现了),但目前很多设计工作还是靠人为来完成的。学会站在一个很高的层次上,对系统整体进行把握,在很长时间内还是可以利于不败之地的。

  然而,设计上也存在很多模式,如果选择作为一个设计者,绝对不能沉浸在已有的模式中沾沾自喜。一旦某日设计工作得以完善的自动化,形成了模式的设计思想将是第一批被设计工具所淹没的。一定要在反复的设计工作中磨练自己的思维,清醒地认识到那些设计已经形成了模式,在套用模式的同时对现有思路进行突破。

  而所谓的行为性代码,其实往往体现着一系列的算法。何谓“算法”?解决特定理论问题的思维模式而已。因此,在学习现有算法的原理和使用现有算法解决问题的同时,仍不要忘记突破模式,寻求更新鲜的思路。如果只是掌握了很多爆难无比的算法的使用,依然寻求不到“出路”,因为它仍然是模式,仍然可以自动生成。比如编译原理,很多人并不能深刻掌握,但有很多工具可以自动生成词法和语法的分析器源代码。那么,掌握了编译原理中的理论,掌握了分析器生成器的使用方法,能解决问题吗?

  总而言之,程序员,优秀的程序员并不好做,以前是这样,以后也是这样。只有提高自己的思维能力,说白了就是做个聪明人,才能有饭吃。不管啥年头,混计算机这行光靠体力活可不行……

posted @ 2006-05-02 23:51  Anders Liu  阅读(8158)  评论(17编辑  收藏  举报