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篇的更多相关文章

  1. C#的变迁史 - C# 3.0篇

    C# 3.0 (.NET 3.5, VS2008) 第三代C#在语法元素基本完备的基础上提供了全新的开发工具和集合数据查询方式,极大的方便了开发. 1. WPF,WCF,WF 这3个工程类型奠定了新一 ...

  2. C#的变迁史 - C# 2.0篇

    在此重申一下,本文仅代表个人观点,如有不妥之处,还请自己辨别. 第一代的值类型装箱与拆箱的效率极其低下,特别是在集合中的表现,所以第二代C#重点解决了装箱的问题,加入了泛型.1. 泛型 - 珍惜生命, ...

  3. C#的变迁史 - C# 1.0篇

    C#与.NET平台诞生已有10数年了,在每次重大的版本升级中,微软都为这门年轻的语言添加了许多实用的特性,下面我们就来看看每个版本都有些什么.老实说,分清这些并没什么太大的实际意义,但是很多老资格的. ...

  4. C#的变迁史 - C# 4.0 之多线程篇

    在.NET 4.0中,并行计算与多线程得到了一定程度的加强,这主要体现在并行对象Parallel,多线程Task,与PLinq.这里对这些相关的特性一起总结一下. 使用Thread方式的线程无疑是比较 ...

  5. C#的变迁史 - C# 4.0 之线程安全集合篇

    作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题,解决临界资源的访问通常是加锁或者是使用信号量,这个大家应该很熟悉了. 而集合作为一种重要的临界资源,通用性更广,为了让大家更安全的使用它们, ...

  6. C#的变迁史 - C# 5.0 之调用信息增强篇

    Caller Information CallerInformation是一个简单的新特性,包括三个新引入的Attribute,使用它们可以用来获取方法调用者的信息, 这三个Attribute在Sys ...

  7. C#的变迁史 - C# 5.0 之并行编程总结篇

    C# 5.0 搭载于.NET 4.5和VS2012之上. 同步操作既简单又方便,我们平时都用它.但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能.这些情况下,我们通常是 ...

  8. C#的变迁史 - C# 4.0 之并行处理篇

    前面看完了Task对象,这里再看一下另一个息息相关的对象Parallel. Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作,其内部使用Task来分装多线程的任务并试图 ...

  9. C#的变迁史 - C# 5.0 之其他增强篇

    1. 内置zip压缩与解压 Zip是最为常用的文件压缩格式之一,也被几乎所有操作系统支持.在之前,使用程序去进行zip压缩和解压要靠第三方组件去支持,这一点在.NET4.5中已有所改观,Zip压缩和解 ...

随机推荐

  1. 这里有个坑---[NotMapped]不要忘了加

    这里有个坑,我们定义实体的时候如果使用了在数据库没有的字段记得加[NotMapped]---------每一个遇到的坑总结后都是一比财富. 在项目中我们一般会使用EF等ORM框架来为我们服务,在ORM ...

  2. 10 个 Redis 建议/技巧

    转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/145.html?1455790611 Redis 在当前的技术社区里是非常 ...

  3. PDO连接mysql和pgsql数据库

    PDO连接mysql数据库 <?php $dsn="mysql:host=localhsot;dbname=lamp87"; $user="root"; ...

  4. 用css来写一些简单的图形

    在写网页的过程中,有时我们需要用到一些简单的图片但是手头又没有合适的,我们其实可以自己来写,下面我就简单的介绍几个例子: 1.上三角 Triangle Up #triangle-up { width: ...

  5. [Spring框架]Spring JDBCTmplate基础入门总结.

    前言:前面有讲过 Spring IOC以及AOP的基本使用方法, 这里就再来讲下Spring JDBCTemplate的使用方法. 一, 概述这里先说一下Spring 整合的一些模板: 从上图中可以看 ...

  6. Atitit 图像清晰度 模糊度 检测 识别 评价算法 源码实现attilax总结

    Atitit 图像清晰度 模糊度 检测 识别 评价算法 源码实现attilax总结 1.1. 原理,主要使用像素模糊后的差别会变小1 1.2. 具体流程1 1.3. 提升性能 可以使用采样法即可..1 ...

  7. Atitit js版本es5 es6新特性

    Atitit js版本es5 es6新特性 Es5( es5 其实就是adobe action script的标准化)1 es6新特性1 Es5( es5 其实就是adobe action scrip ...

  8. 学习ASP.NET MVC(八)——“Code First Migrations ”工具

    在本篇文章中,我们学习如何使用实体框架的“Code First Migrations ”(也称为代码先行功能)工具,使用其中的“迁移”功能对模型类进行一些修改,同时同步更新对应数据库的表结构. 默认情 ...

  9. require.js笔记

    笔记参考来源:阮一峰  http://www.ruanyifeng.com/blog/2012/10/javascript_module.html   1. 浏览器端的模块只能采用“异步加载”方式 = ...

  10. Design4:数据库设计规范

    当数据模型从概念层转到逻辑层时,需要进行规范化设计.要想设计一个结构合理的关系型数据库,至少需要满足1NF,2NF,3NF,即第一范式,第二范式,第三范式. 1,1NF(原子性) 1NF是最基本的,数 ...