** 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(“[email protected]”);
//… initialize mail object
Type t = typeof(Mail);
//invoke Mail method
t.InvokeMember(“Mail”, BindingFlags.InvokeMethod,
null, mail, new object[] {} );
显而易见,类型反射的功能虽然强大,但是使用类型反射的代码的性能和可读性会大大降低。因此,作为组件的设计者,应该尽量确保组件定义的类型与 CLS 兼容。 ( 完 )
- 本文系原创作品,未经作者本人许可请勿转载。