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

Effective C# 原则30:选择与CLS兼容的程序集
Item 30: Prefer CLS-Compliant Assemblies

.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兼容,只用避免接口中的不兼容结构就行了。通用语言的可操作性值得你花点时间。

===============================

   

Item 30: Prefer CLS-Compliant Assemblies
The .NET environment is language agnostic: Developers can incorporate components written in different .NET languages without limitations. In practice, it's almost true. You must create assemblies that are compliant with the Common Language Subsystem (CLS) to guarantee that developers writing programs in other languages can use your components.

CLS compliance is a new twist on that least common denominator approach to interoperability. The CLS specification is a subset of operations that every language must support. To create a CLS-compliant assembly, you must create an assembly whose public interface is limited to those features in the CLS specification. Then any language supporting the CLS specification must be capable of using the component. This does not mean you must limit your entire programming palette to the CLS-compliant subset of the C# language, however.

To create a CLS-compliant assembly, you must follow two rules. First, the type of all parameters and return values from public and protected members must be CLS compliant. Second, any non-CLS-compliant public or protected member must have a CLS-compliant synonym.

The first rule is simple to follow: You can have it enforced by the compiler. Add the CLSCompliant attribute to your assembly:

[ assembly: CLSCompliant( true ) ]

 

The compiler enforces CLS compliance for the entire assembly. If you write a public method or property that uses a construct that is not compliant with CLS, it's an error. That's good because it makes CLS compliance an easy goal. After turning on CLS compliance, these two definitions won't compile because unsigned integers are not compliant with 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 )
{
}

 

Remember that creating a CLS-compliant assembly affects only items that can be seen outside of the current assembly. Foo and Foo2 generate CLS compliance errors when declared either public or protected. However, if Foo and Foo2 were internal, or private, they could be included in a CLS-compliant assembly; CLS-compliant interfaces are required only for items that are exposed outside the assembly.

What about this property? Is it CLS compliant?

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

 

It depends. If MyClass is CLS compliant and indicates that it is CLS compliant, this property is CLS compliant. On the other hand, if MyClass is not marked as CLS compliant, this property is not CLS compliant. That means that the earlier TheProperty is CLS compliant only if MyClass resides in a CLS-compliant assembly.

You cannot build a CLS-compliant assembly if you have types in your public or protected interface that are not CLS compliant. If, as a component designer, you do not have an assembly marked as CLS compliant, you make it harder for users of your component to create CLS-compliant assemblies. They must hide your types and mirror the functionality in a CLS-compliant wrapper. Yes, this can be done. But, no, it's not a good way to treat the programmers who want to use your components. It's better to strive for CLS-compliant assemblies in all your work: This is the easiest way for clients to incorporate your work in their CLS-compliant assemblies.

The second rule is up to you: You need to make sure that you provide a language-agnostic way to perform all public and protected operations. You also need to make sure that you do not sneak a noncompliant object through your interface using polymorphism.

Operator overloading is a feature that some love and others hate. As such, not every language supports or allows operator overloading. The CLS standard does not take a pro or con stance on the concept of operator overloading. Instead, it defines a function name for each operator: op_equals is the function name created when you write an operator = function. op_addis the name for an overloaded addition operator. When you write an overloaded operator, the operator syntax can be used in languages that support overloaded operators. Developers using a language that does not support operator overloading must use the op_ function name. If you expect these programmers to use your CLS-compliant assembly, you should provide a more convenient syntax. That leads to this simple recommendation: Anytime you overload an operator, create a semantically equivalent function:

// 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 );
}

 

Finally, watch out for non-CLS types sneaking into an interface when you use polymorphic arguments. It's easy to do with event arguments. You can create a type that is not compliant with CLS and use it where a base type that is CLS-compliant is expected.

Suppose that you created this class derived from EventArgs:

internal class BadEventArgs : EventArgs
{
  internal UInt32 ErrorCode;
}

 

The BadEventArgs type is not CLS compliant; you should not use it with event handlers written in other languages. But polymorphism makes this easy to do. You can declare the event type to use the base class, 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 );

 

The interface declaration, which uses an EventArgs argument, is CLS compliant. However, the actual type you substituted in the event arguments was not. The end result is a type that some languages cannot use.

This discussion of CLS compliance ends with how CLS-compliant classes implement compliant or noncompliant interfaces. It can get complicated, but we'll simplify it. Understanding CLS compliance with interfaces also will help you fully understand what it means to be CLS compliant and how the environment views compliance.

This interface is CLS compliant if it is declared in a CLS-compliant assembly:

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

 

You can implement that interface in any CLS-compliant class. However, if you declare this interface in an assembly that is not marked as CLS compliant, the IFoo interface is not CLS compliant. In other words, an interface is CLS compliant only if it is defined in a CLS-compliant assembly; conforming to the CLS spec is not enough. The reason is compiler performance. The compilers check CLS compliance on types only when the assembly being compiled is marked as CLS compliant. Similarly, the compilers assume that types declared in assemblies that are not CLS compliant actually are not CLS compliant. However, the members of this interface have CLS-compliant signatures. Even if IFoo is not marked as CLS compliant, you can implement IFoo in a CLS-compliant class. Clients of this class could access DoStuff tHRough the class reference, but not through the IFoo reference.

Consider this small variation:

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

 

A class that publicly implements IFoo2 is not CLS compliant. To make a CLS-compliant class that implements IFoo2, you must use explicit interface implementation:

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 has a CLS-compliant public interface. Clients expecting the IFoo2 interface must access it through the non-CLS-compliant IFoo2 pointer.

Complicated? No, not really. Creating a CLS-compliant type mandates that your public and protected interfaces contain only CLS-compliant types. It means that your base class must be CLS compliant. All interfaces that you implement publicly must be CLS compliant. If you implement a non-CLS compliant interface, you must hide it from your public interface using explicit interface implementation.

CLS compliance does not force you to adopt a least common denominator approach to your designs and implementations. It means carefully watching the publicly accessible interfaces of your assembly. For any public or protected class, any type mentioned in these constructs must be CLS compliant:

Base classes

Return values for public and protected methods and properties

Parameters for public and protected methods and indexers

Runtime event arguments

Public interfaces, declared or implemented

The compiler tries to enforce a compliant assembly. That makes it easy for you to provide some minimum level of CLS support. With a bit of extra care, you can create an assembly that anyone using any language can use. The CLS specification tries to ensure that language interoperability is possible without sacrificing the constructs in your favorite language. You just need to provide alternatives in the interface.

CLS compliance requires you to spend a little time thinking about the public interfaces from the standpoint of other languages. You don't need to restrict all your code to CLS-compliant constructs; just avoid the noncompliant constructs in the interface. The payback of interlanguage operability is worth the extra time.
 
   

你可能感兴趣的:(effective)