Effective C# 原则30:选择与CLS兼容的程序集

JerryXia 发表于 , 阅读 (2,055)

.Net运行环境是语言无关的:开发者可以用不同的.Net语言编写组件。而且在实际开发中往往就是这样的。你创建的程序集必须是与公共语言系统(CLS)是兼容的,这样才能保证其它的开发人员可以用其它的语言来使用你的组件。

CLS的兼容至少在公共命名上要与互用性靠近。CLS规范是一个所有语言都必须支持的最小操作子集。创建一个CLS兼容的程序集,就是说你创建的程序集的公共接口必须受CLS规范的限制。这样其它任何满足CLS规范的语言都可以使用这个组件。然而,这并不是说你的整个程序都要与CLS的C#语言子集相兼容。

为了创建CLS兼容的程序集,你必须遵从两个规则:首先,所以参数以及从公共的和受保护的成员上反回的值都必须是与CLS兼容的。其次,其它不与CLS兼容的公共或者受保护成员必须存在CLS兼容的同意对象。

第一个规则很容易实现:你可以让编译来强制完成。添加一个CLSCompliant
特性到程序集上就行了:

[ assembly: CLSCompliant( true ) ]

编译器会强制整个程序集都是CLS兼容的。如果你编写了一个公共方法或者属性,它使用了一个与CLS不兼容的结构,那么编译器会认为这是错误的。这非常不错,因为它让CLS兼容成了一个简单的任务。在打开与CLS兼容性后,下面两个定义将不能通过编译,因为无符号整型不与CLS兼容:

// Not CLS Compliant, returns unsigned int:
public UInt32 Foo()
{
    return _foo;
}

// Not CLS compliant, parameter is an unsigned int.
public void Foo2( UInt32 parm )
{

}

记住,创建与CLS兼容的程序集时,只对那些可以在当前程序集外面可以访问的内容有效。Foo
和Foo2 在定义为公共或者受保护时,会因与CSL不兼容而产生错误。然而如果Foo
和Foo2是内部的,或者是私有的,那么它们就不会被包含在要与CLS兼容的程序集中;CLS兼容接口只有在把内容向外部暴露时才是必须的。

那么属性又会怎样呢?它们与CLS是兼容的吗?

public MyClass TheProperty
{
    get { return _myClassVar; }
    set { _myClassVar = value; }
}

这要视情况而定,如果MyClass是CLS兼容的,而且表明了它是与CLS兼容的,那么这个属性也是与CLS兼容的。相反,如果MyClass没有标记为与CLS兼容,那么属性也是与CLS不兼容的。就意味着前面的TheProperty属性只有在MyClass是在与CLS兼容的程序集中是,它才是与CLS兼容的。

如果你的公共的或者受保护的接口与CLS是不兼容的,那么你就不能编译成CLS兼容的程序集。作为一个组件的设计者,如果你没有给程序集标记为CLS兼容的,那么对于你的用户来说,就很难创建与CLS兼容的程序集了。他们必须隐藏你的类型,然后在CLS兼容中进行封装处理。确实,这样可以完成任务,但对于那些使用组件的程序员来说不是一个好方法。最好还是你来努力完成所有的工作,让程序与CLS兼容:对于用户为说,这是可以让他们的程序与CLS兼容的最简单的方法。

第二个规则是取决与你自己的:你必须确保所有公共的及受保护的操作是语言无关的。同时你还要保证你所使用的多态接口中没有隐藏不兼容的对象。

操作符重载这个功能,有人喜欢有人不喜欢。同样,也并不是所有的语言都支持操作符重载的。CLS标准对于重载操作符这一概念即没有正面的支持也没有反正的否定。取而代之是,它为每个操作符定义了一了函数:op_equals就是=操作符所对应的函数名。op_addis是重载了加号后的函数名。当你重载了操作符以后,操作符语法就可以在支持操作符重载的语言中使用。如果某些开发人员使用的语言不支持操作符重载时,他们就必须使用op_这样的函数名了。如果你希望那些程序员使用你的CLS兼容程序集,你应该创建更多的方便的语法。介此,推荐一个简单的方法:任何时候,只要重载操作运算符时,再提供一个等效的函数:

// Overloaded Addition operator, preferred C# syntax:
public static Foo operator+( Foo left, Foo right)
{
    // Use the same implementation as the Add method:
    return Foo.Add( left, right );
}

// Static function, desirable for some languages:
public static Foo Add( Foo left, Foo right)
{
    return new Foo ( left.Bar + right.Bar );
}

最后,注意在使用多态的接口时,那些非CLS的类型可能隐藏在一些接口中。最容易出现的就是在事件的参数中。这会让你创建一些CLS不兼容的类型,而在使用的地方却是用与CLS兼容的基类。
假设你创建了一个从EventArgs派生的类:

internal class BadEventArgs : EventArgs
{
    internal UInt32 ErrorCode;
}

这个BadEventArgs类型就是与CLS不兼容的,你不可能在其它语言中写的事件句柄上使用这个参数。但多态性却让这很容易发生。你只是申明了事件参数为基类:EventArgs:

// Hiding the non-compliant event argument:
public delegate void MyEventHandler(object sender, EventArgs args );

public event MyEventHandler OnStuffHappens;

// Code to raise Event:
BadEventArgs arg = new BadEventArgs();
arg.ErrorCode = 24;

// Interface is legal, runtime type is not:
OnStuffHappens( this, arg );

以EventArgs为参数的接口申明是与CLS兼容的,然而,实际取代参数的类型是与CLS不兼容的。结果就是一些语言不能使用。

最后以如何实现CLS兼容类或者不兼容接口来结束对CLS兼容性的讨论。兼容性是可以实现的,但我们可以更简单的实现它。明白CLS与接口的兼容同样可以帮助你完整的理解CLS兼容的意思,而且可以知道运行环境是怎样看待兼容的。

这个接口如果是定义在CLS兼容程序集中,那么它是CLS兼容的:

[assembly:CLSCompliant(true)]
public interface IFoo
{
    void DoStuff(Int32 arg1, string arg2 );
}

你可以在任何与CLS兼容的类中实现它。然而,如果你在与没有标记与CLS兼容的程序集中定义了这个接口,那么这个IFoo接口就并不是CLS兼容的接口。也就是说,一个接口只是满足CLS规范是不够的,还必须定义在一个CSL兼容的程序集中时才是CLS兼容的。原因是编译器造成的,编译器只在程序集标记为CLS兼容时才检测CLS兼容类型。相似的,编译器总是假设在CLS不兼容的程序集中定义的类型实际上都是CLS不兼容的。然而,这个接口的成员具有CLS兼容性标记。即使IFoo没有标记为CLS兼容,你也可以在CLS兼容类中实现这个IFoo接口。这个类的客户可以通过类的引来访问DoStuff,而不是IFoo接口的引用。

考虑这个简单的参数:

public interface IFoo2
{
    // Non-CLS compliant, Unsigned int
    void DoStuff( UInt32 arg1, string arg2 );
}

一个公开实现了IFoo2接口的类,与CLS是不兼容的。为了让一个类即实现IFoo2接口,同时也是CLS兼容的,你必须使用清楚的接口定义:

public class MyClass: IFoo2
{
    // explicit interface implementation.
    // DoStuff() is not part of MyClass's public interface
    void IFoo2.DoStuff( UInt32 arg1, string arg2 )
    {
        // content elided.
    }
}

MyClass 有一个与CLS兼容的接口,希望访问IFoo2
接口的客户必须通过访问与CLS不兼容的IFoo2接口指针。

兼容了吗?不,还没。创建一个CLS兼容类型要求所有的公共以及受保护接口都只包含CLS兼容类型。这就是说,某个类的基类也必须是CLS兼容的。所有实现的接口也必须是CLS兼容的。如果你实现了一个CLS不兼容的接口,你必须实现明确的接口定义,从而在公共接口上隐藏它。

CLS兼容性并没有强迫你去使用最小的公共名称来实现你的设计。它只是告诉你应该小心使用程序集上的公共的接口。以于任何公共的或者受保护的类,在构造函数中涉及的任何类型必须是CLS兼容的,这包含:
*基类
*从公共或者受保护的方法和属性上返回的值
*公共及受保护的方法和索引器的参数
*运行时事件参数
*公共接口的申明和实

编译器会试图强制兼容一个程序集。这会让提供最小级别上的CLS兼容变得很简单。再稍加小心,你就可以创建一个其它语言都可以使用的程序集了。编译器的规范试图确保不用牺牲你所喜欢的语言的结构就可以尽可能的与其它语言兼容。你只用在接口中提供可选的方案就行了。

CLS兼容性要求你花点时间站在其它语言上来考虑一下公共接口。你不必限制所有的代码都与CLS兼容,只用避免接口中的不兼容结构就行了。通用语言的可操作性值得你花点时间。

添加新评论