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# 3.0篇
C# 3.0 (.NET 3.5, VS2008) 第三代C#在语法元素基本完备的基础上提供了全新的开发工具和集合数据查询方式,极大的方便了开发. 1. WPF,WCF,WF 这3个工程类型奠定了新一 ...
- C#的变迁史 - C# 2.0篇
在此重申一下,本文仅代表个人观点,如有不妥之处,还请自己辨别. 第一代的值类型装箱与拆箱的效率极其低下,特别是在集合中的表现,所以第二代C#重点解决了装箱的问题,加入了泛型.1. 泛型 - 珍惜生命, ...
- C#的变迁史 - C# 1.0篇
C#与.NET平台诞生已有10数年了,在每次重大的版本升级中,微软都为这门年轻的语言添加了许多实用的特性,下面我们就来看看每个版本都有些什么.老实说,分清这些并没什么太大的实际意义,但是很多老资格的. ...
- C#的变迁史 - C# 4.0 之多线程篇
在.NET 4.0中,并行计算与多线程得到了一定程度的加强,这主要体现在并行对象Parallel,多线程Task,与PLinq.这里对这些相关的特性一起总结一下. 使用Thread方式的线程无疑是比较 ...
- C#的变迁史 - C# 4.0 之线程安全集合篇
作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题,解决临界资源的访问通常是加锁或者是使用信号量,这个大家应该很熟悉了. 而集合作为一种重要的临界资源,通用性更广,为了让大家更安全的使用它们, ...
- C#的变迁史 - C# 5.0 之调用信息增强篇
Caller Information CallerInformation是一个简单的新特性,包括三个新引入的Attribute,使用它们可以用来获取方法调用者的信息, 这三个Attribute在Sys ...
- C#的变迁史 - C# 5.0 之并行编程总结篇
C# 5.0 搭载于.NET 4.5和VS2012之上. 同步操作既简单又方便,我们平时都用它.但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能.这些情况下,我们通常是 ...
- C#的变迁史 - C# 4.0 之并行处理篇
前面看完了Task对象,这里再看一下另一个息息相关的对象Parallel. Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作,其内部使用Task来分装多线程的任务并试图 ...
- C#的变迁史 - C# 5.0 之其他增强篇
1. 内置zip压缩与解压 Zip是最为常用的文件压缩格式之一,也被几乎所有操作系统支持.在之前,使用程序去进行zip压缩和解压要靠第三方组件去支持,这一点在.NET4.5中已有所改观,Zip压缩和解 ...
随机推荐
- shell脚本常规技巧
邮件相关 发送邮件: #!/usr/bin/python import sys; import smtplib; from email.MIMEText import MIMEText mail_ho ...
- java线程与并发(一)
有好几个月没写博客了,各种破事儿忙完,决定继续写博客,恰好最近想了解下有关Java并发的一些知识,所以就准备这一段时间,用零碎的时间多记录一点有关并发的知识.希望这次能一直坚持下去. 想了解并发,必须 ...
- Macaca-iOS入门那些事2
Macaca-iOS入门那些事2 一. 前言 上文<Macaca-iOS入门那些事>讲到Macaca环境部署及运行了第一个案例,本文将讲解其案例编写. 二. 测试案例解析 iOS案例:ma ...
- Repeater绑定数组并显示其值
web开发中,尤其是对于数据展示,不得不说Repeater是一个万能的控件,而且使用也很方便. 在ASP.NET中将数组绑定到Repeater中请问如何在Repeater前台页面中显示该数组的值? s ...
- 知方可补不足~利用LogParser将IIS日志插入到数据库
回到目录 LogParser是微软开发的一个日志分析工具,它是命令行格式的,我们通过这个工具,可以对日志文件进行操作,对于一个几百兆的log文件,使用记事本打开是件很残酷的事,所以,很多情况下,我们都 ...
- 关于js中sort()排序方法
第一次写这个,算是记录自己的学习前端的一点点的历程吧.今天在做一个图片的随机排序遇到了一个问题,部分截图如下 我用的是json格式存储数组,想通过排序实现img数组中的内容升序或是降序发现用sort自 ...
- Objective-C中@property的所有属性详解
1,assign : 简单赋值,不更改索引计数 假设你用malloc分配了一块内存,并且把它的地址赋值给了指针a,后来你希望指针b也共享这块内存,于是你又把a赋值给(assign)了b.此时a 和b指 ...
- android api sdk
Platform Version API Level VERSION_CODE Notes Android 6.0 23 M API Changes Android 5.1 22 LOLLIPOP_M ...
- javascript基础语法——变量和标识符
× 目录 [1]定义 [2]命名规则 [3]声明[4]特性[5]作用域[6]声明提升[7]属性变量 前面的话 关于javascript,第一个比较重要的概念是变量,变量的工作机制是javascript ...
- CSS字体
字体系列 [1]5种通用字体系列:拥有相似外观的字体系列 serif字体:字体成比例,且有上下短线,包括Times\Georgia\New century Schoolbook sans-serif字 ...