C#的变迁史 - C# 4.0篇

C# 4.0 (.NET 4.0, VS2010)

  第四代C#借鉴了动态语言的特性,搞出了动态语言运行时,真的是全面向“高大上”靠齐啊。

1. DLR动态语言运行时

  C#作为静态语言,它需要编译以后运行,在编译的过程中,编译器要检查语法的正确性和类型的安全性,这是一个静态查找(编译时查找)的过程。确实,在运行之前发现问题总比在运行时发型问题要好的多,早发现早治疗嘛!但是这样做有时候会带来一些麻烦,比如类型在编译时无法获得时。

  看网上经典的一个例子:动态计算器。

  假设有一个计算器,它所在的程序集是动态加载进来的;当我们需要使用这个计算器计算数据时,通常是使用反射的方式:

object calc = GetCalculator();
Type calcType = calc.GetType();
object res = calcType.InvokeMember("Add",
BindingFlags.InvokeMethod, null,
new object[] { , });
int sum = Convert.ToInt32(res);

  不错,很好,可是有点麻烦。

  还有一种情况出现在Office程序中,例如给某单元格赋值:

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

  因为Cells返回的类型要想使用Value2属性,需要进行类型转换。

  在上面的这些例子中,因为C#是静态语言类型,就是强类型语言,所以要使用某个类的成员,就需要在编译的时候保证使用的是这个类的实例,或者是用反射。在这些场合下,写这样的代码无疑是不够优雅的。

  在C# 4.0中,这个情况会得到改善,因为这个版本的C#天生支持运行时类型查找,那就是CLR级别的DLR特性与语法级别的dynamic类型。

  在4.0中,程序可以直接写成:

// Calculator
dynamic calc = GetCalculator();
int sum = calc.Add(, ); // Office
excel.Cells[, ].Value2 = "Hello";

  使用dynamic定义的对象,CLR将不再进行静态查找,而是交给DLR在运行时进行动态的查找。这样的做法无疑是拓展了程序的扩展性和约束,例如此前要实现某些公共的行为,通常是需要先定义一个接口,拥有这个行为的对象实现这个接口,这样在程序中就可以针对这个接口进行编程。但是使用dynamic以后,这个接口就可以省掉了,直接使用成员就可以了。

  dynamic作为新的类型,可以用在任何类型允许出现的场合。当然也可以用在变量的传递中,Runtime会自动选择一个最匹配的方法。使用dynamic类型,就可以不去关心对象的实例是来源于COM, IronPython, HTML DOM或者反射,只要知道有什么方法可以调用就可以了,剩下的工作就交给DLR了。

  其实在某种程度上,可以认为dynamic类型是object类型的一个特殊版本,除了具有object所有的特征外,还指出了对象可以动态地使用。选择是否使用动态行为很简单,任何对象都可以隐式转换为dynamic,直到运行时才动态绑定。反之,从dynamic到任何其他类型都存在隐式转换。例如:

dynamic d = ;
int i = d;

  上面所谓的动态操作,不仅是指方法调用,字段和属性访问、索引器和运算符调用,甚至委托调用都可以动态地调用,例如:

dynamic d = GetDynamicObject(…);
d.M();
d.f = d.P;
d["one"] = d["two"];
int i = d + ;
string s = d(,);

  同时,任何动态操作的结果本身也是dynamic类型的,这个自然是很好理解。

  但是需要注意dynamic也不是万能的:

1). 目前动态查找不支持扩展方法的调用(可能在未来的版本的C#中会提供支持)。

2). 匿名方法和Lambda表达式不能转换为dynamic,也就是说dynamic d = x=>x;是不合法的,事实上lambda表达式也不能转成object。一样的道理,因为lambda表达式会在上下文环境下要么被编译器解释成委托类型,要么被解释成表达式树,但是如果上下文缺乏类型信息,编译器会无法解析。

  所以总的说来,还是那一条,编译器能认识的地方(能编译,能推断)就可以使用dynamic。

  dynamic的实现是基于IDynamicObject接口和DynamicObject抽象类。而动态方法、属性的调用都被转为了GetMember、Invoke等方法的调用。如果想在自己的代码中实现一个动态类型对象,可以继承DynamicObject类,并实现自己的若干get和set方法。看一个网上的例子:

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());

  最后来谈谈DLR(Dynamic Language Runtime),它是.Net 4.0中一组全新的API。对于C#,DLR提供了Microsoft.CSharp.RuntimeBinder命名空间,它为C#提供了强大的运行时互操作(COM,Ironpython等)能力,DLR具有优秀的缓存机制,对象一旦被成功绑定,CLR在下一次调用的时候就可以直接对确定类型的对象进行操作,而不必再通过DLR去查找了。

2. 命名参数(Named Parameter)与可选参数(Optional Parameter)

  这两个概念并没有什么联系,不过却经常纠缠在一起。

  先来看后面的这位仁兄,Optional Parameter与Required Parameter是相对的概念,老实说其它的语言中早就有了,只不过C#中到了4.0中才支持这个特性。看一个例子就会明白:

// 方法声明
public void M(int x, int y = , int z = ); // 方法使用方式
M(, , ); // 这个没什么可说的
M(, ); // 等价于(1, 2, 7)
M(); // 等价于 M(1, 5, 7)

  本质上,可选参数就是提供了函数参数的默认值,如果调用时不提供该参数的值,则取给定的默认值,就是这么简单。它的出现确实极大的减少了使用函数重载的情况,否则的话每种使用默认值的调用情况都得使用重载实现。

  有几点需要说明:

1).可选参数必须有个编译时常量作为其默认值。如果是除String之外的引用类型(包括那个特殊的dynamic类型),默认值只能是null。下面的声明是不能通过编译的

static void Foo(int a, String s = "i'm a string", dynamic b = , MyClass c = new MyClass())

2).可选参数必须从右往左出现在参数列表中(必须后出现),可选参数右边的参数(如果有的话)必须是可选参数。下面的声明是不能通过编译的

static void Foo(String s = "i'm a string", int a, dynamic b = null, MyClass c = null)

3).可选参数不仅适用于普通的方法,还适用于构造器,索引器中,本质上它们没有什么不同。

  说完可选参数,下面再谈谈命名参数。说的简单一点,命名参数就是在调用的时候指定了参数定义时的名称的参数,这样就能帮助有效编译器匹配实参和形参。

  对于Required Parameter来说,调用的时候是严格按顺序来的,自然不需要指定参数名称了,但是指定了因为没关系。

  对于Optional Parameter来说,调用时方法时,由于这些参数中某些参数使用了默认值,所以可能不出现在调用的实参列表中的,为了避免会歧义,这时就需要使用形参名称来避免误会。这是命名参数使用最多的场合。

  而且使用了命名参数后,编译器可以很轻松的配对实参和形参,所以参数的顺序就可以不按照定义时的顺序了。

  看一组简单的例子:

static void Main(string[] args)
{
M(, "A");
M(x: , s: "A");
M(s:"B", x:); M1(, s1:"Hi", s2:"Dong");
M1(, s2:"Dong", s1: "Hi");
M1(, s2: "Dong");
} static void M(int x, string s)
{
Console.WriteLine(x);
Console.WriteLine(s);
} static void M1(int x, string s1 = "Hello", string s2="DXY")
{
Console.WriteLine(x);
Console.WriteLine(s1 + " " + s2);
}

  从上面可以看出,命名参数在避免歧义方面使用起来还是很方便的。

3. 协变与逆变

  协变与逆变(Covariance and contravariance)指的是基类与子类实例之间满足条件的隐式转换;简单来讲,所谓协变(Covariance)是指把类型从“小”升到“大”,比如从子类升级到父类;逆变则是指从“大”变到“小”,比如从父类降级到子类。

  它们是面向对象语言的基本特征之一,与继承机制息息相关。继承机制与面向对象设计五大原则之一的里氏替换原则都要求所有使用基类的地方都可以使用子类,这包括传递参数的时候。

  此外,好的面向对象设计也要求对象满足“宽进严出”,概括的说就是传进对象的对象要求要宽松一点,流出对象的对象要求要严格一点。

  具体来说,这一原则体现在对象的初始化上,就是可以把子类的实例付给基类。这一原则体现在对象方法的实现上,就是方法的参数尽量使用能使用的基类(宽进,这样方法的灵活性就很好,所有基类的子类都可以传入该方法),方法的返回值尽量使用能使用的子类(严出,这样方法的返回值就容易明确方法的目的性,使用该方法的对象更容易处理返回值)。这一原则体现在代理delegate上,就是实例化代理类型的时候,使用的方法的参数可以是代理定义中参数类型的基类,使用的方法的返回值可以是代理定义中返回值类型的子类。比较绕吧,有时候我自己都会用错词,看看例子就会很清楚了:

delegate BaseResult MethodHandler(BaseParameter p);

class Program
{
static void Main(string[] args)
{
// 协变: DerivedParameter -> Parameter
Parameter p = new DerivedParameter(); // 完全匹配
MethodHandler m1 = M1;
// 逆变: 参数Parameter -> BaseParameter
MethodHandler m2 = M2;
// 协变: 返回值DerivedResult -> BaseResult
MethodHandler m3 = M3;
} static BaseResult M1(BaseParameter p) { return null; }
static BaseResult M2(Parameter p) { return null; }
static DerivedResult M3(BaseParameter p) { return null; }
} abstract class Parameter { }
class BaseParameter : Parameter { }
class DerivedParameter : BaseParameter { } abstract class Result { }
class BaseResult : Result { }
class DerivedResult : BaseResult { }

  虽然大部分情况下,我们直接初始化的类型都与定义的类型时完全匹配的,但是上面的例子中的初始化其实都是合法的,不仅合法,而且通常使用协变和逆变的方式其实更符合面向接口编程的方式。

  在4.0这个版本之前,泛型是不能满足协变和逆变的特性的,有兴趣的同学可以验证一下,虽然没什么实际意义。在4.0中,协变和逆变得到了改善,在泛型中的得到了进一步的支持;这是和out与in两个关键字密切相关的:out修饰的泛型参数只能作为函数的输出,in修饰的泛型参数只能作为函数的输入参数类型,使用了这两个关键字的泛型就满足协变和逆变的特性。看下面的例子:

delegate T ActionHandler<out T>();
class Program
{
static void Main(string[] args)
{
ActionHandler<string> a1 = M;
ActionHandler<object> a2 = a1; IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
} static string M() { return null; }
}

  例子中自定义泛型使用了out修饰泛型参数,因而例子中的用法是合法的。.NET Framework中的很多泛型都添加了这个修饰符,例如:

.Net4.0中使用out/in声明的Interface:
System.Collections.Generic.IEnumerable< out T>
System.Collections.Generic.IEnumerator< out T>
System.Linq.IQueryable< out T>
System.Collections.Generic.IComparer< in T>
System.Collections.Generic.IEqualityComparer< in T>
System.IComparable< in T> .Net4.0中使用out/in声明的Delegate:
System.Func< in T, …, out R>
System.Action< in T, …>
System.Predicate< in T>
System.Comparison< in T>
System.EventHandler< in T>

  其实,做这些本质上都是要在保证运行时类型安全的前提下提高代码的可重用性和灵活性。正是因为这个原因,IList<T>泛型没有添加out/in声明,所以下面的用法是不对的:

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

  究其根本原因,还是因为上面的使用无法保证运行时类型安全。例如下面的代码:

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

  这会允许将int插入strings列表中,然后将其作为string取出,这会破坏类型安全,所以IList这种允许修改元素的集合没有添加out/in声明。

  C#4.0中的协变和逆变使得泛型编程时的类型转换更加自然,不过要注意的是上面所说的协变和逆变都只作用于引用类型之间,例如,IEnumerable<int>不能作为IEnumerable<object>使用,因为从int到object的转换是装箱转换,而不是引用转换。而且在目前的泛型语法中,只能对泛型接口和委托使用协变和逆变。此外,一个泛型参数T只能是in或者是out,你如果即想你的委托参数逆变又想返回值协变,是做不到的。

  好了,4.0的主要特性就这些,不再啰嗦了。

上一篇:C#的变迁史 - C# 4.0 之线程安全集合篇


下一篇:C#的变迁史 - C# 1.0篇