Effective C#: 3.尽量使自定义的类型与公共语言规范兼容
陈铭 Microsoft C#/.NET Asia MVP
难度:5/10 条款2
一个阳光明媚的早上,你精神百倍的坐到自己的电脑前面,准备开始一天繁忙的工作。今天的工作内容是编写一个根据用户的订单自动发送确认邮件的程序——毫无疑问,工具是你最拿手的C#。
这时候你突然想到你的一位执著于Visual Basic.NET的同事曾经编写过一个用于发送电子邮件的小组件,为什么不直接使用现成的组件来简化手头上的工作呢?不是说.NET的优点之一就是简便快速的跨语言组件重用吗?看上去是个相当不错的主意。
拿到这个组件和相关的文档并不费什么周折,但是调用文档上的一段文字却让你如坠五里雾中:“……设置邮件地址之后,调用Mail类对象的Mail方法发送邮件……”。似乎函数的名字用Send更好一些,这也还罢了。Mail类的Mail方法,难道是构造函数?怎么可能呢?或者是文档写错了?
困惑无济于事,不如先来看看Visual Basic.NET的一些语法吧:在VB.NET中当然也可以定义类、定义类的构造函数,但其语法却和C#大相径庭——VB.NET不是用类名称,而是用关键字New来定义构造函数的,如下所示:
‘VB.NET definition for class Mail
Public Class Mail
Sub New()
‘Here is the constructor!
End Sub
‘ Other type members
Sub Mail()
‘This is only a member function
‘Not class constructor
End Sub
End Class
由于使用了New来定义构造函数,VB.NET就可以定义一个和类型同名的成员函数,而这在C#里是绝不可能做到的。在编译之后,构造函数在类型元数据(Metadata)中的名称为.ctor,这个名称显然和类型名并不冲突。因此,成员函数名不能与类型名称相同应该不是.NET平台的限制而是C#语法上的限制。
那么.NET平台上的跨语言交互和重用有从何谈起呢?在理解.NET对跨语言交互重用的支持之前,有必先了解以下的两个概念:公共类型系统(CTS, Common Type System)和公共语言规范(CLS, Common Language Specification)。
.NET构建在一个丰富而庞大的类型系统之上,这个类型系统的设计目的之一就是尽可能多的支持现有的各种程序语言的特性,从过程式语言到面向对象语言,甚至包括了诸如LISP的函数式语言(如果你从来没有接触过函数式语言,那么.NET支持的TailCall函数调用方式可能会令你吃惊不小)。微软为这个作为.NET建筑根基的类型系统定名为公共类型系统,即CTS。
由于CTS具有如此完备的定义,构建于其上的.NET——更具体地说是.NET的“汇编语言”MSIL——具有非常强大的表达能力,例如:使用MSIL可以在类集中定义不属于任何类的全局函数和变量(你没有看错,.NET确实支持全局变量),支持仅有返回值类型不同的函数重载(可以在条款X中找到这种重载的应用实例),甚至支持为接口定义静态成员变量和方法。
进而,正是MSIL丰富的表达能力,才使得.NET能够海纳百川般的将各式各样的程序设计语言纳入.NET框架之中。从为.NET量身定做的C#,改头换面之后的C++(Managed C++ Extension)、Visual Basic(VB.NET)到Eiffel,甚至是像Jscript.NET和Python这样的脚本语言,.NET平台目前支持的程序语言已经有十几种之多,而且仍然不断的有新的成员加入到这个群体中来。不需要经历漫长而陡峭的学习曲线就可以在全新的.NET平台上应用自己熟知的程序语言进行应用程序开发,这对于现实中的程序员来说无疑是一个莫大的福音。
但是,至此我们还只能说“.NET是一个支持多种语言开发的环境”,不同语言之间的交互仍然无从谈起。虽然采用了统一的类型信息存储形式和中间代码,但是不同的语言表达能力不尽相同,支持的语法也各异。这就使得不同的.NET程序设计语言暴露出的CTS的功能子集不完全相同。比如说,C#支持的运算符重载在VB.NET中没有直接对应的调用方法、而函数式语言的所谓TailCall函数调用方式更是不可能被其它.NET语言所支持。
为此,.NET又在CTS的基础上定义了公共语言规范(CLS, Common Language Specification)。与CTS不同,CLS并不追求功能的完备性,而是着重规定了各种.NET语言在功能上必须实现的CTS的一个子集,以及如何在程序中正确的使用这个集合内的功能。由于CLS的出发点是不同语言之间的交互性,所以它只适用于从类集外部可访问的类型及其成员,例如类集中声明为public的类型及其声明为public/protected的成员变量和成员函数。只要确保这些从类集以外可以访问的类型及其成员符合CLS规范,就能够保证所设计的类库能够在几乎所有.NET程序语言中毫无困难的使用,从而实现.NET的跨语言交互能力。至于其它私有类型及其成员,则可以充分发挥特定语言所特有的表达能力,物尽其用,以求最大限度的简化编程工作。比如说VB.NET直接支持COM对象的晚绑定(late binding),而在诸如C#的其它.NET语言当中实现同样的功能并不容易,那么就可以考虑由VB.NET编写进行COM对象操作的类集,用C#实现应用的其它部分,只要它们所暴露的界面符合CLS的规定,各模块之间就可以轻而易举的实现无缝集成。
完整的CLS规范非常琐碎细致的规定了.NET程序设计的方方面面(幸好,并不是必须记住这些条款才能确保你的程序符合CLS规范,编译器可以完成大部分的检查工作,稍后会有更详细的介绍)。下表罗列了CLS的部分规则,熟悉这些较为常用的规则将会有助于更明确的理解CLS的意义:
关于命名 | 字符和大小写 | 不能仅凭大小写的不同来区分两个标识 |
标识的唯一性 | 除重载以外,同一名称解析空间中不能有 任意两个同名的标识 | |
函数声明 | 函数声明中用到的参数和返回值类型必须与CLS兼容 | |
关于类型 | 内建类型 | .NET内建类型中,仅有Byte、Int16、Int32、Int64、Single、Double、Boolean、Char、Decimal、IntPtr和String是CLS兼容的* |
闭合特性 | 与CLS兼容的接口和抽象基类的所有成员必须保证与CLS兼容 | |
构造函数调用 | 在访问任何类成员之前,子类的构造函数必须先调用父类的构造函数 | |
类型成员 | 重载 | 函数、属性和事件不能仅凭返回值类型的不同进行重载 |
* 这些类型在C#里分别对应于:byte, short, int, long, float, double, bool, char, decimal和string
这里罗列的仅是CLS规范的极小的一个部分。要求程序员记住这些繁琐的规则条款似乎有些不切实际,因此,各种.NET语言的编译器均提供了CLS兼容性检测的功能。只要你为那些希望与CLS规范兼容的类集添加上CLSCompliantAttribute特性,编译器就会帮助你检查代码是否符合CLS规范,并且给出必要的错误信息。
例如,你可以尝试编译下面的C#代码:
//clstest.cs
using System;
[assembly:CLSCompliant(true)]
namespace Effective.Csharp.Chapter3 {
public class MyClass {
//uint类型不与CLS兼容
public uint GetMyData() {
//… 函数的具体实现
}
}
}
编译器就会产生出如下的错误信息:
clstest.cs(7,10): error CS3002: Return type of
'Effective.Csharp.Chapter3.MyClass.GetMyData()' is not CLS-compliant
而如果将GetMyData函数的访问设置改成private或者internal,编译器就不会再报类似的错误了。
这样,有了编译器的帮助,确保你所设计的类型与CLS兼容就变得轻而易举了。但是,有些类库最初就未能按照CLS兼容的标准设计(就向本章起始提到的那个例子),而你仅仅是这个类库的用户,当这些类库触及不同语言功能交互的死角的时候,我们不就无能为力了吗?其实不然,.NET提供了异常强大的类型反射(Reflection)功能,可以用于访问那些程序设计语言力所不能及的类型及其成员,例如针对本章开头提到的例子,可以使用类型反射来调用Mail方法:
//…use reflection to invoke Mail.Mail
Mail mail = new Mail(“someone@somewhere.com”);
//… initialize mail object
Type t = typeof(Mail);
//invoke Mail method
t.InvokeMember(“Mail”, BindingFlags.InvokeMethod,
null, mail, new object[] {} );
显而易见,类型反射的功能虽然强大,但是使用类型反射的代码的性能和可读性会大大降低。因此,作为组件的设计者,应该尽量确保组件定义的类型与CLS兼容。(完)