C#的变迁史05 - 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[] { 10, 20 }); int sum = Convert.ToInt32(res);不錯,很好,可是有點麻煩。
還有一種情況出現在Office程序中,例如給某單元格賦值:
((Excel.Range)excel.Cells[1, 1]).Value2= "Hello";因為Cells返回的類型要想使用Value2屬性,需要進行類型轉換。
在上面的這些例子中,因為C#是靜態語言類型,就是強類型語言,所以要使用某個類的成員,就需要在編譯的時候保證使用的是這個類的實例,或者是用反射。在這些場合下,寫這樣的代碼無疑是不夠優雅的。
在C# 4.0中,這個情況會得到改善,因為這個版本的C#天生支持運行時類型查找,那就是CLR級別的DLR特性與語法級別的dynamic類型。
在4.0中,程序可以直接寫成:
// Calculator dynamic calc = GetCalculator(); int sum = calc.Add(10, 20);// Office excel.Cells[1, 1].Value2 = "Hello";使用dynamic定義的對象,CLR將不再進行靜態查找,而是交給DLR在運行時進行動態的查找。這樣的做法無疑是拓展了程序的擴展性和約束,例如此前要實現某些公共的行為,通常是需要先定義一個接口,擁有這個行為的對象實現這個接口,這樣在程序中就可以針對這個接口進行編程。但是使用dynamic以后,這個接口就可以省掉了,直接使用成員就可以了。
dynamic作為新的類型,可以用在任何類型允許出現的場合。當然也可以用在變量的傳遞中,Runtime會自動選擇一個最匹配的方法。使用dynamic類型,就可以不去關心對象的實例是來源于COM, IronPython, HTML DOM或者反射,只要知道有什么方法可以調用就可以了,剩下的工作就交給DLR了。
其實在某種程度上,可以認為dynamic類型是object類型的一個特殊版本,除了具有object所有的特征外,還指出了對象可以動態地使用。選擇是否使用動態行為很簡單,任何對象都可以隱式轉換為dynamic,直到運行時才動態綁定。反之,從dynamic到任何其他類型都存在隱式轉換。例如:
dynamic d = 7; int i = d;上面所謂的動態操作,不僅是指方法調用,字段和屬性訪問、索引器和運算符調用,甚至委托調用都可以動態地調用,例如:
dynamic d = GetDynamicObject(…); d.M(7); d.f = d.P; d["one"] = d["two"]; int i = d + 3; string s = d(5,7);同時,任何動態操作的結果本身也是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 = 5, int z = 7); // 方法使用方式 M(1, 2, 3); // 這個沒什么可說的 M(1, 2); // 等價于(1, 2, 7) M(1); // 等價于 M(1, 5, 7)本質上,可選參數就是提供了函數參數的默認值,如果調用時不提供該參數的值,則取給定的默認值,就是這么簡單。它的出現確實極大的減少了使用函數重載的情況,否則的話每種使用默認值的調用情況都得使用重載實現。
有幾點需要說明:
1).可選參數必須有個編譯時常量作為其默認值。如果是除String之外的引用類型(包括那個特殊的dynamic類型),默認值只能是null。下面的聲明是不能通過編譯的:
static void Foo(int a, String s = "i'm a string", dynamic b = 2, 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(1, "A");M(x: 1, s: "A");M(s:"B", x:2);M1(3, s1:"Hi", s2:"Dong");M1(4, s2:"Dong", s1: "Hi");M1(5, 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 -> ParameterParameter p = new DerivedParameter();// 完全匹配MethodHandler m1 = M1;// 逆變: 參數Parameter -> BaseParameterMethodHandler m2 = M2;// 協變: 返回值DerivedResult -> BaseResultMethodHandler 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[0] = 5; string s = strings[0];這會允許將int插入strings列表中,然后將其作為string取出,這會破壞類型安全,所以IList這種允許修改元素的集合沒有添加out/in聲明。
C#4.0中的協變和逆變使得泛型編程時的類型轉換更加自然,不過要注意的是上面所說的協變和逆變都只作用于引用類型之間,例如,IEnumerable<int>不能作為IEnumerable<object>使用,因為從int到object的轉換是裝箱轉換,而不是引用轉換。而且在目前的泛型語法中,只能對泛型接口和委托使用協變和逆變。此外,一個泛型參數T只能是in或者是out,你如果即想你的委托參數逆變又想返回值協變,是做不到的。
好了,4.0的主要特性就這些,不再啰嗦了。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的C#的变迁史05 - C# 4.0篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第八节:数据库层次的锁机制详解和事务隔离
- 下一篇: 董宇辉出圈 新东方在线股价翻六倍 一场有