[转]C# 4.0新特性 - 动态编程

本文转自:http://www.cnblogs.com/yangqi/archive/2010/07/16/1778767.html

C# 4.0新特性

  Microsoft Visual C# 3.0作为Visual Studio 2008的一部分发布以来已经快一年了。在VS Managed Languages团队,我们一直努力创建该语言的下一个版本(没什么惊喜,就是C# 4.0),而这个文档是我们现在所看到的、计划中的语言特性的第一个公开描述。

  请记住本文内容都是针对产品早期的,而且将来会改变。这么早地共享我们的计划,一部分原因是为了获取某些反馈,这些反馈会让我们在产品最终发布出来之前改进它。

  与该白皮书一同发布的还有Visual Studio 2010的第一个公开的CTP(社区技术预览),它通过Virtual PC映像发布,任何人都可以试用。请使用这个CTP来把玩和试验这些特性,并让我们知道您的想法。我们希望知道您对这个早期产品的理解和不满,尤其是新添加的实现特性是否不具备最终产品应有的质量和稳定性。这个CTP的目的不是为您提供一个高生产力的工作环境,而是让您对我们正在为下一个发布版做些什么有个印象。

  该CTP包含了大量的演练,其中一些着重于C# 4.0的语言新特性。要详细了解这些特性的常用场景,这些演练是绝好的上手教程。您可以认为该白皮书是这些演练的伴随文档,全面地介绍了语言的特性和工作原理,而那些演练则是介绍具体的场景。

  C# 4.0

  C# 4.0的主要主题是动态编程。对象的意义变得越来越“动态”,它们的结构和行为无法通过静态类型来捕获,或者至少编译器在编译程序时无法得知对象的结构和行为。例如——
  a. 来自动态编程语言——如Python或Ruby——的对象   b. 通过IDispatch访问的COM对象   c. 通过反射访问的一般.NET类型   d. 结构发生过变化的对象——如HTML DOM对象

  尽管C#依然是静态类型语言,但我们的目的是改善它与这些对象的交互。

  另一个次要主题是与Visual Basic协同进步co-evolution)。将来我们希望仅维护每种语言单独的特征,而重要的新特性会同时引入两种语言。它们的区别仅仅是风格和感觉上的,而不在于特性集方面。

  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类型的对象后,你“对他做的事情”只会在运行时进行解析——
  dynamic d = GetDynamicObject(...);     d.M(7); 

  C#编译器允许你使用任何参数在d上调用一个方法,因为它的类型是dynamic。运行时会检查d的实际类型,并检测在它上面“用一个int调用M”是什么意思。

  可以认为dynamic类型是object类型的一个特殊版本,指出了对象可以动态地使用。选择是否使用动态行为很简单——任何对象都可以隐式转换为dynamic,“挂起信任”直到运行时。反之,从dynamic到任何其他类型都存在“赋值转换”,可以类似于赋值的结构中进行隐式转换——

  1.dynamic d = 7; // implicit conversion     2.int i = d; // assignment conversion 

  动态操作

  不仅是方法调用,字段和属性访问、索引器和运算符调用甚至委托调用都可以动态地分派——

  dynamic d = GetDynamicObject(…);  
  1. d.M(7); // calling methods  
  2. d.f = d.P; // getting and settings fields and properties  
  3. d[“one”] = d[“two”]; // getting and setting thorugh indexers  
  4. int i = d + 3; // calling operators  
  5. 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允许直接使用属性语法来访问对象的属性。

  简单对象

  除此之外,则d是一个标准的.NET对象,操作是通过在其类型上进行反射来分派的,C#的“运行时绑定器(runtime binder)”实现了运行时的C#查找和重载解析。其背后的本质是将C#编译器作为运行时组件运行,来“完成”被静态编译器延迟的动态操作。

  示例

  考虑下面的代码——

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. 使用反射获取两个对象d1d2的实际运行时类型,它们没有静态类型(包括静态类型dynamic)。结果为d1Foo类型而d2Bar
  • 2. 使用普通的C#语义在Foo类型上对M(string,Bar,3,null)调用进行方法查找和重载解析。
  • 3. 如果找到了该方法,则调用它;否则抛出运行时异常。

  带有动态参数的重载解析

  即便方法调用的接受者是静态类型的,重载解析依然发生在运行时。当一个或多个实参是dynamic类型时就会出现这种情况——

Foo foo =new Foo();   dynamic d =new Bar();     var result = foo.M(d); 

 

 

  C#运行时绑定器会基于d的运行时类型——也就是Bar——在FooM方法的静态可知(statically known)重载之间进行选择。其结果是dynamc类型。

  动态语言运行时

  动态语言运行时(Dynamic Language Runtime,DLR)是动态查找的底层实现的一个重要组件,也是.NET 4.0中新增的API。

  DLR不仅为C#动态查找,还为很多其他.NET上的动态语言——如IronPython和IronRuby——的实现提供了底层的基础设施。这一通用基础设施确保了高度的互操作性,更重要的是,DLR提供了卓越的缓存机制,使得运行时分派的效率得到巨大的改善。

  对于使用C#动态查找的用户来说,除了更高的性能之外,根本感觉不到DLR的存在。不过,如果你希望实现自己的动态分派对象,可以使用IDynamicObject接口来与DLR互操作,并向其中插入自己的行为。这是一个非常高级的任务,要求对DLR的内部工作原理有相当深入的了解。对于编写API的人,值得在这些问题上花些功夫,这样能够更广泛地改善可用性,例如为一个本身就是动态的领域编写类库。

  已知问题

  这里可能有一些限制或与你期望的结果不同。

  • DLR允许从一个表示类的对象创建对象。然而,C#的当前实现还不具备支持这一功能的语法。
  • 动态查找不能查找扩展方法。不论扩展方法是否依赖该调用的静态上下文(也就是出现了using语句),因为该上下文信息并不会作为有效载荷的一部分保留下来。
  • 匿名函数(也就是lambda表达式)不能作为实参传递给动态方法调用。在不知道要转换成什么类型的情况下,编译器不能绑定(也就是“理解”)一个匿名函数。

  这些限制导致的结果就是很难在动态对象上使用LINQ查询——

var result = collection.Select(e => e +5);  dynamic collection = ...;  

 

 

  如果Selected方法是个扩展方法,动态查找将找不到它。即便它是一个实例方法,上面的代码也无法编译,因为lambda表达式不能作为参数传递给动态操作。

  在C# 4.0中没有计划解决这些限制。

  命名参数和可选参数

  命名参数和可选参数是两个截然不同的功能,但通常一起使用。在进行成员调用时,可以忽略可选参数;而命名参数的方式可以通过名称来提供一个参数,而无需依赖它在参数列表中出现的位置。

  有些API——尤其是COM接口——如Office自动化API——确实本身就是通过命名参数和可选参数编写的。之前在C#中调用这些API非常痛苦,尤其有的时候需要多达30几个参数都必须显式传递,而其中大多数都具有合理的默认值,是可以忽略的。

  即便是编写.NET中的API,你也会发现很多时候你在被迫为不同的参数组合方式编写一个方法的大量重载形式,以便给调用者提供最高的可用性。在这种情况下,可选参数就会成为一种非常有用的替代方式。

  可选参数

  为一个参数提供默认值就可以将其声明为可选的——

publicvoid M(int x, int y =5, int z =7); 

 

 

  这里的yz就是可选参数,在调用时可以忽略——

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,可以写——

  1. M(1, z: 3); // passing z by name 

  1. M(x: 1, z: 3); // passing both x and z by name 

甚至

  1. M(z: 3, x: 1); // reversing the order of arguments 

  这几种形式都是等价的,不过参数总是按照其出现的顺序进行求值,因此对于最后一个示例来说,3会在1之前求值。

  可选参数和命名参数不仅可以用在方法调用中,还可以用在索引器和构造器中。

  重载解析

  命名参数和可选参数影响了重载解析,但产生的变化相当简单——

  如果所有的参数或者是可选的,或者在调用时(通过名字或位置)明确提供了对应的实参,并且实参能够转换为形参类型,则该签名是可适用的applicable)。

  转换的最优原则只用于明确给定的实参——出于最优的目的,忽略掉的可选参数在重载解析时将不做考虑。

  如果两个签名一样好,则没有忽略可选参数的那个胜出。

M(string s, int i =1);   M(object o);   M(int i, string s = “Hello”);   M(int i);     M(5); 

 

  对于给定的这些重载,我们可以看看上述规则的工作方式。M(string,int)不是可适用的,因为5不能转换为stringM(int,string)是可适用的,因为它的第二个参数是可选的,然后很明显,M(object)M(int)也是可适用的。

  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

  这意味着你可以直接在返回的对象上访问成员,或者可以使用强类型的局部变量为其赋值,而无需进行转换。例如,你可以写

excel.Cells[1, 1].Value ="Hello"

 

 

  而不用写

((Excel.Range)excel.Cells[1, 1]).Value2="Hello"

 

    又如
Excel.Range range = excel.Cells[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#编译器允许按值传递这些参数,并自动生成存放传入值的临时变量,并在调用返回后丢弃这些变量。使用这种方式,调用方看到的语义是按值传递,而且不会有任何副作用,而被调用的方法得到的依然是一个引用。

  已知问题

  一小部分COM接口特性没有出现在C#中。尤其是索引属性和默认属性。如果是动态访问COM,可以用之前提到的特性来解决,但静态类型的C#代码仍然无法识别它们。

  在C# 4.0中没有计划解决这些剩余的速度损失。

  变性

  泛型的某个方面会让人感到奇怪,比如下面的代码是不合法的——

IList<string> strings =new List<string>();   IList<object> objects = strings; 

  第二个赋值是不允许的,因为stringsobjects的元素类型并不一样。这样做有这充分的原因。如果允许那样写的话,你可能会写——

objects[0] =5;   string s = strings[0]; 

  这会允许将int插入strings列表中,然后将其作为string取出。这会破坏类型安全。

  然而,对于某些接口来说上述情况并不会发生,尤其是不能将对象插入集合时。例如IEnumerable<T>就是这样的接口。如果改为——

IEnumerable<object> objects = strings; 

 

 

  这样就没法通过objects将错误类型的东西插入到strings中了,因为objects没有插入元素的方法。变性(variance)就是用于在这种能保证安全的情况下进行赋值的。结果就是很多之前让我们感到奇怪的情况现在可以工作了。

  协变性

  在.NET 4.0中,IEnumerable<T>接口将会按照下面的方式进行定义——

publicinterface IEnumerable<out T> : IEnumerable   {           IEnumerator<T> GetEnumerator();   }     publicinterface IEnumerator<out T> : IEnumerator   {           bool MoveNext();           T Current { get; }   } 

 

 
  这些声明中的“out”指出T只能出现在接口的输出位置——如果不是这样的话,编译器会报错。有了这一限制,接口对于T类型就是“协变的”,这意味着如果A可以按引用转换为B,则IEnumerable<A>可以当作IEnumerable<B>使用。

  其结果是,任何一个字符串序列也就是一个对象序列了。

  这很有用,例如在LINQ方法中。使用上面的定义——

var result = strings.Union(objects); // succeeds with an IEnumerable<object> 

 

 

  之前这样做是不允许的,你必须做一些麻烦的包装,使得两个序列具有相同的元素类型。

  逆变性

  类型参数还可以具有“in”修饰符,限制它们只能出现在输入位置上。例如IComparer<T>

publicinterface IComparer<in T>   {           publicint Compare(T left, T right);   } 
 

  其结果有点让人迷惑,就是IComparer<object>可以作为IComparer<string>使用!这样考虑这个结果就会很有意义——如果一个比较器可以比较任意两个object,它当然也可以比较两个string。这种性质被称作“逆变性(contravariance)”。

  泛型类型可以同时拥有带有inout修饰符的类型参数,例如Func<...>委托类型——

publicdelegate TResult Func<in TArg, out TResult>(TArg arg); 

 

 

  很明显参数永远都是传的,而结果永远只能是传的。因此,Func<object, string>可以用作Func<string, object>

  限制

  变性类型参数只能在接口和委托类型中声明,这是CLR的限制。变性只能应用在类型参数的按引用转换之间。例如,IEnumerable<int>不能作为IEnumerable<object>使用,因为从intobject的转换是装箱转换,而不是引用转换。

  还要注意的是,CTP中并没有包含前面提到的.NET类型的新版本。为了试验变性,你需要自己声明变性接口和委托类型。

  COM示例

  这里有一个稍大一些的Office自动化示例,展示了大部分C#新特性的实际应用。

using System;    using System.Diagnostics;    using System.Linq;    using Excel = Microsoft.Office.Interop.Excel;    using Word = Microsoft.Office.Interop.Word;       class Program    {        staticvoid 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();        }    }  

  比起C# 3.0编写的等价代码,这段代码更加简洁可读。

  尤其注意如何动态地访问Value属性的。这实际上是一个索引属性,也就是说,这是一个带有参数的属性;C#并不能理解这种属性。然而其参数是可选的。由于访问是动态进行的,运行时COM绑定器会知道用默认值替换参数并调用索引属性。因此,动态COM可以避免访问Excel区域的令人迷惑的Value2属性。

  与Visual Basic的关系

  C# 4.0中引入的大量特性已经或者将要以另外的形式引入Visual Basic中——

  • VB中的迟绑定在很多方面都和C#中的动态查找很像,并且将来会更多地使用DLR,与C#更加等价。
  • 命名参数和可选参数在Visual Basic中已经存在很久了,这个特性的C#版本明显会使与VB的互操作能力最大化。
  • 无PIA和变性会同时引入VB和C#。

  VB也增加了大量曾经是C#所独有的特性。C#和VB未来的版本在功能上将更加等价,这对于每个人都是有益的
  资源

  有关C# 4.0的所有可用的资源都可以在C# Dev Center找到(www.csharp.net)。该白皮书和其他资源可以在Code Gallery站点找到(code.msdn.com/csharpfuture)。祝愉快!

posted on 2013-03-26 18:26  freeliver54  阅读(314)  评论(0编辑  收藏  举报

导航