C#4.0 新特性, dynamic, 可选参数,协变与抗变 (转)
C#4.0新特性之(一)动态查找
在大神Anders的领导下,C#这门语言也越来越快地朝着编程语言宇宙第一神器进化,C#4.0的新特征都是围绕“动态”(dynamic)的概念的,本文我们先来看看第一个新特性:动态查找(Dynamic Lookup)。
1.初识dynamic
动态查找允许动态(即在运行时)实现对某个对象的操作与对象类型的绑定,而不管这个对象是来自COM,IronPython,HTML DOM还是CLR的反射。你可以在程序中绕过编译器的类型检查,而把类型的匹配(lookup)丢给运行时去作。如果你需要对这样的对象进行操作,则会用到一个全新的类型:dynamic
dynamic是一个和之前所有CTS支持的类型都很不一样的类型,因为他不是object!确切的说,它会告知编译器“请暂时别把我当成任何object!”。看上去这和过去的反射很类似,但是dynamic可以让我们在代码里就可以直接实现对这个未知类型对象的操作,下面我们通过一个例子来说明dynamic带来的便利。我的电脑上安装了一种叫X雷的下载软件,它提供了一些COM组件可供调用,在过去,我需要这样来调用这个COM对象:
without Dynamic
这种通过Type.InvokeMemer在COM对象上调用方法实属别扭且无奈之举,因为编译器要先对方法的调用者进行类型绑定。不过现在有了dynamic类型,我们可以按照这样的方式对上述com对象进行操作:
with Dynamic
Type agentType;
if (url != null && url.Length > 0)
{
agentType = Type.GetTypeFromProgID("ThunderAgent.Agent");
dynamic dAgent = Activator.CreateInstance(agentType);
dAgent.AddTask5(url, "", "", "", url, -1, 0, -1, "","", "", 1, "", -1);
dAgent.CommitTasks2(1);
}
这样直接的调用方式要自然多了。不过你也许会问,既然这里的dAgent的类型未知,而其AddTask5方法在编译时也完全不知道其存在性,那么岂不是任何合法或者非法的调用都不会受到编译器的监管,而把一切可能的危险留给了运行时?的确,编译器只会检查发生在CTS支持的各种类型上的调用,而dynamic在编译时还没有被映射到任何一种CTS类型。
Tips 前面说的dynamic不是object句话当且仅当程序运行前是正确的,运行时dynamic会首先被声明成为一个object,下面是IL描述的分配本地参数的stack上的信息:
dynamic IL
.method private hidebysig static void DynamicCall([opt] string url) cil managed
{
.param [1] = "http://www.sunhao.cc/temp/lgxz.wma"
// Code size 420 (0x1a4)
.maxstack 17
.locals init ([0] class [mscorlib]System.Type agentType,
[1] object dAgent,
[2] bool CS$4$0000,
[3] class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo[] CS$0$0001)
//以下省略N行
而编译器对任何对发生在dynamic类型上的操作无能为力,他能做的唯一工作就是为运行时收集一些该dynamic对象的信息,比如它上面的方法签名。dynamic提供了访问com对象的方便,但是由于它在一定程度上破坏了C#强类型的特性,同时也要求程序员对自己写下的代码完全负责,增加了debug的成本。所以说dynamic有风险,使用需谨慎。
2.DLR与自定义动态类型
Dynamic Language Runtime是.Net 4.0中一组全新的API。对于C#,DLR提供了Microsoft.CSharp.RuntimeBinder命名空间[1],它为C#提供了强大的运行时互操作(COM,Ironpython等)能力,DLR也有优秀的缓存机制,对象一旦被成功绑定,CLR在下一次调用的时候就可以直接对确定类型的对象进行操作,而不必再通过DLR去lookup了。如果想在自己的代码中实现一个动态类型对象,可以继承DynamicObject[2]类,并实现自己的若干get和set方法。例如下面这个简单的例子:
MyClass
public class MyClass:DynamicObject
{
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
result = binder.Name;
return true;
}
}
上述代码在尝试invoke某个方法的时候直接返回该方法的名字。于是下面的代码将输出方法名:
代码
dynamic d = new MyClass();
Console.WriteLine(d.AnyMember());
3.dynamic的使用
由于dynamic本身也是一个类型(虽然只有编译器认识它,运行时不认识它),而且dynamic实现了implicit和explicit运算符,理论上任何可以使用CLR的类型的地方都可以用dynamic。以下的代码是合法的:
dynamic use case
dynamic d = (dynamic)2;
Action<dynamic> dAct = new Action<dynamic>((dynamic n) => { Console.Write(n.GetType()+": "+n); });
dAct(d);
但是dynamic也不是万能的:
1).目前动态查找不支持扩展方法的调用(可能在未来的版本的C#中会提供支持)。
2).匿名方法和Lambda表达式不能转换为dynamic,也就是说dynamic d = x=>x;是不合法的,事实上lambda表达式也不能转成object。一样的道理,因为lambda表达式会在上下文环境下要么被编译器解释成委托类型,要么被解释成表达式树,但是如果上下文缺乏类型信息,编译器会confuse掉。
4.总结
dynamic是C#4.0的核心特征,感觉上是C#这种强类型的语言多了一些动态语言的特征,是对C#和.Net的一个完善。如本文开头所说,作为一门编程语言,C#正在猛练北冥神功[3] ,这样下去可能C#要和ms word一样成为居家旅行杀人越货必备的武器了。
C#4.0新特性之(二)命名参数,可选参数与COM互操作
1.简介
之前C#(2.0)和java一样是一门的纯粹的面向对象的语言,他们都使用重载而不是可选参数。但是实际上使用的其他外部程序,COM组件却经常不要求指定所有参数(这在很多VC,VB编写的组件或者操作IronPython的对象的时候很常见,他们一直使用可选参数)。这会导致一个C#程序员不得不用Type.Missing塞满整个参数列表。不过C#4.0终于支持命名参数/可选参数了。程序员可以在方法调用的时候通过命名参数指定可选参数。而这一切都是为了让.Net 4.0的动态语言运行库(DLR)在动态绑定的时候具有更好的兼容性。
2.命名与可选参数
这个对C#来说是又一个新特征,但对C++,VB,Python etc. 的程序员来说这只是很自然的一个特征。C# 4.0种的可选参数和其他语言中的用法大致相同,这里不需要VB中额外的关键词修饰,也不能像C++中只用点点点来表示可以无视,倒是和python比较像,下面的声明是合法的:
可选参数Foo
static void Foo(int a, String s = "i'm a string", dynamic b =null, MyClass c = null)
简单来讲,C#4.0中使用可选参数必须遵循以下几条原则:
0).可选参数必须有个编译时常量作为其默认值。如果是除String之外的引用类型(包括那个特殊的dynamic类型),默认值只能是null。下面的声明是不能通过编译的:
代码
static void Foo(int a, String s = "i'm a string", dynamic b = 2, MyClass c = new MyClass())
1).可选参数必须从右往左出现在参数列表中(必须后出现),可选参数右边的参数(如果有的话)必须是可选参数。下面的声明是不能通过编译的:
代码
static void Foo(String s = "i'm a string", int a, dynamic b = null, MyClass c = null)
2).可选参数的赋值必须通过命名参数的方式指定,即必须使用可选参数的参数名称对其进行赋值。而非可选参数则不一定要用命名参数。如下的调用都是合法的:
Foo calls
Foo(2);
Foo(a: 2);
Foo(2, s: "hello", b: 3.14);
Foo(2, s: "hello", b: 3.14, c:new MyClass());
Foo(2, c: new MyClass());
特别的,一旦调用方法时使用的是命名参数,则命名参数的位置可以是任意顺序,如下的调用是合法的:
代码
Foo(b:3.14, c:new MyClass(), a:2);
唯一的影响就是上述调用中会对b先求值,然后再是c和a。函数总是按照参数出现的顺序进行求值操作的。
另外,可选参数不仅适用于普通的方法,还适用于构造器,索引器中,本质上它们没有什么不同。
3.可选参数与重载决策
毫无疑问,命名参数和可选参数让CLR在方法的重载决策(overload resolution)变得稍微复杂了一些,不用担心,这里你只需要搞清楚重载决策的下面几个特点就可以了:
0).在带可选参数的方法签名中,重载决策不会认可被可选参数代替的重载版本,比如下面两个声明:
public static void Foo(int a, String s = "i'm a string", dynamic b = null, MyClass c = null);
public static void Foo(int a, String s = "i'm a string");
如果按照以下方式调用,编译器会提示你它已经被上面两个方法confused了:
Foo(2);
Foo(a: 2);
1).在调用方式同样合法的情况下,重载决策会优先选择不带可选参数的重载版本。比如下面两个方法:
public static void Foo(int a, String s = "i'm a string", dynamic b = null, MyClass c = null);
public static void Foo(int a);
如果使用以下方式调用:被调用的会是void Foo(int a);这个版本:
Foo(2);
Foo(a: 2);
2).在调用方式同样合法的情况下,重载决策会优先选择类型最为匹配(最易转化)的重载,例如下面两个方法:
public static void Foo(byte a, String s = "i'm a string", dynamic b = null, MyClass c = null);
public static void Foo(object a);
Foo(2)和Foo(a:2)都将调用前一个方法,因为int到byte是值类型之间的转化,其代价要比从int转到object的代价低。
4.总结
C#4.0中很大一部分特征弥补了它之前的一些令开发者不爽的地方,无论是动态类型还是可选参数,新的C#让那些和各种组件(COM,IronPython etc.)打交道的程序员获得一定程度的解脱。C#越来越变得以人为本,更确切地,以程序员为本。
C#4.0新特性之(三)协变与逆变
1.C#3.0以前的协变与逆变
如果你是第一次听说这个两个词,别担心,他们其实很常见。C#4.0中的协变与逆变[1](Covariance and contravariance)有了进一步的完善,主要是两种运行时的(隐式)泛型类型参数转换。简单来讲,所谓协变(Covariance)是指把类型从“小”升到“大”,比如从子类升级到父类;逆变则是指从“大”变到“小”,两者各有不同的条件和用途。下面的例子演示了C#3.0以前对协变与逆变支持[2] :
代码1
public class Animal { }
public class Cat : Animal { }
public delegate Animal AniHandler(Animal a);
public static Animal AniMethod(Animal a) { return null; }
public static Cat CatMethod(Object o) { return null; }
public static void TestCovariance()
{
AniHandler handler1 = AniMethod;
AniHandler handler2 = CatMethod;//这里是合法的
}
这里的CatMethod虽然不是严格满足委托AniHandler的签名,但它被用作AniHandler是合法的,在协变(Cat->Animal)和逆变(object->Animal)的作用下,委托指向的方法中,传入的参数可以是一个大的,宽泛的类型,而返回出来的结果可以是一个更小的,精确的类型(子类),因为它包含了更多的信息。注意这里是站在方法里面这样说的,而在调用者使用方法的角度,恰恰是相反的,在调用方法时,参数可以是一个“小”的子类,而返回值可以用作一个“大”的父类,如下面的调用是合法的:
object o = AniMethod(new Cat());
呵呵,听上去有点晕,现在我要试着把问题简洁地表达清楚。无论是协变还是逆变,它都是为了让这样一个非常合理的事实成立:如果提供的类型信息比所需要的类型信息多(而不是相等),那这当然是可以的。在代码1的例子中,AniHandler委托需要一个Animal作为返回值,但是我返给它一个Cat,Cat包含了Animal的所有特征,这当然是可以的,这就是协变;同时AniHandler需要一个Animal作为参数,为了让函数获得的信息比要求的多,我可以只要求传进来一个object,这也当然是可以的,这就是逆变。
2.C#4.0中的协变
我们先来看一下和谐的协变是如何发生的。C#4.0中的协变与C#3.0中的宽松委托非常类似,新的C#协变特征还体现在泛型接口或者泛型委托的类型参数上。还是以经典的Animal和Cat为例,在你看过上面代码1之后,既然Cat CatMethod()可以被用作Animal AniHandler,那么你完全有理由相信下面的代码在C#3.0中也是合法的:
代码3
delegate T THandler<T>();
static void Main(string[] args)
{
THandler<Cat> catHandler= () => new Cat();
THandler<Animal> aniHandler = catHandler;//Covariance
}
很遗憾,您错了,在C#3.0中,上面的代码不能通过编译,你会被告知这样的错误:
时代进步了,现在在C#4.0的编译器是支持上面的写法的。你只需要在声明THandler的类型参数前加一个out关键字即可:
delegate T THandler<out T>();
单独的使用一个关键字而不是直接允许隐式转换也是为了类型安全的考虑。所以当你写下out的时候,就应该知道可能发生的Covariance。
3.C#4中的逆变
我们继续使用Animal和Cat的例子,在VS2008中,以下的代码不能通过编译:
代码5
delegate void THandler<T>(T t);
public static void TestContravariance()
{
THandler<Animal> aniHandler = (ani) => { };
THandler<Cat> catHandler = aniHandler;
}
而在VS2010中,呃,同样不能。呵呵,其实就差一点点,这里如果在类型参数T前面加上关键字“in”,即delegate void THandler<in T>(T t);就可以实现Cat->Animal的Contravariance。
4.总结
C#4中的协变和逆变使得泛型编程时的类型转换更加自然,不过要注意的是上面所说的协变和逆变都只作用于引用类型之间,而且目前只能对泛型接口和委托使用。一个T参数只能是in或者是out,你如果即想你的委托参数逆变又想返回值协变(如代码1所示),是做不到的