http://msdn.microsoft.com/zh-cn/magazine/ff796223.aspx
C# 编程语言自 2002 年初次发布以来已经有了极大的改善,可以帮助程序员编写更清晰易懂、更容易维护的代码。这种改善来自于不断加入的新功能,例如泛型类型、可为空的值类型、lambda 表达式、迭代器方法、分部类以及其他大量有用的语言结构。而且,这些改变还经常伴随着为 Microsoft .NET Framework 库提供相应的支持。
C# 4.0 延续了这种不断提高易用性的趋势。这款产品大大简化了许多常见任务,包括泛型类型、传统的互操作以及处理动态对象模型。本文旨在为深入调查探讨这些新功能。我将先介绍泛型方差,然后探讨传统互操作特性和动态互操作特性。
协变与逆变
协变与逆变最好通过示例来介绍,而最好的示例就在框架中。在 System.Collections.Generic 中,IEnumerable<T> 和 IEnumerator<T> 分别表示一个包含 T 的序列的对象和一个用来遍历该序列的枚举器(或迭代器)。这些接口长期以来承担了大量繁重的任务,因为它们允许实现 foreach 循环构造。在 C# 3.0 中,它们变得更加突出,因为它们在 LINQ 和 LINQ 到对象中起着重要作用:它们是表示序列的 .NET 接口。
因此,假如您有一个类层次结构,其中包含一个 Employee 类型以及从这个 Employee 类型派生而来的 Manager 类型(毕竟经理也是员工),那么您认为以下代码会产生什么效果?
IEnumerable<Manager> ms = GetManagers(); IEnumerable<Employee> es = ms;
看起来好像应该能够将 Manager 序列当作 Employee 序列。但是在 C# 3.0 中,赋值操作将失败;编译器将提示您,没有相应的转换功能。毕竟,该版本根本不能理解 IEnumerable<T> 的语义。这可以是任何接口,因此对于任意接口 IFoo<T>,为什么就能说 IFoo<Manager> 基本上能替代 IFoo<Employee> 呢?
而在 C# 4.0 中,赋值操作是有效的,因为 IEnumerable<T> 以及其他几种接口均发生了变化,这种变化是由 C# 中新增的类型参数协变支持实现的。
IEnumerable<T> 比任意 IFoo<T> 更加特殊,因为尽管初看起来并不明显,但是使用类型参数 T 的成员(IEnumerable<T> 中的 GetEnumerator 和 IEnumerator<T> 中的 Current 属性)实际上仅在返回值的位置使用 T。因此,您只能从序列中获取 Manager,而绝不能向其中放入 Manager。
相比之下,让我们看看 List<T>。由于以下原因,用 List<Manager> 来替代 List<Employee> 将是一场灾难:
List<Manager> ms = GetManagers(); List<Employee> es = ms; // Suppose this were possible es.Add(new EmployeeWhoIsNotAManager()); // Uh oh
正如这段代码所示,如果您认为您是在查看 List<Employee>,那您就可以插入任何员工。但实际上正在操作的列表是 List<Manager>,因此插入非 Manager 的员工一定会失败。如果允许这种操作,就会失去类型安全性。List<T> 不能对 T 进行协变。
而 C# 4.0 中有一项新的语言功能,允许定义一些类型(例如新的 IEnumerable<T>),只要正在处理的类型参数之间存在一定的关系,就允许在这些类型参数之间进行转换。.NET Framework 开发人员编写 IEnumerable<T> 时就使用了这项特性,他们的代码类似于下面的代码(当然经过了简化):
public interface IEnumerable<out T> { /* ... */ }
请注意修饰类型参数 T 的定义的 out 关键字。当编译器遇到此关键字时,会将 T 标记为协变,并检查接口定义中使用的 T 是否均正常(即,它们是否仅在输出位置使用,这也就是 out 关键字的由来)。
为什么将这种特性称为协变呢?通过绘制箭头,最容易看出原因。为了更形象,还是让我们使用 Manager 和 Employee 类型。由于这两个类之间存在继承关系,从 Manager 到 Employee 之间存在隐式引用转换。
Manager → Employee
现在,由于 IEnumerable<out T> 中 T 的注释,从 IEnumerable<Manager> 到 IEnumerable<Employee> 之间也存在隐式引用转换。这就是 T 注释的目的:
IEnumerable<Manager> → IEnumerable<Employee>
这被称为协变,因为两个示例中的箭头都指向相同的方向。我们首先定义两个类型 Manager 和 Employee。然后利用这两个类型来定义新类型 IEnumerable<Manager> 和 IEnumerable<Employee>。新类型的转换方式与旧类型一样。
当这两种转换的方向相反时,即为逆变。您可能已经想到这种情况将发生在仅将类型参数 T 用作输入时,那您就对了。例如,System 命名空间包含一个名为 IComparable<T> 的接口,该接口有一个名为 CompareTo 的方法:
public interface IComparable<in T> { bool CompareTo(T other); }
如果您有 IComparable<Employee>,则应该能够将其视为 IComparable<Manager>,因为您能做的唯一操作就是将 Employee 添加到该接口中。由于经理本身就是员工,加入经理应该是可行的,实际上也确实加入成功了。本例使用 in 关键字来修饰 T,并且以下方案能够正确执行:
IComparable<Employee> ec = GetEmployeeComparer(); IComparable<Manager> mc = ec;
这称为逆变,因为这次的两个箭头方向相反:
Manager → Employee
IComparable<Manager> ← IComparable<Employee>
到现在为止,可以很容易地总结语言特性:在您定义类型参数时可以添加 in 或 out 关键字,从而为您提供额外的自由转换。不过,还是有一些限制。
首先,这种方式仅适用于泛型接口和委托。您不能以这种方式为类或结构声明泛型参数。造成这种限制的一个简单原因是:委托很像只拥有一个方法的接口,而由于字段的存在,无论如何都不能将类看作某种形式的接口。您可以将泛型类的任何字段当作既是输入又是输出,具体取决于您是对它执行写入还是读取操作。如果这些字段涉及类型参数,则这些参数既不能协变也不能逆变。
其次,如果某个接口或委托具有协变或逆变类型参数,则只有在该接口使用(而不是其定义)中,类型参数是引用类型时,才允许对该类型执行新的转换。例如,由于 int 是值类型,因此即使看起来应该能行,但是 IEnumerator<int> 事实上不能转换为 IEnumerator <object>:
IEnumerator <int> IEnumerator <object>
出现这种行为的原因是转换必须保留类型的表现形式。如果允许执行 int 到 object 的转换,就不可能调用结果的 Current 属性,因为值类型 int 与对象引用在堆栈中所具有的表现形式是不一样的。但所有引用类型在堆栈中都具有相同的表现形式,因此只有在类型参数为引用类型时,才能实现这些额外的转换。
大多数 C# 开发人员很可能会开心地使用这项新的语言功能,因为在使用 .NET Framework 提供的某些类型(IEnumerable<T>、IComparable<T>、Func<T>、Action<T> 等等)时,他们会获得更多框架类型的转换,而且编译器错误也会更少。事实上,只要设计的库包含泛型接口和委托,设计人员就可以根据需要自由使用新的 in 和 out 类型参数,使其用户能够更轻松地使用他们设计的库。
另外,此功能需要运行时的支持,但这项支持早就存在了。可是由于没有什么语言用到这项支持,它已经沉寂了好几个版本。同样,前几个版本的 C# 允许一些有限的逆变转换。特别是,它们允许您使用具有兼容返回类型的方法来生成委托。此外,数组类型始终都是协变的。这些既有的功能与 C# 4.0 中的新功能截然不同,后者实际上是允许您定义自己的类型,使类型中的部分类型参数支持协变和逆变。
动态调度
在 C# 4.0 中的互操作功能方面,我们将首先介绍有可能是最大的变化。
C# 现在支持动态后期绑定。该语言以前始终是强类型的,在 4.0 版中仍将是这样。Microsoft 认为这会使 C# 快捷易用,适合 .NET 程序员交给它的所有任务。但有时候,您需要与不是基于 .NET 的系统通信。
通常,至少有两种方法可以实现此目的。第一种方法是直接将外部模型作为代理导入到 .NET 中。COM Interop 就是一个这样的例子。自从最初发布 .NET Framework 以来,它就通过一种名为 TLBIMP 的工具实施了这种策略:该工具可以创建您能够直接在 C# 中使用的新 .NET 代理类型。
C# 3.0 中附带的 LINQ-to-SQL 包含一种名为 SQLMETAL 的工具,可以将现有的数据库导入到 C# 代理类中,以便与查询结合使用。您还可以找到一种工具,用于将 Windows Management Instrumentation (WMI) 类导入到 C# 中。许多技术都允许您编写 C#(通常带有特性),然后使用手写代码作为外部操作的基础来执行互操作,这样的技术包括:LINQ-to-SQL、Windows Communication Foundation (WCF) 和序列化。
第二种方法完全放弃了 C# 类型系统,而将字符串和数据嵌入到代码中。当您编写代码来调用 JScript 对象的方法或者在 ADO.NET 应用程序中嵌入 SQL 查询时,您实际上就采用了这种方法。甚至您使用反射将绑定推迟到运行时(在这种情况下是与 .NET 本身进行互操作),实际上也是采用了这种方法。
C# 中的 dynamic 关键字的目的就是处理这些方法所面对的麻烦事。让我们来举一个简单的示例:反射。通常,使用反射时需要大量样板基础结构代码,例如:
object o = GetObject(); Type t = o.GetType(); object result = t.InvokeMember("MyMethod", BindingFlags.InvokeMethod, null, o, new object[] { }); int i = Convert.ToInt32(result);
有了 dynamic 关键字,就不需要按照这种方式使用反射来调用某个对象的 MyMethod 方法,而是可以告诉编译器:请将 o 视为动态的,将所有分析都推迟到运行时。其实现代码如下所示:
dynamic o = GetObject(); int i = o.MyMethod();
此代码能够正常使用,并且它用极其简洁的代码就完成了同样的工作。
如果您看看用来支持 JScript 对象操作的 ScriptObject 类,可能会更清楚地认识到这种简洁的 C# 语法的价值。 该类有一个 InvokeMember 方法拥有较多不同的参数;而在 Silverlight 中,该类却有一个 Invoke 方法(请注意名称上的差别)拥有较少的参数。这两种方法与您在调用 IronPython 或 IronRuby 对象的方法时所需的方法都不相同,与您在调用可能需要交互的任意数量的非 C# 对象的方法时所需的方法也不相同。
除了来自动态语言的对象以外,您还会发现许多本身就是动态的且由不同 API 提供支持的数据模型,例如 HTML DOM、System.Xml DOM 和用于 XML 的 XLinq 模型。COM 对象通常都是动态的,因此如果能将某些编译器分析推迟到运行时,就能获益匪浅。
本质上,C# 4.0 为动态操作提供了一种简单而又统一的视角。为了充分利用这一点,您要执行的所有操作就是指定给定的值是动态的,从而确保对该值执行的所有操作都推迟到运行时进行分析。
在 C# 4.0 中,dynamic 是一种内置类型,用一个伪关键字标出。但是请注意,这种 dynamic 与 var 不同。用 var 声明的变量实际上具有强类型,不过程序员将其留给编译器判断罢了。当程序员使用 dynamic 时,编译器不知道要使用的类型,程序员将这种判断留给运行时决定。
动态和 DLR
在运行时支持这些动态操作的基础结构称为动态语言运行时 (DLR)。这个新的 .NET Framework 4 库与其他任何托管库一样运行于 CLR 之上。它负责在启动动态操作的语言与实际发生动态操作的对象之间协调每个动态操作。如果动态操作不是由实际发生动态操作的对象处理的,C# 编译器的运行时组件将会处理绑定。简单但不完整的体系结构图如图 1 所示。
图 1 DLR 运行于 CLR 之上
关于动态操作(例如动态方法调用)有一件有趣的事,即接收对象在运行时有机会将其本身注入到绑定中,从而能够完全决定任何给定动态操作的语义。例如,让我们看一看以下代码:
dynamic d = new MyDynamicObject(); d.Bar("Baz", 3, d);
如果 MyDynamicObject 的定义如下所示,您可以想象一下会发生什么:
class MyDynamicObject : DynamicObject { public override bool TryInvokeMember( InvokeMemberBinder binder, object[] args, out object result) { Console.WriteLine("Method: {0}", binder.Name); foreach (var arg in args) { Console.WriteLine("Argument: {0}", arg); } result = args[0]; return true; } }
事实上,该代码会输出:
Method: Bar Argument: Baz Argument: 3 Argument: MyDynamicObject
通过将 d 声明为类型 dynamic,使用 MyDynamicObject 实例的代码就能有效地取消对 d 参与的操作进行的编译时检查。使用 dynamic,即表示“我不知道此变量会是什么类型的,因此我不知道它现在有哪些方法或属性。编译器,请让它们通过编译,留待运行时真正存在对象时再做判断。”因此即使编译器不知道 Bar 调用是什么意思,也能正确编译该调用。随后,在运行时,将由该对象本身来决定 Bar 调用将执行什么操作。这就是 TryInvokeMember 所知道的处理方式。
现在,假设您使用一个 Python 对象来代替 MyDynamicObject:
dynamic d = GetPythonObject(); d.bar("Baz", 3, d);
如果该对象是下面列出的文件,则该代码也能正确执行,并且输出几乎完全一样:
def bar(*args): print "Method:", bar.__name__ for x in args: print "Argument:", x
实际上,每次使用 dynamic 值时,编译器都会生成一些代码,用来初始化和使用 DLR CallSite。该 CallSite 包含在运行时进行绑定所需的全部信息,包括方法名称、额外数据(例如是否在已检验上下文中执行操作)以及有关参数及其类型的信息。
如果您必须维护此代码,则此代码完全像上面所示的反射代码、ScriptObject 代码或包含 XML 查询的代码一样丑陋。这是 C# 中动态功能的要求,但您没必要像这样编写代码!
在使用 dynamic 关键字时,您的代码完全可以像您所期望的那样:像简单的方法调用、索引器调用、运算符(例如 +)、类型转换甚至复合运算符(例如 += 或 ++)。您甚至可以在语句中使用 dynamic 值,例如 if(d) 和 foreach(var x in d)。还可以通过 d && ShortCircuited 或 d ??ShortCircuited 等代码支持短路。
由 DLR 为这类操作提供通用基础结构的价值在于您不再需要为代码中使用的每种动态模型处理不同的 API,只需要一个 API 就够了。而且,您甚至不需要使用它。C# 编译器会为您使用它,这使您能够有更多时间来编写真正需要的代码。您必须维护的基础结构代码越少,意味着您的效率越高。
C# 语言没有为定义动态对象提供快捷方式。C# 中的动态功能仅仅涉及到使用 动态对象。请看以下代码:
dynamic list = GetDynamicList(); dynamic index1 = GetIndex1(); dynamic index2 = GetIndex2(); string s = list[++index1, index2 + 10].Foo();
此代码能够进行编译,并且包含大量动态操作。首先是 index1 存在动态的预先递增,其次是对 index2 的动态加操作。接着对列表调用了一个动态索引器获取操作。这些操作所产生的结果又调用了成员 Foo。最后,该表达式的总结果被转换为 string 并存储到 s 中。一行代码中包含五个动态操作,每一个都是在运行时调度的。
每个动态操作的编译时类型即为 dynamic 本身,因此“动态”很像是在计算之间流动。即使您没有多次包含动态表达式,仍然可能会有大量动态操作。这一行代码中仍然有五个动态操作:
string s = nonDynamicList[++index1, index2 + 10].Foo();
由于两个索引表达式是动态的,因此索引本身也是动态的。而由于索引的结果是动态的,因此 Foo 调用也是动态的。然后,您面临着将 dynamic 值转换为 string。这当然也是动态发生的,因为该对象可以是要在面对转换请求时执行某种特殊计算的动态对象。
请注意,在上一个示例中,C# 允许从任何动态表达式向任何类型的隐式转换。最后转换为 string 就是一个隐式转换,不需要显式的类型转换操作。同样,任何类型也都可以隐式转换为 dynamic 类型。
在这种情况下,dynamic 更像一个对象,而且这两者之间的相似性不仅局限在这些方面。当编译器发出您的程序集并需要发出 dynamic 变量时,它是通过使用类型对象,然后专门对该对象进行标记,来完成这种操作的。从某种意义上讲,dynamic 是一种对象别名,不过是增加了在您使用它时动态解析操作的额外行为而已。
如果您尝试在两种泛型类型之间进行转换,且这两种类型的唯一差别是 dynamic 和 object,您就会看到这种效果;这样的转换总是能够正确进行的,因为在运行时,List<dynamic> 的实例实际上就是 List<object> 的实例:
List<dynamic> ld = new List<object>();
如果您尝试重写一个在声明时带有 object 参数的方法,也可以了解 dynamic 和 object 之间的相似性:
class C { public override bool Equals(dynamic obj) { /* ... */ } }
尽管它在您的程序集中会解析为修饰的对象,但我确实喜欢将 dynamic 视为真正的类型,因为它可以提醒您:您使用其他任何类型能够执行的大部分操作,也可以使用 dynamic 来执行。您可以将其用作类型参数或返回值。例如,以下函数定义将让您动态使用函数调用的结果,而不需要将其返回值保存到 dynamic 变量中:
public dynamic GetDynamicThing() { /* ... */ }
对于处理和调度 dynamic 的方式,还有许多细节信息,但是您即使不了解这些信息,也能使用该功能。关键的一点是您可以编写类似于 C# 的代码,如果您编写的代码的任何部分是动态的,编译器就会将其留到运行时处理。
对于动态功能,我要介绍的最后一个主题是:故障。由于编译器无法检查您所使用的动态内容是否真的具有名为 Foo 的方法,因此编译器无法报告错误。当然,这并不意味着 Foo 调用在运行时就能正确执行。它有可能正确执行,但也有很多对象并不具有名为 Foo 的方法。当您的表达式在运行时绑定失败时,绑定器将尽量为您提供一个异常。在一定程度上,这个异常与您未使用 dynamic 作为开头时,编译器提供的异常相似。
请考虑以下代码:
try { dynamic d = "this is a string"; d.Foo(); } catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException e) { Console.WriteLine(e.Message); }
在此,我有一个字符串,而字符串明显不会有名为 Foo 的方法。当调用 Foo 的那行代码执行时,绑定就会失败,而您将得到 RuntimeBinderException。下面是上一个程序的输出:
'string' does not contain a definition for 'Foo'
这正是 C# 程序员所希望看到的错误消息。
命名参数和可选参数
在 C# 的另一项新增功能中,方法现在支持具有默认值的可选参数,因此在您调用这样的方法时,您可以省略这些参数。在下面的 Car 类中,您可以看到其示例:
class Car { public void Accelerate( double speed, int? gear = null, bool inReverse = false) { /* ... */ } }
您可以按以下方式调用该方法:
Car myCar = new Car(); myCar.Accelerate(55);
这种方式与以下代码等效:
myCar.Accelerate(55, null, false);
由于编译器将插入您省略的所有默认值,因此这两段代码完全相同。
C# 4.0 还允许您在调用方法时通过名称来指定某些参数。通过这种方法,您可以直接向可选参数传递实参,而不用向该参数之前的所有参数都传递实参。
比如,您希望按相反的顺序调用 Accelerate,但您不希望指定 gear 参数。那么您可以按以下方式调用:
myCar.Accelerate(55, inReverse: true);
这是一种新的 C# 4.0 语法,跟以下代码是等效的:
myCar.Accelerate(55, null, true);
事实上,无论您调用的方法中的参数是否可选,您都可以在传递实参时使用名称。例如,这两种调用都是允许的,并且两者是等效的:
Console.WriteLine(format: "{0:f}", arg0: 6.02214179e23); Console.WriteLine(arg0: 6.02214179e23, format: "{0:f}");
如果调用的方法有大量参数,您甚至可以使用名称作为代码中的说明,帮助您记住每一个具体的参数。
表面上看起来,可选参数和命名参数与互操作功能一点都不像。您在使用这两种参数时甚至都不会想到互操作。但是,推出这些功能的动机都来自于 Office API。例如,想想 Word 编程以及 Document 接口上简单的 SaveAs 方法。此方法有 16 个参数,所有参数都是可选的。在前几个版本的 C# 中,如果您想调用此方法,就必须编写类似如下的代码:
Document d = new Document(); object filename = "Foo.docx"; object missing = Type.Missing; d.SaveAs(ref filename, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing);
而现在,您可以编写如下代码:
Document d = new Document(); d.SaveAs(FileName: "Foo.docx");
我认为,对于任何需要使用类似 API 的程序员来说,这都是一项进步。改善 Office 程序员的生活质量无疑是在语言中添加命名参数和可选参数的一项决定性因素。
现在,在编写 .NET 库并考虑添加具有可选参数的方法时,您将面临一个选择:添加可选参数,还是像 C# 程序员多年以来所做的那样引入重载。在 Car.Accelerate 示例中,后一种决定会使您得到类似如下的类型:
class Car { public void Accelerate(uint speed) { Accelerate(speed, null, false); } public void Accelerate(uint speed, int? gear) { Accelerate(speed, gear, false); } public void Accelerate(uint speed, int? gear, bool inReverse) { /* ... */ } }
为您编写的库选择合适的模型,完全由您自己决定。由于 C# 到目前为止还没有可选参数,.NET Framework(包括 .NET Framework 4)倾向于使用重载。如果您决定混合搭配重载和可选参数,C# 重载解析有一些明显的决定性规则,可在任何环境下决定调用哪一个重载。
带索引的属性
C# 4.0 中,有一些较小的语言功能仅在针对 COM 互操作 API 进行编程时受到支持。上一个示例中的 Word 互操作就是一个例子。
C# 代码始终都有一个索引器的概念,您可以将索引器添加到类中,以便在该类的实例中有效地重载 [] 运算符。这种索引器也称为默认索引器,因为它没有名称,调用时也不需要名称。有些 COM API 还具有非默认的索引器,这种索引器不能通过使用 [] 有效地进行调用,而必须指定名称。此外,您也可以将带索引的属性当作能够接受一些额外参数的属性。
C# 4.0 在 COM 互操作类型上支持带索引的属性。您不能在 C# 中定义具有带索引的属性的类型,但是只要 COM 类型具有带索引的属性,您就可以使用这种类型。例如,如果考虑 Excel 工作表上的 Range 属性,则执行此操作的 C# 代码将如下所示:
using Microsoft.Office.Interop.Excel; class Program { static void Main(string[] args) { Application excel = new Application(); excel.Visible = true; Worksheet ws = excel.Workbooks.Add().Worksheets["Sheet1"]; // Range is an indexed property ws.Range["A1", "C3"].Value = 123; System.Console.ReadLine(); excel.Quit(); } }
在此示例中,Range[“A1”, “C3”] 不是一个名为 Range 并返回带索引内容的属性。它是一个 Range 访问器调用,并且传递了 A1 和 C3。尽管 Value 看起来可能不像一个带索引的属性,它实际上还是一个带索引的属性!它的所有参数都是可选的,并且因为它是一个带索引的属性,所以您可以通过不指定参数来省略这些参数。在语言支持带索引的属性之前,您必须编写如下调用代码:
ws.get_Range("A1", "C3").Value2 = 123;
此处,加入 Value2 属性,只是因为带索引的属性 Value 在 C# 4.0 以前的版本中不能正确执行。
在 COM 调用点省略 Ref 关键字
有些 COM API 在编写时通过引用传递了许多参数,甚至在实现方案不需要写回这些参数时也是如此。在 Office 套件中,Word 就是一个明显的例子,其 COM API 都是这么做的。
当您面对这样一个库,并且需要通过引用来传递参数时,您就不能再传递任何非本地变量或字段的表达式,这是一个大难题。在 Word SaveAs 示例中,您可以看到这种情况:只是为了调用 SaveAs 方法,您必须声明一个局部调用的 filename 和一个局部调用的 missing,因为这两个参数需要通过引用来传递。
Document d = new Document(); object filename = "Foo.docx"; object missing = Type.Missing; d.SaveAs(ref filename, ref missing, // ...
在下面的新 C# 代码中,您可能会注意到我不再声明 filename 局部变量:
d.SaveAs(FileName: "Foo.docx");
这是因为 COM 互操作的新的省略 ref 功能。现在,在调用 COM 互操作方法时,您可以通过值,而不是通过引用来传递任何参数。如果您通过值来传递参数,编译器将代表您临时创建一个局部变量,然后根据需要为您通过引用传递该局部变量。当然,如果方法对参数进行了改变,您就看不出方法调用的效果。如果您希望这样,请通过引用来传递参数。
这应该会使像这样使用 API 的代码变得更清晰。
嵌入 COM 互操作类型
这更像是 C# 编译器功能,而不像是 C# 语言功能,但您现在可以使用 COM 互操作程序集,而不要求该程序集在运行时必须存在。目的是减轻将 COM 互操作程序集与您的应用程序一起部署的负担。
当 COM 互操作在最初版本的 .NET Framework 中引入时,就确立了主互操作程序集 (PIA) 的概念。引入此概念,是为了解决在组件之间共享 COM 对象的难题。如果您有一些不同的互操作程序集,分别定义了一个 Excel Worksheet,则我们无法在组件之间共享这些 Worksheet,因为它们具有不同的 .NET 类型。PIA 通过只存在一次而解决了这个难题:所有客户端都使用它,因此 .NET 类型始终是匹配的。
尽管 PIA 在理论上是个好主意,但在实际部署中却被证明是个大麻烦,因为它只有一份,而有多个应用程序可能会尝试安装或卸载它。而由于 PIA 通常很大,事情更复杂了。Office 在默认 Office 安装方式中并未部署它们,用户只需通过使用 TLBIMP 来创建自己的互操作程序集,即可轻松绕过这一个程序集系统。
因此,现在为了扭转这种局面,发生了两件事:
- 对于两个结构相同且共享相同识别特征(名称、GUID 等)的 COM 互操作类型,运行时能够聪明地将其看作同一个 .NET 类型。
- C# 编译器利用这一点的方式是在编译时直接在您自己的程序集中重现互操作类型,因此不再要求在运行时存在该互操作程序集。
由于篇幅所限,我不得不省略一些详细信息,但即使不了解这些信息,您也应该能够毫无障碍的使用这个功能,就像动态功能一样。您通过将引用上的“嵌入式互操作类型”属性设置为 true,告诉编译器为您将互操作类型嵌入到 Visual Studio 中。
由于 C# 团队希望这种方法成为引用 COM 程序集的首选方法,因此在默认情况下,Visual Studio 会将添加到 C# 项目中的任何新互操作引用的此属性设置为 True。如果您使用命令行编译器 (csc.exe) 来编译您的代码,请使用 /L 开关,而不是 /R 开关,来嵌入您必须引用的互操作程序集中的互操作类型。
本文中介绍的所有功能本身都可以产生大量讨论,每一个主题都值得撰文论述。我省略了许多详细信息,也有许多详细信息只是一带而过,但我希望本文能够成为探索 C# 4.0 的良好起点,并且您能腾出时间来研究和使用这些功能。如果您这么做,我希望您能够享受它们在效率以及程序可读性方面带来的好处,而这正是它们的目标所在。
Chris Burrows 是 Microsoft 的 C# 编译器团队中的开发人员。他在 C# 编译器中实现了动态功能,参与 Visual Studio 的开发也已经有九年了。
衷心感谢以下技术专家审阅本文:Eric Lippert