C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)
Lambda表達式和表達式樹
先放一張委托轉換的進化圖
看一看到lambda簡化了委托的使用。
lambda可以隱式的轉換成委托或者表達式樹。轉換成委托的話如下面的代碼:
Func<string, int> getLength = s => s.Length;轉換成表達式樹的話是下面的代碼:
Expression<Func<string, int>> getLength = s => s.Length;委托方面的東西前面都做了詳細的介紹。我們主要學習表達式樹
表達式樹
表達式是當今編程語言中最重要的組成成分。簡單的說,表達式就是變量、數值、運算符、函數組合起來,表示一定意義的式子。例如下面這些都是(C#)的表達式:
3 //常數表達式 a //變量或參數表達式 !a //一元邏輯非表達式 a + b //二元加法表達式 Math.Sin(a) //方法調用表達式 new StringBuilder() //new 表達式myString.length//MemberAccess表達式
首先澄清一個概念:表達式。表達式是一個以;結尾的句子,如果是兩句,那就不叫表達式了,比如下面不是表達式:
{ .... }代碼作為數據是一個古老的概念,.NET3. 5的表達式樹提供了一種抽象的方式將一些代碼表示成一個對象樹。 它類似于CodeDOM, 但是在一個稍高的級別上操作。 表達式樹主要用于LINQ, 本節稍后會解釋表達式樹對于整個LINQ的重要性。
System.Linq.Expressions命名空間包含了代表表達式的各個類, 它們都繼承自Expression,一個抽象的主要包含一些靜態工廠方法的類, 這些方法用于創建其他表達式類的實例。 然而,Expression 類也包括兩個屬性。
- Type屬性代表表達式求值后的.NET類型, 可把它 視為一個返回類型。 例如,如果一個表達式要獲取一個字符串的Length屬性, 該表達式的類型就是int。
- NodeType屬性返回所代表的表達式的種類。 它是ExpressionType枚舉的成員, 包括LessThan、Multiply和Invoke等。 仍然使用上面的例子,對于myString. Length 這個屬性訪問來說, 其節點類型是MemberAccess。
上面的結果最終會輸出(2+3).這意味著這些表達式樹類覆蓋了ToString來產生可讀的輸出。
就像代碼中所做的,首先創建的是葉表達式,然后自下而上的去創建這個完整的表達式。這是由“ 表達式不易變” 這一事實決定的——創建好表達式后, 它就永遠不會改變。 這樣就可以隨心所欲地緩存和重用表達式。
LambdaExpression是從Expression派生的類型之一。 泛型類Expression<TDelegate>又是從LambdaExpression派生的。
Expression和Expression<TDelegate> 類的區別在于, 泛型類以靜態類型的方式標識了它是什么種類的表達式,也就是說,它確定了返回類型和參數。 很明顯, 這是用TDelegate類型參數來表示的, 它必須是一個委托類型。 例如, 假設我們的簡單加法表達式就是一個 不獲取任何參數, 并返回整數的委托。 與之匹配的簽名就是Func<int>, 所以可以使用一個Expression <Func<int>>, 以靜態類型的方式表示該表達式。 我們用Expression.Lambda 方法來完成這件事。 該方法有許多重載版本——我們的例子使用的是泛型方法, 它用一個類型參數來指定我們想要表示的委托的類型。
static void Main(string[] args){Expression firstArg = Expression.Constant(2);Expression secondArg = Expression.Constant(3);BinaryExpression add = Expression.Add(firstArg, secondArg);Func<int> lambda = Expression.Lambda<Func<int>>(add).Compile();Console.WriteLine(lambda());Console.ReadKey();}Expression.Lambda有泛型的重載,指示表達式可以轉換為一個類型實參為Func<int>的Expression,然后,可以通過Compile方法來將表達式樹編譯成委托。
印象。我們在程序中創建了一些邏輯塊(比如Expression firstArg = Expression.Constant(2);), 將其表示成普通對象, 然后要求框架將所有的東西都編譯成可以執行的“真實” 的代碼。 你或許永遠都不需要真正以這種方式使用表達式樹, 甚至永遠都不需要在程序中構造 它們,但它提供了相當有用的背景知識,可以幫助你理解LINQ是怎樣工作的。
上面介紹的是將表達式樹編譯成lambda,下面介紹的是----
將lambda轉換成表達式樹
我們知道,Lambda表達式能顯式或隱式地轉換成恰當的委托實例。 然而, 這并非唯一能進行的轉換。 還可以要求編譯器通過你的Lambda 表達式構建一個表達式樹, 在執行時創建Expression<TDelegate> 的一個實例。 例如,下面展示了用一種精簡得多的方式 創建“返回 5” 的表達式, 然后編譯這個表達式, 并調用編譯得到的委托。
static void Main(string[] args){Expression<Func<int>> return5 = () => 5;Func<int> lambda = return5.Compile();Console.WriteLine(lambda());Console.ReadKey();}一些限制:
- 并非**所有** Lambda表達式都能轉換成表達式樹。 不能將帶有一個語句塊(即使只有一個return語句) 的Lambda 轉換成表達式樹—— 只有對單個表達式進行求值的Lambda才可以。
- 表達式中還不能包含賦值操作,因為在表達式樹中表示不了這種操作。 盡管.NET4 擴展了表達式樹的功能, 但只能轉換單一表達式這一限制仍然有效。?
- 還有一些其他的限制,不過很少見,你會在編譯錯誤的時候得到編譯器的提示
更復雜的例子:
static void Main(string[] args){Expression<Func<string, string, bool>> expression = (x, y) => x.StartsWith(y);var lambda = expression.Compile();Console.WriteLine(lambda("first that i did","first"));//trueConsole.ReadKey();}上面這個例子如果用表達式樹來做的話是非常復雜的:
static void Main(string[] args){MethodInfo method = typeof(string).GetMethod("StartsWith", new []{typeof(string)});//①構造這個方法調用的各個部件var target = Expression.Parameter(typeof(string), "x");var methodArg = Expression.Parameter(typeof(string), "y");Expression[] methodArgs =new[] {methodArg};Expression call = Expression.Call(target, method, methodArgs);//②從以上部件構建callexpressionvar lambdaParameters = new[]{target, methodArg};//③將callexpression轉化成lambdavar lambda = Expression.Lambda<Func<string,string,bool>>(call,lambdaParameters);var compiled = lambda.Compile();Console.WriteLine(compiled("first","second"));//falseConsole.WriteLine(compiled("first", "fir"));//trueConsole.ReadKey();}首先感謝編譯器能夠讓lambda隱式轉化成表達式樹!
唯一的好處是它確實更清晰地展示樹中涉及的東西以及參數是如何綁定的。 為了構造最終的方法調用表達式, 我們需要知道方法調用的幾個部件①, 其中包括: 方法的目標(也就是調用StartsWith的字符串);方法本身( MethodInfo);參數列表(本例只有一個參數)。在本例中, 方法的目標和參數恰好都是傳遞給表達式的參數, 但它們完全可能是其他表達式類型, 如常量、其他方法調用的結果、屬性的求值結果, 等等。 將方法調用構造成一個表達式之后 ?, 接著需要把它轉換成Lambda表達式 ?, 并綁定參數。 我們重用 了作為方法調用(部件)信息而創建的參數表達式的值(ParameterExpression): 創建Lambda表達式時指定的參數順序就是最終調用委托時使用的參數順序。
這是編譯好之后的表達式樹。
位于linq核心的表達式樹
沒有lambda表達式,表達式樹幾乎沒有任何價值。從一定程度上說,沒有表達式樹,lambda也就沒那么有用了。LINQ在C#的全部體現就包括lambda、表達式樹和擴展方法這三部分。
長期以來, 我們要么能在編譯時進行很好的檢查, 要么能指示另一個平臺運行一些代碼, 這些指示一般表示成文本(如SQL查詢)。 但是,魚和熊掌不可兼得(這個卻是lambda的優點,既能轉化成委托,在進程內配合迭代器進行處理集合序列,又能編譯成表達式樹,以供其他提供器翻譯成另一個平臺上的語言,比如sql)。 Lambda表達式提供 了編譯時檢查的能力, 而表達式樹可以將執行模型從你所需的邏輯中提取出來。 將兩者合并到一起之后, 魚和熊掌就能兼得 了—— 當然是在一個合理的范疇之內。“ 進程外” LINQ提供器的中心思想在于, 我們可以從一個熟悉的源語言(如 C#) 生成一個表達式樹, 將結果作為一個中間格式, 再將其轉換成目標平臺上的本地語言, 比如SQL。 某些時候, 你更多地會遇到一個本機API, 而不是一種簡單的本機語言。 例如, 這個API可能根據表達式所表示的 內容來調用不同的Web服務。下圖展示LINQ to Objects 和 LINQ to SQL 的 不同 路徑。
?
?除了LINQ,表達式樹也可以用在別的地方
1、我們在以后的內容中討論C#動態類型 時, 將看到更多關于動態語言運行時的內容。 表達式樹是其架構的核心部分。 它們具有三個特點對DLR特別有吸引力:
- 它們是不易變的, 因此可以安全地緩存;
- 它們是可組合的, 因此可以在簡單的塊中構建出復雜的行為;
- 它們 可以編譯為委托, 后者可以像平常那樣進一步JIT 編譯為本地代碼。
2、可以放心地對成員的引用進行重構
以后C#會推出一個infoof的操作符,但具體是干啥的還不知道,這里先做標記。以后來補充。
3、其他。。。。
這個回頭再來看一下書上的介紹吧。貌似沒有用到過
類型推斷和重載決策的改變
C#3中lambda表達式的加入使得原先的類型推斷和重載決策為了新的環境而做了改變。
用一個Lambda表達式調用一個泛型方法,同時傳遞一個隱式類型的參數列表,編譯器就必須推斷出你想要的是什么類型,然后才能檢查出Lambda表達式主體。
static void PrintConvertedValue<TInput, TOutput>(TInput input, Converter<TInput, TOutput> converter) { Console.WriteLine(converter(input));//C#2中,編譯將失敗,C#2類型推斷單獨針對每一個實參來進行的,從一個實參無法推斷出另一個實參 }....... PrintConvertedValue("hello world",x=>x.length);
這段代碼聽說在C#2上面是沒有辦法通過編譯的,但我沒辦法測試。因為我在用C#7。
還比如,以下代碼如法在C#2通過編譯,并提示”The type arguments for method? cannot by inferred from the useage,Try specifying the type argument explicitly."
delegate T Funcs<out T>();//定義一個委托static void WriteResult<T>(Funcs<T> func)//向方法傳遞一個定義好的委托{Console.WriteLine(func());}static void Main(string[] args){WriteResult(() => 5);//error:”The type arguments for method? cannot by inferred from the useage,Try specifying the type argument explicitly."Console.ReadKey();}在C#2中編譯器建議你顯示的為方法指定一個類型實參或強制轉換:
WriteResult< int>( delegate { return 5; }); WriteResult(( MyFunc< int>) delegate { return 5; });我們希望編譯器能像對非委托類型所做的那樣, 執行相同的類型推斷, 也就是根據返回的表達式的類型來推斷T的類型。 那正是 C# 3為匿名方法和Lambda表達式所做的事情—— 但其中存在一個陷阱。
delegate T Funcs<out T>();static void WriteResult<T>(Funcs<T> func){Console.WriteLine(func());}static void Main(string[] args){WriteResult(delegate(){if (DateTime.Now.Hour<12){return 10;}return new object();});Console.ReadKey();}在這種情況下, 編譯器采用和處理隱式類型的數組時相同的邏輯來確定返回類型,比如var objs = new[] {new object(), "hello world"};。 它構造一個集合,其中包含了來自匿名函數主體中的return 語句的所有類型 (本例是int和object), 并檢查是否集合中的所有類型都能隱式轉換成其中的一個類型。 int 到object 存在一個隱式轉換(通過裝箱),但object 到 int 就不存在了。 所以,object被推斷為返回類型。 如果沒有找到符合條件的類型, 或者找到了多個, 就無法推斷出返回類型, 編譯器會報錯。
分兩個階段進行的類型推斷
在這里, 我們打算以一種較為“籠統” 的方式來思考類型推斷—— 效果和你粗讀一下C# 語言規范差不多, 但我們的方式會更容易理解。 事實上,假如編譯器不能完全按照你希望的方式執行類型推斷, 最后幾乎肯定會造成一個編譯錯誤, 而不會生成一個行為不正確的程序。如果你的代碼未能成功編譯, 請嘗試向編譯器提供更多的信息——就那么簡單。 然而, 我下面仍然要大致地解釋一下C# 3發生的改變。
第一個巨大的改變是所有方法實參在C# 3中是一個“ 團隊” 整體。在C# 3中, 實參可提供一些信息—— 被強制隱式轉換為具體類型參數的最終固定變量的類型。 用于推斷固定值所采用的邏輯與推斷返回類型和隱式類型的數組是一樣的。
下面展示了一個例子—— 沒有使用任何Lambda表達式, 就連匿名方法都沒用。不存在協變逆變,以及方法組轉換之類的。就是單純的類型推斷
static void TestTypeParameter<T>(T firstValue, T secondValue){Console.WriteLine(typeof(T));}static void Main(string[] args){TestTypeParameter(1,new object());Console.ReadKey();}第二個改變在于, 類型推斷現在是分兩個階段進行的。 第一個階段處理的是“普通” 的實參, 其類型是一開始便知道的。 這包括那些參數列表是顯式類型的匿名函數。
稍后進行的第二個階段是推斷隱式類型的Lambda表達式和方法組的類型。 其思想是, 根據我們迄今為止拼湊起來的信息, 判斷是否足夠推斷出Lambda表達式(或方法組) 的參數類型。 如果能, 編譯器就可以檢查Lambda 表達式的主體并推斷返回類型—— 這個返回 類型通常能幫助我們確定當前正在推斷的另一個類型參數。 如果第二個階段提供了更多的信息, 就重復執行上述過程, 直到我們用光了所有線索, 或者最終推斷出涉及的所有類型參數。
static void PrintConvertedValue< TInput, TOutput> (TInput input, Converter< TInput, TOutput> converter){
Console. WriteLine( converter( input));
}
...
PrintConvertedValue(" I' m a string", x => x. Length);
還是使用上面的那個代碼,來說明:
1、階段1開始。
2、第1個參數是TInput類型, 第1個實參是string類型。 我們推斷出肯定存在從string 到TInput的隱式轉換。
3、第2個參數是 Converter< TInput, TOutput> 類型, 第2個實參是一個隱式類型的Lambda表達式。 此時不執行任何推斷, 因為我們沒有掌握足夠的信息。
4、階段2開始。
5、TInput不依賴任何非固定的類型參數, 所以它被確定為string。
6、第2個實參現在有一個固定的輸入類型, 但有一個非固定的輸出類型。 我們可把它視為( string x) => x. Length, 并推斷出其返回類型是int。 因此, 從int到TOutput 必定會發生一個隱式轉換。
7、重復“ 階段 2”。
8、TOutput不依賴任何非固定的類型參數,所以它被確定為int。
9、完成了。
再看一下下面這個:多級推斷
static void Main(string[] args){ConvertTwice("AnotherString",x=>x.Length, length => Math.Sqrt(length));Console.ReadKey();}static void ConvertTwice<TInput, TMiddle, TOutput>(TInput input, Converter<TInput, TMiddle> firstConverter,Converter<TMiddle, TOutput> secondConverter){TMiddle middle = firstConverter(input);TOutput output = secondConverter(middle);Console.WriteLine(output);}類型推斷的“階段1” 告訴編譯器肯定存在從string到TInput 的一個轉換。 第一次執行“ 階段 2” 時, TInput固定為string, 我們推斷肯定存在從int到TMiddle的一個轉換。 第二次執行“ 階段 2” 時, TMiddle固定為int, 我們推斷肯定存在從double到TOutput的一個轉換。 第三次執行“階段2” 時, TOutput 固定 為 doluble, 類型推斷成功。 當類型推斷結束后, 編譯器就可以正確地理解Lambda表達式中的代碼。
?注意:Lambda 表達式的主體只有在輸入參數的類型已知之后才能進行檢查。上面的例子中,只有編譯器只有等到階段1完事兒了,判斷出TInput的類型是string之后,才會進入第二階段,對第一個Converter進行檢查。以此類推。
C#3選擇正確的被重載的方法(方法組)
首先看一下直接調用方法組
有一個有趣的規則是“更好的轉換”規則,拿個例子來說:
void Write( int x) ; void Write( double y);Write( 1.5) 的含義顯而易見, 因為不存在從 double 到 int 的 隱式 轉換, 但 Write( 1) 對應的調用就麻煩一些。 由于存在從 int 到 double 的隱式轉換, 所以以上兩個方法似乎都合適。 在這種情況下, 編譯器會考慮從int 到 int 的轉換, 以及從 int 到 double 的轉換。 從 任何類型“ 轉換成它本身” 被認為好于“ 轉換成一個不同的類型”。 這個規則稱為“ 更好的轉換” 規則。 所以對于這種特殊的調用, Write( int x) 方法被認為好于Write( double y)。
?
委托返回類型影響了重載選擇
static void Execute(Func<int> action){Console.WriteLine($"method returns a int:{action()}");}static void Execute(Func<double> action){Console.WriteLine($"method returns a double:{action()}");}static void Main(string[] args){Execute(()=> 4.3);//①Execute(()=>4);//②Console.ReadKey();}
對Execute的調用可以換用一個匿名方法來寫, 也可以換用一個方法組—— 不管以什么方式, 凡是涉及轉換, 所應用的規則都是一樣的。同樣這個代碼如果在C#2中應用的話編譯器還是會報錯,但是現在添加了新的語言規則:‘
如果一個匿名函數能轉換成參數列表相同, 但返回類型不同的兩個委托類型, 就根據從“ 推斷 的返回類型”到“委托的返回類型” 的轉換來判定哪個委托轉換“ 更好”。
總結以下本節的重點:
- 匿名函數(匿名方法和Lambda表達式) 的返回類型是根據所有return語句的類型來推斷的;
- Lambda表達式要想被編譯器理解, 所有參數的類型必須為已知;
- 類型推斷不要求根據不同的(方法) 實參推斷出的類型參數的類型完全 一致, 只要推斷出來的結果是兼容的就好;
- 類型推斷現在分階段進行, 為一個匿名函數推斷的返回類型可作為另一個匿名函數的參數類型使用;
- 涉及匿名函數時, 為了找出“ 最好” 的重載方法, 要將推斷的返回類型考慮在內。
?
轉載于:https://www.cnblogs.com/pangjianxin/p/8669313.html
總結
以上是生活随笔為你收集整理的C#复习笔记(4)--C#3:革新写代码的方式(Lambda表达式和表达式树)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 不要機翻謝謝!!
- 下一篇: 4万平方米需多大电锅炉