首页 > 代码库 > 《CLR via C#》精髓:C#是个“骗子”!

《CLR via C#》精髓:C#是个“骗子”!

其实,CLR非常“单纯”,他远没有C#(确切说应该是C#编译器,下同)那样“狡猾”!多年以来,C#一直对CLR隐瞒着许多事情。这不仅欺骗了纯真的CLR,更迷惑了数以万计天真的开发者,让他们误以为很多事都是CLR干的。

随着C#年龄的增长(版本升级),他的这种欺骗行为也愈演愈烈。CLR越来越无助,开发者越来越迷茫。是真相大白于天下的时候了,是细数C#“罪状”的时候了。

1、CLR根本不知道“命名空间”(Namespace)这回事,都是C#编出来“骗人”的。

C#编译器编译你的代码时,会自动把命名空间名称和类型名称拼到一起,形成类型全名(Full name,如:“System.IO”+“FileStream”形成“System.IO.FileStream”)。CLR对此一无所知,他看到的永远都是类型全名,并认为类型名称理应如此。

C#引入命名空间只是为了让你少打几个字,不必每次都繁琐地写下一个类型的完整名称。另外,命名空间还是一种对类型进行逻辑分类的方法(让相似功能或用途的类型在同一个命名空间内)。

但是,请记住,命名空间名称与程序集(Assembly)名称没有半点关系,虽然大多数程序集(尤其是FCL中)的名称里都包含了命名空间,但这只是一种习惯,并不是一种标准,一个程序集内可能包含多个命名空间,一个命名空间也可能存在于多个程序集内。

2、CLR不知道什么叫“可选参数”(Optional Parameter),这都是C#整出来的名词。

当C#编译器发现你为一个方法的参数设定了一个默认值时,他会“偷偷地”在参数上做上些标记(自动增加两个特性:System.Runtime.InteropServices.OptionalAttribute和System.Runtime.InteropServices.DefaultParameterValueAttribute),并在编译时为其生成特殊元数据(你所设定的默认值也将以常量的形式存于元数据中)。当该方法被调用时,如果传递的参数省略了这个可选参数,C#编译器会从元数据中取出默认值,自动为你补全参数。对于上述整个过程,CLR全然不知。

3、CLR“不知有var,无论dynamic。”

详见既生dynamic何生var一文。

4、C#一边告诉我们ref和out有本质区别,一边又对CLR说他们俩是一回事。

C#编译器会为ref和out生成完全相同的元数据,所以在CLR看来他们没有区别。相反,C#编译器对开发者则比较“苛刻”。他让我们区分ref和out,其实是希望我们能够显式地表明我们的意图:参数会在方法外被初始化(ref)还是在方法内被初始化(out)。当C#编译器通过ref或out窥探到我们的意图后,便可据此完成语法检测,以保证参数确实在我们所设想的地方完成初始化。

顺便说一下,CLR允许基于方法参数是否使用了ref和out来进行方法重载(Overload),例如:

public sealed class Point {
    static void Add(Point p) { }
    static void Add(ref Point p) { }
}

但不允许重载方法时仅通过ref和out区分,例如:

// 编译出错
public sealed class Point {
    static void Add(ref Point p) { }
    static void Add(out Point p) { }
}

原因很简单:CLR认为ref和out是一回事。

5、CLR对C#说:“方法的参数数量是有限的”,C#却转告我们:“方法的参数数量可以是无限的。”

C#向我们承诺,只要声明方法参数时使用了params关键字,以后就可以向该方法传递无限数量的参数。String类型的Format静态方法就是这方面的典型应用。但是,CLR基本就不认得params这个词,更不可能允许一个方法接受无限多个参数。

事实上,当C#编译器发现我们声明方法参数时使用了params关键字后,他将自动为此参数添加一个System.ParamArrayAttribute特性。当此方法被调用时,C#编译器会将以逗号分隔的参数序列打包为一个数组后传给该方法。CLR对此过程一无所知,只知道一个带有数组参数的方法被调用了。

6、(待续)

 

虽说C#一直在做“欺上瞒下”的事,但作为开发者,我们一定要弄清事实真相,避免像CLR那样被C#所“迷惑”——即便这是C#的一种“善举”,我们也应尽知详情。

仅以此文献给CLR和开发者。愿CLR洗脱“不白之冤”;愿开发者从此了然不惑。