C# 4.0的主要主题是动态编程。对象的意义变得越来越“动态”,它们的结构和行为无法通过静态类型来捕获,或者至少编译器在编译程序时无法得知对象的结构和行为。
a. 来自动态编程语言——如Python或Ruby——的对象
b. 通过IDispatch访问的COM对象 c. 通过反射访问的一般.NET类型 d. 结构发生过变化的对象——如HTML DOM对象
C# 4.0中的新特性分为四组——
动态查找
动态查找允许在编写方法、运算符和索引器调用、属性和字段访问甚至对象调用时,绕过C#静态类型检查,而在运行时进行解析。
命名参数和可选参数
现在C#中的参数可以通过在成员声明中为其提供默认值来指名它是可选的。在调用该成员时,可选参数可以忽略。另外,在传入任何参数时都可以按照参数名而不是位置进行传递。
特定于COM的互操作特性
动态查找以及命名参数和可选参数都有助于使针对COM的编程不再像今天这样痛苦。在这些特性之上,我们还添加了大量其他小特性,进一步改善了互操作体验。
变性
过去,IEnumerable<string>并不是IEnumerable<object>。现在它是了——C#包含了类型的“协变性和逆变性(co-and contravariance)”而且通用的BCL也将利用这一特性进行更新
动态查找
动态查找可以用统一的方式来动态调用成员。有了动态查找,当你拿到一个对象时,不用管它是来自于COM还是IronPython、HTML DOM或是反射;只需要对其进行操作即可,运行时会帮你指出针对特定的对象,这些操作的具体意义。
C#的一个设计目标就是允许在每个单独的调用中选择是否使用动态行为。
类型
C# 4.0引入了一个新的静态类型,称为dynamic。当你拥有了一个dynamic类型的对象后,你“对他做的事情”只会在运行时进行解析——
1 2 | dynamic d = GetDynamicObject(...); d.M(7); |
C#编译器允许你使用任何参数在d上调用一个方法,因为它的类型是dynamic。运行时会检查d的实际类型,并检测在它上面“用一个int调用M”是什么意思。
可以认为dynamic类型是object类型的一个特殊版本,指出了对象可以动态地使用。选择是否使用动态行为很简单——任何对象都可以隐式转换为dynamic,“挂起信任”直到运行时。反之,从dynamic到任何其他类型都存在“赋值转换”,可以类似于赋值的结构中进行隐式转换——
1 2 | 1. dynamic d = 7; // implicit conversion 2. int i = d; // assignment conversion |
动态操作
不仅是方法调用,字段和属性访问、索引器和运算符调用甚至委托调用都可以动态地分派——
1 2 3 4 5 6 7 | dynamic d = GetDynamicObject(…); d.M(7); // calling methods d.f = d.P; // getting and settings fields and properties d[“one”] = d[“two”]; // getting and setting thorugh indexers int i = d + 3; // calling operators string s = d(5,7); // invoking as a delegate |
C#编译器在这里的角色就是打包有关“在d上做什么”的必要信息,使得运行时可以获取这些信息并检测对于实际对象d这些操作的确切含义。可以认为这是将编译器的部分工作延迟到了运行时。
任何动态操作的结果本身也是dynamic类型的
运行时查找
在运行时,动态操作将根据目标对象d的本质进行分派——
COM对象
如果d是一个COM对象,则操作通过COM IDispatch进行动态分派。这允许调用没有主互操作程序集(Primary Interop Assembly,PIA)的COM类型,并依赖C#中没有对应概念的COM特性,如索引属性和默认属性。
动态对象
如果d实现了IDynamicObject接口,则请求d自身来执行该操作。因此通过实现IDynamicObject接口,类型可以完全重新定义动态操作的意义。这在动态语言——如IronPython和IronRuby——中大量使用,用于实现他们的动态对象模型。API也会使用这类对象,例如HTML DOM允许直接使用属性语法来访问对象的属性。
实例:
1 2 3 4 5 | dynamic d1 = new Foo(); dynamic d2 = new Bar(); string s; d1.M(s, d2, 3, null ); |
由于对M进行调用的接受者是dynamic类型的,C#编译器不会试图解析该调用的意义。而是将有关该调用的信息起来,供运行时使用。该信息(通常称作“有效载荷”)本质上等价于
——
“使用下面的参数执行一个称作M的实例方法——
- 1. 一个string
- 2. 一个dynamic
- 3. 一个int字面值3
- 4. 一个object字面值null”
在运行时,假设d1的实际类型Foo不是COM类型,也没有实现IDynamicObject。在这种情况下,C#运行时绑定器担负起了重载解析的工作,这是基于运行时类型信息完成的,按照下面的步骤进行处理——
- 1. 使用反射获取两个对象d1和d2的实际运行时类型,它们没有静态类型(包括静态类型dynamic)。结果为d1是Foo类型而d2是Bar。
- 2. 使用普通的C#语义在Foo类型上对M(string,Bar,3,null)调用进行方法查找和重载解析。
- 3. 如果找到了该方法,则调用它;否则抛出运行时异常。
带有动态参数的重载解析
即便方法调用的接受者是静态类型的,重载解析依然发生在运行时。当一个或多个实参是dynamic类型时就会出现这种情况
1 2 3 4 | Foo foo = new Foo(); dynamic d = new Bar(); var result = foo.M(d); |
C#运行时绑定器会基于d的运行时类型——也就是Bar——在Foo上M方法的静态可知(statically known)重载之间进行选择。其结果是dynamc类型。
动态语言运行时
动态语言运行时(Dynamic Language Runtime,DLR)是动态查找的底层实现的一个重要组件,也是.NET 4.0中新增的API。
DLR不仅为C#动态查找,还为很多其他.NET上的动态语言——如IronPython和IronRuby——的实现提供了底层的基础设施。这一通用基础设施确保了高度的互操作性,更重要的是,DLR提供了卓越的缓存机制,使得运行时分派的效率得到巨大的改善。
对于使用C#动态查找的用户来说,除了更高的性能之外,根本感觉不到DLR的存在。不过,如果你希望实现自己的动态分派对象,可以使用IDynamicObject接口来与DLR互操作,并向其中插入自己的行为。
已知问题
这里可能有一些限制或与你期望的结果不同。
- DLR允许从一个表示类的对象创建对象。然而,C#的当前实现还不具备支持这一功能的语法。
- 动态查找不能查找扩展方法。不论扩展方法是否依赖该调用的静态上下文(也就是出现了using语句),因为该上下文信息并不会作为有效载荷的一部分保留下来。
- 匿名函数(也就是lambda表达式)不能作为实参传递给动态方法调用。在不知道要转换成什么类型的情况下,编译器不能绑定(也就是“理解”)一个匿名函数。
这些限制导致的结果就是很难在动态对象上使用LINQ查询——
1 2 | var result = collection. Select (e => e + 5); dynamic collection = ...; |
如果Selected方法是个扩展方法,动态查找将找不到它。即便它是一个实例方法,上面的代码也无法编译,因为lambda表达式不能作为参数传递给动态操作。
命名参数和可选参数
命名参数和可选参数是两个截然不同的功能,但通常一起使用。在进行成员调用时,可以忽略可选参数;而命名参数的方式可以通过名称来提供一个参数,而无需依赖它在参数列表中出现的位置。
有些API——尤其是COM接口——如Office自动化API——确实本身就是通过命名参数和可选参数编写的。之前在C#中调用这些API非常痛苦,尤其有的时候需要多达30几个参数都必须显式传递,而其中大多数都具有合理的默认值,是可以忽略的。
即便是编写.NET中的API,你也会发现很多时候你在被迫为不同的参数组合方式编写一个方法的大量重载形式,以便给调用者提供最高的可用性。在这种情况下,可选参数就会成为一种非常有用的替代方式。
可选参数
为一个参数提供默认值就可以将其声明为可选的——
1 | public void M( int x, int y = 5, int z = 7); |
这里的y和z就是可选参数,在调用时可以忽略——
1 2 3 | M(1, 2, 3); // ordinary call of M M(1, 2); // omitting z – equivalent to M(1, 2, 7) M(1); // omitting both y and z – equivalent to M(1, 5, 7) |
命名的和可选的实参
C# 4.0不允许忽略逗号之间的实参,比如M(1,,3)。否则会导致大量不可读的、需要“数逗号”的代码。替代方式是任何参数都可以通过名字传递。因此如果在调用M时只希望忽略y,可以写——
- M(1, z: 3); // passing z by name
或
- M(x: 1, z: 3); // passing both x and z by name
甚至
- M(z: 3, x: 1); // reversing the order of arguments
这几种形式都是等价的,不过参数总是按照其出现的顺序进行求值,因此对于最后一个示例来说,3会在1之前求值。
可选参数和命名参数不仅可以用在方法调用中,还可以用在索引器和构造器中。
重载解析
命名参数和可选参数影响了重载解析,但产生的变化相当简单——
如果所有的参数或者是可选的,或者在调用时(通过名字或位置)明确提供了对应的实参,并且实参能够转换为形参类型,则该签名是可适用的(applicable)。
转换的最优原则只用于明确给定的实参——出于最优的目的,忽略掉的可选参数在重载解析时将不做考虑。
如果两个签名一样好,则没有忽略可选参数的那个胜出。
1 2 3 4 5 6 | M(string s, int i = 1); M(object o); M( int i, string s = “Hello”); M( int i); M(5); |
M(int,string)和M(int)都比M(object)要好,因为将5转换为int优于将5转换为object。
最后,M(int)优于M(int,string),因为它没有被忽略的可选参数。
因此,最终调用的方法是M(int)。
COM互操作特性
动态查找以及命名参数和可选参数极大地改善了与COM API——如Office Automation API——互操作的体验。为了减少更多的速度损失,C# 4.0还添加了大量特定于COM的小特性。
动态导入:
很多COM方法接受并返回可变类型,这在PIA中会表现为object。在绝大多数情况下,程序员在调用这些方法之前就已经从上下文中知道了一个返回值对象的静态类型,但为了使用这些知识,必须明确地在返回值上进行类型转换。这些转换非常普遍,带来了巨大的麻烦。
为了得到无缝体验,现在你可以选择使用dynamic类型来代替可变类型的方式。换句话说,从你的角度来看,COM签名中出现的是dynamic而不是object。
这意味着你可以直接在返回的对象上访问成员,或者可以使用强类型的局部变量为其赋值,而无需进行转换。例如,你可以写
1 | excel.Cells[1, 1].Value = "Hello" ; |
而不用写
1 | ((Excel.Range)excel.Cells[1, 1]).Value2= "Hello" ; |
又如
1 | Excel.Range range = excel.Cells[1, 1]; |
而不用写
1 | Excel.Range range = (Excel.Range)excel.Cells[1, 1]; |
无PIA的编译
主互操作程序集(Primary Interop Assembly)是从COM接口生成的大型.NET程序集,用于协助完成强类型的互操作。它们为设计时提供了巨大的支持,就好像其中的类型真的是 用.NET定义的一样。然而,在运行时这些大型程序集很容易使你的程序膨胀起来,而且很容易导致版本问题,因为它们是分布式的,不依赖你的应用程序。
无PIA特性允许你继续在设计时使用PIA,而无需在运行时使用它们。C#编译器会将程序中实际用到的PIA中的一小部分直接编译到程序集中。在运行时无需加载PIA。
省略ref
由于采用了不同的编程模型,很多COM API包含大量的引用参数。与C#中的ref相反,这些参数并不意味着要修改传入的实参以供调用方之后使用,而只是另外一种传递参数值的简单方式。
C#程序员必须为所有这些ref参 数创建临时变量,并按引用进行传递,这看上去一点也不合理。因此,对于COM方法,C#编译器允许按值传递这些参数,并自动生成存放传入值的临时变量,并 在调用返回后丢弃这些变量。使用这种方式,调用方看到的语义是按值传递,而且不会有任何副作用,而被调用的方法得到的依然是一个引用。
变性
泛型的某个方面会让人感到奇怪,比如下面的代码是不合法的——
1 2 | IList<string> strings = new List<string>(); IList<object> objects = strings; |
第二个赋值是不允许的,因为strings和objects的元素类型并不一样。这样做有这充分的原因。如果允许那样写的话,你可能会写——
1 2 | objects[0] = 5; string s = strings[0]; |
这会允许将int插入strings列表中,然后将其作为string取出。这会破坏类型安全。
然而,对于某些接口来说上述情况并不会发生,尤其是不能将对象插入集合时。例如IEnumerable<T>就是这样的接口。如果改为——
1 | IEnumerable<object> objects = strings; |
这样就没法通过objects将错误类型的东西插入到strings中了,因为objects没有插入元素的方法。变性(variance)就是用于在这种能保证安全的情况下进行赋值的。结果就是很多之前让我们感到奇怪的情况现在可以工作了。
协变性
在.NET 4.0中,IEnumerable<T>接口将会按照下面的方式进行定义——
1 2 3 4 5 6 7 8 9 10 | public interface IEnumerable< out T> : IEnumerable { IEnumerator<T> GetEnumerator(); } public interface IEnumerator< out T> : IEnumerator { bool MoveNext(); T Current { get; } } |
其结果是,任何一个字符串序列也就是一个对象序列了。
这很有用,例如在LINQ方法中。使用上面的定义——
1 | var result = strings. Union (objects); // succeeds with an IEnumerable<object> |
之前这样做是不允许的,你必须做一些麻烦的包装,使得两个序列具有相同的元素类型。
类型参数还可以具有“in”修饰符,限制它们只能出现在输入位置上。例如IComparer<T>—
1 2 3 4 | public interface IComparer< in T> { public int Compare(T left , T right ); } |
其结果有点让人迷惑,就是IComparer<object>可以作为IComparer<string>使用!这样考虑这个结果就会很有意义——如果一个比较器可以比较任意两个object,它当然也可以比较两个string。这种性质被称作“逆变性(contravariance)”。
泛型类型可以同时拥有带有in和out修饰符的类型参数,例如Func<...>委托类型——
1 | public delegate TResult Func< in TArg, out TResult>(TArg arg); |
很明显参数永远都是传入的,而结果永远只能是传出的。因此,Func<object, string>可以用作Func<string, object>。
限制
变性类型参数只能在接口和委托类型中声明,这是CLR的限制。变性只能应用在类型参数的按引用转换之间。例如,IEnumerable<int>不能作为IEnumerable<object>使用,因为从int到object的转换是装箱转换,而不是引用转换。
还要注意的是,CTP中并没有包含前面提到的.NET类型的新版本。为了试验变性,你需要自己声明变性接口和委托类型。
COM示例
这里有一个稍大一些的Office自动化示例,展示了大部分C#新特性的实际应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | using System; using System.Diagnostics; using System.Linq; using Excel = Microsoft.Office.Interop.Excel; using Word = Microsoft.Office.Interop.Word; class Program { static void Main(string[] args) { var excel = new Excel.Application(); excel.Visible = true ; excel.Workbooks. Add (); // optional arguments omitted excel.Cells[1, 1].Value = "Process Name" ; // no casts; Value dynamically excel.Cells[1, 2].Value = "Memory Usage" ; // accessed var processes = Process.GetProcesses() .OrderByDescending(p => p.WorkingSet) .Take(10); int i = 2; foreach (var p in processes) { excel.Cells[i, 1].Value = p.ProcessName; // no casts excel.Cells[i, 2].Value = p.WorkingSet; // no casts i++; } Excel.Range range = excel.Cells[1, 1]; // no casts Excel.Chart chart = excel.ActiveWorkbook.Charts. Add ( After : excel.ActiveSheet); // named and optional arguments chart.ChartWizard( Source: range.CurrentRegion, Title: "Memory Usage in " + Environment.MachineName); //named+optional chart.ChartStyle = 45; chart.CopyPicture(Excel.XlPictureAppearance.xlScreen, Excel.XlCopyPictureFormat.xlBitmap, Excel.XlPictureAppearance.xlScreen); var word = new Word.Application(); word.Visible = true ; word.Documents. Add (); // optional arguments word.Selection.Paste(); } } |