Effective C# Item 30: Prefer CLS-Compliant Assemblies
.Net环境是与语言无关的:开发者可以不受限制的将各种不同的.Net语言编写的组件结合起来。我们必须创建遵从于CLS(公共语言规范)的程序集以便其他编程语言的开发者可以使用我们的组件。
CLS是每种语言必须支持的操作的子集。要创建一个符合CLS的程序集,我们必须将这个程序集的接口限制在符合CLS的范围之内。这样任何一种符合CLS的语言都可以使用这个程序集。不过这并不是要求我们在整个编程过程中都使用这些符合CLS的语言子集。
要创建符合CLS的程序集,我们必须遵循两条规则。首先所有公开和受保护成员的参数和返回值的类型必须是符合CLS的。其次,对于任何不符合CLS的公开或受保护成员,必须有相当的符合 CLS 的替换成员。
第一个规则很容易达到。我们可以通过为程序集添加CLSCompliant属性来强制编译器进行检测。
这样编译器会对整个程序集强制要求符合公共语言规范。当我们的方法或属性达不到要求时就会生成一个警告。这使得我们可以方便的达到目标。下面两个例子不能通过要求,因为32位无符号整数是不符合CLS的。
{
}
public void Foo2(UInt32 parm)
{
}
我们应当明白创建符合CLS规范的程序集只会涉及到那些外部可见的成员。当Foo和Foo2被声明为public或protected时会出现不符合CLS的警告。但是如果他们是private或internal的话,就可以包含在符合CLS规范的程序集内。CLS接口规范只要求那些暴露在外的程序集项目。
下面的属性是否符合CLS呢?
{
get
{
return _myClassVar;
}
set
{
_myClassVar = value;
}
}
这要取决于MyClass是否符合CLS了。只有当MyClass符合CLS的情况下该属性才符合CLS。
当我们的公开和受保护的接口不符合CLS时,我们是不能创建符合CLS的程序集的。从组件设计的角度上来看,这使得我们的组件很难被那些希望创建符合CLS的程序集应用。他们必须将这些接口隐藏起来,创建相同功能性的包装。虽然这是可行的,但是对于组件使用者来说并不是一个好的做法。我们应当在工作中尽量遵从CLS,对于那些需要创建符合CLS的程序集的用户来说,这是最简便的做法。
第二个规则要求我们确保所有公开和受保护的操作都是和语言无关的。另外我们也需要确保没有这种不符合的对象隐藏在应用了多态的接口中。
操作符重载这个特性有的人喜欢,有的人讨厌。并不是所有的语言都支持和允许操作符重载。CLS标准对于操作符重载的概念并没有表示支持或者反对。它为每一个操作符定义了一个函数名称:当我们使用=操作符时就创建了名为op_equals的函数。当我们重载了+运算符时就创建名为op_add的函数。当我们重载了运算符时,这些新的语义就可以在支持运算符重载的语言中使用了。对于那些使用不支持运算符重载的开发者来说,他们必须使用“op_运算符函数”这样的名称。如果我们希望这些开发者能使用我们的程序集,那么我们就需要提供更加清晰的语义。这里推荐一个比较简单的方法:每当我们重载操作符,应当再创建一个相同功能的函数。
{
return Foo.Add(left, right);
}
public static Foo Add(Foo left, Foo right)
{
return new Foo(left.Bar + right.Bar);
}
最后我们要注意在接口的多态中是否隐藏有不符合CLS的类型。这种情况比较容易出现在事件的参数中。我们可能会创建一个不符合CLS的类型并在使用其基类时要求其符合CLS。
假设我们创建这这样一个派生自EventArgs的类:
{
internal UInt32 ErrorCode;
}
这个BadEventArgs类是不符合CLS的。我们在其他语言中不能使用这样的事件句柄。但是通过多态我们可以简单的做到。我们可以声明一个基类的事件类型EventArgs:
public event MyEventHandler OnStuffHappens;
BadEventArgs arg = new BadEventArgs();
arg.ErrorCode = 24;
OnStuffHappens(this, arg);
在接口的声明中,我们使用的是EventArgs参数,这是符合CLS的。但是实际上所用的类型却是不符合的。这使得它在一些语言中无法使用。
最后我们讨论一下如何创建符合CLS的类和不符合的接口。这可能非常复杂,这里我们简单的介绍一下。了解符合CLS的几口有助于我们全面理解CLS的含义以及运行环境是如何看待这种兼容的。
当我们为接口声明为符合CLS时,它就是符合CLS的。
public interface IFoo
{
void DoStuff(Int32 arg1, string arg2);
}
我们可以在任何符合CLS的类声明中这样的接口。然而,如果在没有标记为符合CLS的程序集中声明这样的接口,它就不是符合CLS的。换句话说,只有定义在声明为符合CLS的程序集中的接口才是符合CLS的。仅仅声明接口是不足以达到目的的。这种情况是由于编译器造成的。只有当程序集声明符合CLS时,它才检查每个类型是否符合CLS。在没有声明的时候,编译器总是假设该程序集是不符合CLS的,其中的类,即便是该类声明为CLS兼容,也被认为是不符合CLS的。
我们来看下面的这种情况:
{
void DoStuff(UInt32 arg1, string arg2);
}
一个类公开的实现了不符合CLS的IFoo2接口。如果想要这个实现了IFoo2接口的类符合CLS,那么我们必须使用接口的显式实现。
{
void IFoo2.DoStuff(Int32 arg1, string arg2)
{
}
}
MyClass拥有一个符合CLS的公开接口。期望使用IFoo2接口的客户端必须通过不符合CLS的IFoo2来访问它。
但是这样做还不够。创建一个符合CLS的类型要求所有的公开和受保护接口只包含符合CLS的类型。这意味着我们的基类必须是符合CLS的。实现的所有接口也必须是符合CLS的。如果实现了一个非CLS的接口,那么我们就必须使用显式接口实现来从公共接口中隐藏它。
符合CLS并不是强制我们要采用最普遍的公用名称来进行设计和实现。它要求我们关注程序集中的公共接口。对于任何公开或受保护的类来说,下面提到的几项都应当是符合CLS的:
1、 基类
2、 公开和受保护的函数和属性的返回值
3、 公开和受保护的函数和索引的参数
4、 事件的参数
5、 公共接口的声明和实现
我们只要多注意一些,就可以创建一个可以被其他语言使用的程序集了。CLS会确保语言的交互性。我们需要多花上一点时间在公共接口上。并不是代码中所有的类型都必须是CLS的,只要避免那些不符合CLS的类出现在公共接口中就可以了。跨语言互操作性值得我们花费这些时间。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
回到目录