C#与C++的发展历程第二 – C#4.0再接再厉
系列文章目录
开始本系列的第二篇,这篇文章中将介绍C#4.0中一些变化,如C++有类似功能也将一并介绍。个人感觉C#4.0中增加的语言方面的特性不是很多,可能是这个时期都在着力完成随之发布的新的4.0版的CLR。总体来说C#4.0中有4个方面的特性。下面依次介绍:
C#4.0 (.NET Framework 4.0, CLR 4.0)
C# 动态类型
在诸如Javascript这样的脚本语言中我们可以随时给对象添加成员 :
1
2
3
4
5
6
7
8
9
10
11
|
var student = {}; student.Name= '张三' ; student.StudyDay=110; student.PlayDay=50; student.CalcMark = function (talent) { var study = Math.pow(1.01,student.StudyDay)/4.9*100; var play=Math.pow(0.99,student.PlayDay)/4.9*100; var talentmark=student.PlayDay/160*100; return talent?study+talentmark:study+play; }; |
现在C#中通过动态类型的支持,我们也能编写类似的代码。如下:
1
2
3
4
5
6
7
8
9
10
|
dynamic student = new ExpandoObject(); student.Name = "李四" ; student.StudyDay = 120; student.PlayDay=40; student.CalcMark = (Func< bool , double >)(talent=>{ var study=Math.Pow(1.01,student.StudyDay)/4.9*100; var play=Math.Pow(0.99,student.PlayDay)/4.9*100; var talentMark=student.PlayDay/160*100; return talent?study+talentMark:study+play; }); |
调用动态添加和方法和调用普通方法一样。
1
|
var mark = student.CalcMark( false ); |
给动态类型添加事件也是可以的,如:
1
2
3
|
student.MakeMistakes = null ; student.MakeMistakes += new EventHandler((sender, e)=> Console.WriteLine( "{0}被请家长。" , ((dynamic)sender).Name)); |
触发事件看起来和调用函数差不多:
1
|
student.MakeMistakes(student, new EventArgs()); |
这种动态类型的一个很大作用就是在我们想传递一个对象但不想为此定义一个类时采用。如ASP.NET MVC 3起ControllerBase类新增的的ViewBag属性就是dynamic对象适用场景的典型例子。
1
|
public dynamic ViewBag { get ; } |
有了ViewBag可以灵活的在Controller和View之间传递数据,而不要事先定义Model。当然使用ViewBag的效率也不如强类型的Model类。
我们可以定义一个返回dynamic对象的方法:
1
2
3
4
5
6
|
public dynamic GetStudent() { dynamic student = new ExpandoObject(); //略去添加成员的代码.. return student; } |
调用这个方法我们可以得到一个动态对象,并访问其中定义的值:
1
|
dynamic student = GetStudent(); |
同样我们也可以定义一个接受dynamic对象的函数:
1
2
3
4
|
public void GetMark(dynamic student) { double mark = student.CalcMark( false ); } |
使用这种接收或返回动态类型的函数要非常小心,往往错误到了运行时才会出现。
这里dynamic声明的对象是一类实现了IDynamicMetaObjectProvider接口类型的对象(也必须如此),这个接口提供的功能可以概括为”延迟绑定”,即在运行时才确定类型。IDynamicMetaObjectProvider类型的对象执行在DLR上完成。
DLR是CLR4.0开始新增的专门处理动态类型的部分,全称Dynamic Language Runtime。通过DLR对动态类型的支持,可以在支持 DLR 互操作性模型的各种语言(如IronPython)之间共享 IDynamicMetaObjectProvider接口对象的实例。如我们用C#实例化一个DynamicObject实例,并传递给IronPython函数。
上文中介绍的ExpandoObject是一个IDynamicMetaObjectProvider接口的简单实现,其提供了基本的在运行时添加和删除实例的成员以及设置和获取这些成员的值的功能。
ExpandoObject 类还实现 IDictionary<String, Object> 接口,使用这个接口的方法可以在运行时判断某个动态成员是否存在。下面的例子来自MSDN:
示例代码中将 ExpandoObject 类的实例强制转换为 IDictionary<TKey, TValue> 接口,并枚举该实例的成员。
1
2
3
4
5
6
7
8
|
dynamic employee = new ExpandoObject(); employee.Name = "John Smith" ; employee.Age = 33; foreach ( var property in (IDictionary<String, Object>)employee) { Console.WriteLine(property.Key + ": " + property.Value); } |
通过将 ExpandoObject 实例强制转换到 IDictionary<String, Object> 接口,还可以通过IDictionary接口以删除键值对的方式来删除动态成员。 如下例子:
1
2
3
|
dynamic employee = new ExpandoObject(); employee.Name = "John Smith" ; ((IDictionary<String, Object>)employee).Remove( "Name" ); |
ExpandoObject类还实现了INotifyPropertyChanged 接口,在成员添加、删除或修改时会引发 PropertyChanged 事件。在一些使用MVVM模式的环境中,以ExpandoObject作为ViewModel对象能够方面的通知View层其内容更改以实现数据绑定。
使用动态对象的代码在第一次给动态对象赋值后,在运行时赋值代码执行完成后,动态类型也就确定了。后续使用就应该注意类型要匹配。如下面的代码在运行时就会由于类型不匹配而出现异常。
1
2
|
dynamic year= "2014" ; year++; |
重新给动态对象赋值可以改变其运行时的类型,下面的代码在运行时不会报错:
1
2
3
|
dynamic year= "2014" ; year = 2014; year++; |
如果需要更复杂的运行时动态行为,可以使用DynamicObject对象,但是需要注意不能直接实例化DynamicObject类型的对象,需要继承DynamicObject类型创建自己的对象,然后再实例化这个自定义DynamicObject类型的对象。DynamicObject类预定义一些高级的延迟绑定功能,通过实现它可以很轻松的创建自己的动态类型,从而自定义可以在动态对象上执行哪些操作以及如何执行这些操作。下面同样是一个来自MSDN的例子。
示例代码实现的类继承了DynamicObject类,并给出了新的实现,其将动态类的属性保存在一个内部字典中,并提供了查询动态设置的属性数量的Count属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// DynamicDictionary继承自DynamicObject public class DynamicDictionary : DynamicObject { //内部字典对象 Dictionary< string , object > dictionary = new Dictionary< string , object >(); //返回动态添加的属性的数量,即内部字典元素的个数 public int Count { get { return dictionary.Count; } } //当试图获取一个不是定义于类的属性的值时,将调用这个方法 public override bool TryGetMember(GetMemberBinder binder, out object result) { //将属性名变为小写,实现属性名大小写无关 string name = binder.Name.ToLower(); //如果在字典中找到访问的属性名,将属性值通过result返回,并返回true,反之返回false return dictionary.TryGetValue(name, out result); } //当试图设置一个不是定义于类的属性的值时,将调用这个方法 public override bool TrySetMember(SetMemberBinder binder, object value) { //将属性名变为小写,实现属性名大小写无关 dictionary[binder.Name.ToLower()] = value; //任何时候都可以添加新的属性,方法总是返回true return true ; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class Program { static void Main( string [] args) { //实例化一个自定义动态类对象 dynamic person = new DynamicDictionary(); //添加动态属性(TrySetMember方法被调用) person.FirstName = "Ellen" ; person.LastName = "Adams" ; //获取动态属性的值(TryGetMember方法被调用) //属性名大小写无关 Console.WriteLine(person.firstname + " " + person.lastname); //获取Count属性的值(没有调用TryGetMember方法,Count属性是定义于类中的) Console.WriteLine( "动态属性的数量为:" + person.Count); //下面的语句会在运行时抛出异常 //由于不存在"address"属性,TryGetMember方法返回false,从而导致RuntimeBinderException Console.WriteLine(person.address); } } |
更多关于实现DynamicObject类型的讨论,可以见园友JustRun的这篇文章。
其中实现了一个DynamicJson类,可以将json字符串转为一个可以像Javascript中一样使用的json动态对象。
如果DynamicObject也无法实现某些需要的功能,可以直接实现IDynamicMetaObjectProvider接口来控制动态类型对象的延迟绑定行为。
最后来看下动态类型的一些其它作用。通过动态类型的支持,我们可以把之前一些需要反射来实现的功能交由动态类型来实现。同样是之前使用的Student类作为例子:
先定义一个Student类:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Student { public string Name { get ; set ; } public int StudyDay { get ; set ; } public int PlayDay { get ; set ; } public double CalcMark( bool talent) { var study = Math.Pow(1.01, StudyDay) / 4.9 * 100; var play = Math.Pow(0.99, PlayDay) / 4.9 * 100; var talentMark = PlayDay / 160 * 100; return talent ? study + talentMark : study + play; } } |
假如这个Student类位于一个没有在当前程序编译时引用的程序集中,即你需要动态加载一个程序集并通过反射实例化Student类型,设置其属性并调用方法:
1
2
3
4
5
6
7
8
9
|
Assembly myAssembly = Assembly.LoadFrom( "StudentAssembly.dll" ); Type studentType = myAssembly.GetType( "Student" ); object student = Activator.CreateInstance(studentType); studentType.GetProperty( "Name" ).SetValue(student, "王五" , null ); studentType.GetProperty( "StudyDay" ).SetValue(student, 120, null ); studentType.GetProperty( "PlayDay" ).SetValue(student, 30, null ); object markobj = studentType.GetMethod( "CalcMark" ).Invoke(student, new object [] { false }); double mark = Convert.ToDouble(markobj); |
现在用了动态类型的支持,我们可以编写下面这样赏心悦目的代码:
1
2
3
4
5
6
|
dynamic student = Activator.CreateInstance(studentType); student.Name = "赵六" ; student.StudyDay = 120; student.PlayDay = 30; double mark = student.CalcMark( false ); |
.NET社区的开源项目Clay将.NET的动态特性发挥到极致,使用Clay可以完全摆脱C#的类型限制。关于Clay再次推荐JustRun的一篇文章:
C++方面没有类似特性,没的写。
C# 可选参数与命名参数
可选参数
可选参数这个特性说起来很简单, 来看一个带有可选参数方法的声明:
1
2
3
4
|
public void Log( string log, LogLevel level = LogLevel.Alarm ,LogTo destination = LogTo.Database) { //... } |
调用这个函数有三种方式:
1
2
3
|
Log( "hello world" ); Log( "hello world" , LogLevel.Info); Log( "hello world" , LogLevel.Info, LogTo.File); |
需要注意的是可选参数必须定义在所有非可选参数的后面。这个特性的加入使以前很多需要定义重载函数的场景可以用一个支持可选参数的函数代替。
如果有一个函数重载和带可选参数方法的一种调用方式相同,则会优先调用没有可选参数的重载。即如果有下面的函数重载:
1
2
3
4
|
public void Log( string log, LogLevel level) { //... } |
则对于上文中第二种调用方式会优先选择这个重载。
关于可选参数的实现内幕可以看Artech大神的这篇文章。
命名参数
命名参数功能可以让我们在传入实参时,在前面加上形参的名字,用以标识实参对应那个形参。
对于上面方法的调用可以这样:
1
|
Log( "hello world" , level: LogLevel.Info, destination:LogTo.File); |
使用命名参数时,可以改变实参传入的顺序:
1
|
Log( "hello world" , destination:LogTo.File, level: LogLevel.Info); |
这样的调用效果和之前是一样的。
和可选参数定义类似的是,命名参数必须放在调用参数列表的最后。
C++既没有可选参数也不支持命名参数,C++11对参数的改进就是在原来可变参数的基础上增加了可变参数模板。这其实是很强大的一个功能,让变长的参数也可以强类型化。这个特性留到后面的文章再说。
C# 改进的COM交互
C#中COM互操作的改进完全是得益于前两条新特性的加入。当然这块也是博主最不熟悉的领域,之前从没写过和COM交互的代码。所以还是从国外程序员的博客上借(piao)用(qie)一个例子(出处)吧。
在C#4之前调用COM的代码常常会出现很多ref参数等。
1
2
3
4
5
6
|
static void Main() { Word.Application app = new Word.Application(); object m = Type.Missing; app.Documents.Add( ref m, ref m, ref m, ref m); } |
C#4.0出现使这些都方便了很多:
1
2
|
Word.Application app = new Word.Application(); app.Documents.Add(); |
就是这样简单、任性!
另外结合动态类型,一些返回值的获取、参数传递以及赋值也大大简化了。
关于COM交互的支持在将来可能就真没什么用了,现在COM已经完美转型为WinRT,WinRT平台上微软给各种语言的交互提供了很完美的支持。据说是性能不太好,但代码颜值很高
这条目就更没C++什么事了。
C# 逆变与协变
C#4.0和.NET Framework 4.0开始支持的一个很重要的特性就是逆变与协变。为此C#引入了2个新的关键字in和out,分别表示逆变和协变。那什么是逆变和协变呢。还是以例子开始说明,现在有两个拥有继承关系的类型:
为什么关键字是in和out呢,可以这样理解,in表示泛型的参数被传入一个更小的作用域内,而这个范围内的方法知道如何去处理一个更具体的类型,这样操作是安全的。out表示泛型的参数会被返回到一个更大的作用域,由于本身就可以用父类去调用一个子类中从父类继承的属性或方法所以这种操作也是安全的。
1
2
3
4
5
6
7
8
9
10
11
|
public class Panda : Animal { public string NickName { get ; set ; } } public class Animal { public string Name { get ; set ; } public int Age { get ; set ; } } |
在这之前的.NET版本中,下面的代码是无法被支持的,因为Animal和Panda是不同的类型:
1
2
|
var pandas = new List<Panda>(); IEnumerable<Animal> animals = pandas; |
在C#4.0和.NET Framework 4.0之后这样代码可以顺利被执行,原因就是协变的支持,来看一下.NET 4.0中IEnumerable接口的定义:
1
|
public interface IEnumerable< out T> : IEnumerable |
可以看到在泛型类型之前多了表示协变的out关键字,我们来给协变一个具体的定义,通过定义大家就可以了解为啥上面的赋值可以成立了。
协变:协变允许与泛型类型参数所定义的类型相比,派生程度更大的类型。相反,逆变就是允许与泛型形参所指定的实参类型相比,派生程度更小的实参类型。
.NET中支持逆变与协变的主要有以下几个地方:部分接口,委托还有数组。下表整理了下这些支持逆变或协变的类型(自行通过in还是out判断是逆变还是协变),主要参考的MSDN文档,可能有不全,欢迎补充:
分类 | 类名 |
接口
主要是集合接口和比较接口 |
IEnumerable<out T> |
IEnumerator<out T> | |
IQueryable<out T> | |
IGrouping<out TKey, out TElement> | |
IComparer<in T> | |
IEqualityComparer<in T> | |
IComparable<in T> | |
Action<in T>
Action<in T1, in T2> … … |
|
委托 | Func<out TResult>
Func<in T, out TResult> Func<in T1, in T2, out TResult> … … |
Predicate<in T> | |
Comparison<in T> | |
Converter<in TInput, out TOutput> |
前面的例子展示了协变,再来看一个逆变的例子,现在我们把Animal改成支持比较的,比较的是两只动物年龄的大小:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class Animal: IComparable<Animal> { public string Name { get ; set ; } public int Age { get ; set ; } public int CompareTo(Animal other) { if (other == null ) return 1; return Age.CompareTo(other.Age); } } |
基于IComparable<T>逆变的支持,下面的代码可以正常工作:
1
2
3
4
5
6
|
var pandas = new List<Panda>() { new Panda() {NickName = "HuanHuan" ,Age = 12 }, new Panda() {NickName = "LeLe" , Age = 11 } }; var pandaSmallToBig = pandas.OrderBy(p=>p).ToList(); |
在排序后的列表中,LeLe在HuanHuan前面。
看过接口对协变和逆变的支持,再来看一些委托中的逆变与协变,由于现在基本上可以用Action和Func表示一切委托,所以示例也以它们为代表:
1
2
3
4
5
6
|
//逆变 Action<Animal> animalAction = ani => ani.Age = 15; Action<Panda> pandaAction = animalAction; //协变 Func<Panda> pandaFunc = () => new Panda() { NickName = "HuanHuan" , Age = 12 }; Func<Animal> animalFunc = pandaFunc; |
相信通过这四行代码足矣了解委托中的协变和逆变。
最后数组的逆变也可以用一行代码来概括:
1
2
|
var pandas = new List<Panda>(); Animal[] animal = pandas.ToArray(); |
这行代码会导致Resharper给出一个警告,说可能会因为协变出现运行时异常。
上面所介绍的都是.NET中一些内置类型对逆变和协变的支持。在我们自己的接口中也可以支持逆变或协变。
先来看看协变的使用,以代码为例,可以应用协变的场景有如下两种,注意代码注释:
1
2
3
4
5
6
7
8
9
10
|
//1.泛型类型作为返回值类型 interface ICovariant< out R> { R GetSomething(); } //2.作为委托类型参数的泛型类型 interface ICovariant< out R> { void DoSomething(Action<R> callback); } |
对于逆变一般只用于内部函数的参数类型:
1
2
3
4
|
interface IContravariant< in A> { void SetSomething(A sampleArg); } |
逆变和协变可以混合使用,但也要各自遵循上面的规则。
另外一个值得一提的,协变或逆变都布不能“继承”,以协变为例来说明:
1
2
|
interface ICovariant< out T> { } interface IDerived<T> : ICovariant<T> { } |
IDerived<T>不具备协变特性,如果需要协变,需要显示指定:
1
|
interface IDerived< out T> : ICovariant<T> { } |
注意这里IDerived也只能是协变,指定逆变是不行的。
1
2
|
//下面代码无法编译 //interface IDerived<in T> : ICovariant<T> { } |
最后特别注意,逆变和协变只针对引用类型有效,值类型无法进行逆变和协变。标记为逆变或协变的泛型类型也不能用作ref或out参数。不能给协变的泛型类型添加泛型约束,但是可以给逆变的泛型参数在泛型方法中添加泛型约束。
.NET Framework 4.0 Tuple类型
.NET 4.0中新增的Tuple这个类型很有用,当然这个类使用很简单,所以也很容易介绍。一句话概括Tuple的作用就是把不同类型的对象组合在一起。Tuple类有九个兄弟,最多的有8个泛型参数。
创建一个Tuple对象有两种方法,第一使用构造函数,如:
1
|
var student = new Tuple< int , string , int , int >(3, "小明" , 12, 95); |
或者可以使用Tuple非泛型类上的Create方法:
1
|
var student = Tuple.Create(3, "小明" , 12, 95); |
第二种方法可以省略泛型类型,Create泛型方法的泛型类型会自动推断。
有了Tuple对象之后,可以通过Item1,Item2这样的属性依次访问存入Tuple的值,注意这些属性都是只读的。
在博主接触的场景中,Tuple的作用主要有二。
其一,用做返回多个对象的函数的返回类型。在Tuple出现之前,如果一个函数需要返回多个值/对象,只能通过ref或out参数这种方式。而有了Tuple,可以把要返回的对象保证在Tuple中。这个很简单就不给代码示例了。
其二,使用Dictionary<K,V>时,有时候V是一个包含多个对象的对象。现在除了用一个类来封装所有属性之外,对于很简单的情况,可以直接放在Tuple中作为字典值。
总之,有了Tuple,代码的可读性会大大提高。
C++11 STL std::tuple类型
不知道是不是跟C#学的,在C++11标准下的STL也增加了tuple类型。这是一个模板类型,其对象的创建和成员的访问都与.NET Framework中的Tuple非常类似。
不同的是借助C++11新增的可变参数模板特性,C++11STL中的tuple可以接受任意多了个类型参数,而不像.NET Framework中的Tuple仅有九种不同的实现,一次性接受的参数个数有限。这也再次体现了C++的模板功能方面的强大。
C++11 STL中的tuple的定义和初始化方式如下:
定义,需要在模板中指明成员的类型,如下(使用C#中的场景):
1
|
std::tuple< int , std::string, int , int > student; |
如果在声明时直接初始化,必须使用直接初始化语法(tuple的构造函数为explicit,由于拷贝初始化方式隐式使用构造函数,所以无法进行拷贝初始化):
1
2
3
|
std::tuple< int , std::string, int , int > student(3, "小明" , 12, 95); //正确,显示调用构造函数 std::tuple< int , std::string, int , int > student{3, "小明" , 12, 95}; //正确,使用直接初始化 //std::tuple<int, std::string, int, int> student = {3, "小明", 12, 95};//错误,不能使用拷贝初始化方式 |
STL中也提供了make_tuple函数来构造一个tuple对象,这点与.NET中Tuple的Create方法很想,通过make_tuple,可以不用去写那一串模板类型:
1
|
auto student = make_tuple(3, "小明" , 12, 95); |
STL也提供了一个模板函数get用于访问tuple中的成员,模板参数用于传入要访问第几个成员:
1
2
3
|
auto no = get<0>(student); auto name = get<1>(student); auto age = get<2>(student); |
STL的中的tuple重写了<以及==等运算符,所以可以方便比较两个tuple对象。
同样,在C++中tuple类型最重要的一个作用也是在函数中使用tuple一次返回多个值。这个使用上也很简单也没有什么特殊之处,篇幅原因就不给出例子了。
.NET Framework 4.0 并行LINQ
并行LINQ来自于.NET Framework 4.0新增的TPL(Task Parallel Library,即任务并行库)。TPL带来了.NET 4中对并行和异步两部分的新特性,其覆盖范围相当广泛,异步部分在C#5.0又有了大改变将在下一篇文章中重点介绍,并行部分这小节只讨论PLINQ,其他话题不多介绍,园子里也相关文章太多太多。有兴趣的同学可以分别去了解异步和并行两方面TPL的新特性。奉上一张有关.NET对异步并发编程的支持的思维导图。园友们可以参考其中的分支对不了解的区域重点学习。
图1 .NET中的并发/异步编程
本节介绍其中的并行LINQ主要是考虑到前文介绍过LINQ,为了保证完整性,这里把并行LINQ也提一下。
PLINQ主要是使之前顺序执行的LINQ查询可以以并行方式来执行,从而充分利用多核CPU的计算能力。
这里同样准备一个图片帮助大家理解下PLINQ的相关方法的作用,可以将其作为一个纲要来学习其中的相关细节。
图2
一图胜千言,下面对照上面的图来大概介绍下PLINQ的某些方面。PLINQ最为.NET中最容易使用的一种并行编程技术,其“入口”只有一个AsParallel()方法。通过这个方法可以将一个普通的LINQ查询转为并行LINQ即PLINQ查询(如果经执行环境评估后面的查询不能并行执行,则依然会顺序执行)。这样LINQ的查询操作将会并行执行。除了并行执行,PLINQ支持几乎全部的LINQ查询如Select()、Where()以及那些聚合操作如Average()和Sum()等。与AsParallel()相对还可以通过AsSequence()方法将一个PLINQ转回LINQ即停止后续查询的并行执行。
PLINQ查询默认不保持原集合中元素的顺序,如果想让得到的结果保持原顺序,请在AsParallel()后使用AsOrdered()然后再编写查询操作。
对于PLINQ执行结果的使用,如果可以并行使用就尽量使用ForAll(),这样效率最好,如果使用foreach等方式使用PLINQ的集合,集合将被处理为一个序列。
在取消和异常处理方面,PLINQ使用CancellationToken和AggregateException,这与.NET环境中其他并行和异步组件的模式是相同的,很容易使用。
具体使用代码就不再举例了。此段内容仅作为抛砖引玉。
预告
下篇文章将重点介绍C#5.0和.NET 4.5带来的异步编程的巨大改进。同时也会附加介绍WinRT框架下使用C++和C#实现异步编程的方法。另外还有一些C#5.0其他方面的小改进。