代码改变世界

1.8 公共语言规范

2011-11-18 10:41  iRead  阅读(1189)  评论(0编辑  收藏  举报

  COM允许使用不同语言创建的对象相互通信你。现在,CLR集成了所有语言,允许在一种语言中使用由另一种语言创建的对象。之所以能实现这样的集成,是因为CLR使用了标准类型集、元数据(自描述的类型信息)以及公共执行环境。

  语言的集成是一个宏伟的目标,最棘手的问题是各种编程语言存在极大的区别。例如,有的语言在处理符号时不区分大小写,有的语言不支持unsigned(无符号)整数、操作符重载或者参数数量可变的方法。

  要创建很容易从其他编程语言中访问的类型,只能从自己的编程语言中挑选其他所有语言都确定支持的那些功能。为了在这个方面提供帮助,Microsoft定义了一个“公共语言规范”(Common Language Specification,CLS),它详细定义了一个最小功能集。任何编译器生成的类型要想兼容于由其他“符合CLS、面向CLR的语言”所生产的组件,就必须支持这个最小功能集。

  CLR/CTS支持的功能比CLS定义的子集多得多。如果不关心语言之间的互操作性,可以开发一套功能非常丰富的类型,它们仅受你选用的那种语言的功能集的限制。具体地说,在开发类型和方法的时候,如果希望它们对外“可见”,能够从符合CLS的任何一种编程语言中访问,就必须遵守由CLS定义的规则。注意,假如代码只是从定义(这些代码的)程序集的内部访问,CLS规则就不适用了。图1-6形象地演示了这一段想要表达的意思。

图1-6  每种语言都提供了CLR/CTS的一个子集以及CLS的一个超集(但不一定是同一个超集) 

  如图1-6所示,CLR/CTS提供了一个功能集。有的语言公开了CLR/CTS的一个较大的子集。例如,假定开发人员使用IL汇编语言写程序,就可以使用CLR/CTS提供的全部功能。但是,其他大多数语言(比如C#、Visual Basic和Fortran)只向开发人员公开了CLR/CTS的一个功能子集。CLS定义了所有语言都必须支持的一个最小功能集。

  用一种语言定义一个类型时,如果希望在另一种语言中使用该类型,就不要在该类型的public和protected成员中使用位于CLS外部的任何功能。否则,其他开发人员使用其他语言写代码时,就可能无法访问这个类型的成员。

  以下代码使用C#定义一个符合CLS的类型。然而,类型中含有几个不符合CLS的构造,造成C#编译器报错:  

using System;
//  告诉编译器检查CLS相容性
[assembly: CLSCompliant(true)]

namespace SomeLibrary
{
    //  因为是public类,所以会显示警告

    public sealed class SomeLibraryType
    {
        //  警告:SomeLibrary.SomeLibraryType.Abc()的返回类型不符合CLS
        public UInt32 Abc() { return 0; }

        //  警告:仅大小写不同的标识符SomeLibrary.SomeLibraryType.abc()不符合CLS
        public void abc() { }

        //  不会显示警告:该方法是私有的
        private UInt32 ABC() { return 0; }

    }
}

  上述代码将[assembly:CLSCompliant(true)]这个attribute1应用于程序集。这个attribute告诉编译器检查public类型,判断是否存在任何不合适的构造,阻止了从其他编程语言中访问该类型。上述代码编译时,C#编译器会报告两条警告消息。第一个警告是因为Abc方法返回了一个无符号整数;有一些语言是不能操作无符号整数值的。第二个警告是因为该类型公开了两个public方法,这两个方法(Abc和abc)只是大小写和返回类型有别。Visual Basic和其他一些语言无法区别这两个方法。

  有趣的是,删除sealed class SomeLibraryType之前的public字样,然后重新编译,两个警告都会消失。因为这样一来,SomeLibraryType类型将默认为internal(而不是public),将不再向程序集的外部公开。要获得完整的CLS规则列表,请参见.NET Framework SDK文档的“跨语言互操作性”一节(http://msdn.microsoft.com/zh-cn/library/730f1wy3.aspx)。

  现在,让我们提炼一下CLS的规则。在CLR中,一个类型的每个成员要么是一个字段(数据),要么是一个方法(行为)。这意味着每一种编程语言都必须能访问字段和调用方法。这些字段和方法通过特殊或者通用的方式来使用。为了编程进行编程,语言通常提供了额外的抽象,对这些常见的编程模式进行简化。例如,语言可能公开枚举、数组、属性、索引器、委托、事件、构造器、析构器、操作符重载、转换操作符等概念。编译器在源代码中遇到上述任何一种构造,必须将其转换成字段和方法,使CLR和其他编程语言能够访问这些构造。

  在以下类型定义中,包含一个构造器、一个析构器、一些重载的操作符、一个属性、一个索引器以及一个事件。注意,这些代码的目的只是让代码更够编译,并不代码实现一个类型的正确方式。  

using System;

internal sealed class Test
{
    //  构造器
    public Test() { }

    //  析构器
    ~Test() { }

    //  操作符重载
    public static bool operator ==(Test t1, Test t2)
    {
        return true;
    }
    public static bool operator !=(Test t1, Test t2)
    {
        return false;
    }

    //  一个操作符重载
    public static Test operator +(Test test1, Test test2) { return null; }

    //  一个属性
    public string AProperty
    {
        get { return null; }
        set { }
    }

    //  一个索引器
    public string this[int x]
    {
        get { return null; }
        set { }
    }

    //  一个事件
    event EventHandler AnEvent;
}

  编译器编译上述代码会得到一个类型,其中包含大量字段和方法。可以使用.NET Framework SDK配套提供的IL反汇编器工具(ILDasm.exe)来检查最终生成的托管模块,如果1-7所示。

图1-7  ILDasm显示了Test类型的字段和方法(从元数据中提取)

  表1-4总结了编程语言的各种构造与CLR字段CLR字段/方法的对应关系。

  表1-4  Test类型的字段和方法(从元数据中提取)

类型的成员 成员的类型 对应的编程语言构造
AnEvent 字段 事件;字段名是AnEvent,类型是System.EventHandler
.ctor 方法 构造器
Finalize 方法 析构器
add_AnEvent 方法 事件的add访问器方法
get_AProperty 方法 属性的get访问器方法
get_Item 方法 索引器的get访问器方法
op_Addition 方法 +操作符
op_Equality 方法 ==操作符
op_Inequality 方法 !=操作符
remove_AnEvent 方法 事件的remove访问器方法
set_AProperty 方法 属性的set访问器方法
set_Item 方法 索引器的set访问器方法

  Test类型还有另一些节点未在表1-4中列出,其中包括.class,.custom,AnEvent,AProperty以及Item--它们标识了与类型有关的其他元数据。这些节点不映射到字段或方法;它们只是提供了有关类型的一些额外的信息,供CLR、编程语言或者工具访问。例如,利用某个工具,可以发现Test类型提供了一个名为AnEvent的事件,该事件通过两个方法(add_AnEvent和remove_AnEvent)公开。

 

  返回目录


  1、在.NET Framework SDK文档中,attribute和property均被翻译成属性。一些人将attribute翻译成特性,将property翻译成属性。但为了避免造成困扰,本书保留attribute原文,但将property翻译成属性。