"為了使LINQ能夠正常工作,代碼必須簡化到它要求的程度。" - Jon Skeet
為了提高園子中諸位兄弟的英語水平,我將重要的術語后面配備了對應的英文。
隱式類型的局部變量
隱式類型允許你用var修飾類型。用var修飾只是編譯器方便我們進行編碼,類型本身仍然是強類型的,所以當編譯器無法推斷出類型時(例如你初始化一個變量卻沒有為其賦值,或賦予null,此時就無法推斷它的類型),用var修飾就會發生錯誤。另外,只能對局部變量使用隱式類型。
使用隱式類型的幾個時機:
- 當變量的類型太長或者難以推測,但類型本身不重要時,比如你的LINQ語句中用了Groupby,那么一般來說基本很少人可以准確地推測出結果的類型吧。。。
- 當變量初始化時,此時可以根據new后面的類型得知變量類型,故不會對可讀性造成影響
- 在Foreach循環中你迭代的對象,此時一般不需要顯式指出類型
總的來說,如果使用隱式類型導致你的代碼的可讀性下降了,那么就改用顯式類型。一般第二條原則已經是一個不成文的規定了。Resharper在檢測到變量初始化時,如果你沒有使用隱式類型,也會提醒你可以用var代替之。
LINQ中隱式類型的體現:你可以統統用var來修飾LINQ語句返回的類型。一般來說LINQ語句的返回類型通常名字都比較長,而且也不是十分顯而易見。如果沒有隱式類型,在寫代碼時就會比較痛苦。
自動實現的屬性
現在應該滿世界都在用自動實現的屬性了。注意在結構體中使用自動實現的屬性(注意字段不需要),需要顯式的調用無參構造函數this()。這是結構體和類的一個區別。
public struct Foo { public int a { get; private set; } Foo(int A) : this() { a = A; } }
上面代碼如果去掉this()將會發生錯誤,在默認無參構造函數將結構體的屬性設為默認值之前,不能使用這些屬性。如果將上面代碼的屬性改為字段,則即使不調用this()也不會有問題。
匿名類型(Anonymous Type)
匿名類型允許你直接在括號中建立一個類型。雖然不需要指定成員的具體類型,但匿名類型的成員都是強類型的。
static void Main(string[] args) { var tom = new {Name = "Tom", Age = 15}; Console.WriteLine("{0}: {1}", tom.Name, tom.Age); }
對匿名類型進行初始化之后,就可以如同實際類型一樣使用點符號獲取匿名類型的成員,但變量tom只能用var或者object修飾。如果兩個匿名類型有相同數量的成員,且所有成員擁有相同的類型名稱和值的類型,而且以相同的順序出現,則編譯器會將它們看作是同一個類型。
static void Main(string[] args) { var family = new[] { new {Name = "Tom", Age = 15}, new {Name = "Jerry", Age = 16} }; var cat = new {Age = 27, Name = "Cat"}; var dog = new {Age = 2222222222222222, Name = "Dog"}; }
如果在初始化中交換了屬性的順序,或者某個屬性使用了long而不是int,則會引入一個新的匿名類型。
匿名類型包含了一個默認的構造函數,它獲取你賦予的所有初始值。另外,它包含了你定義的類型成員,以及繼承自object類型的若干方法(重寫的Equals, 重寫的GetHashCode, ToString等等)。同一個匿名類型的兩個實例在判斷相等性時,采用的是依次比較每個成員的值的方式。
在LINQ中,我們可以使用匿名類型來裝載查詢返回的數據,尤其是最后使用Select或SelectMany等方法返回若干列時。在每次查詢都要為返回數據定制一個類顯得太繁瑣了,雖然有時候是需要的(ViewModel),但也有時候只是為了一次性的展示數據。如果你要創建的類型只在一個方法中使用,而且其中只有簡單的字段或者屬性而沒有方法,則可以考慮使用匿名類型。
表達式和表達式樹(Expression & Expression Tree)
Express是表達的意思(它還有很多其他意思,例如快速的),加上名詞后綴-sion即為表達式。
表達式是當今編程語言中最重要的組成成分。簡單的說,表達式就是變量、數值、運算符、函數組合起來,表示一定意義的式子。例如下面這些都是(C#的)表達式:
- 3 //常數表達式
- a //變量或參數表達式
- !a //一元邏輯非表達式
- a + b //二元加法表達式
- Math.Sin(a) //方法調用(lambda)表達式
- new StringBuilder() //new 表達式
表達式的一個重要的特點是它可以無限組合,只要符合正確的類型和語義。表達式樹則是將表達式轉換為樹形結構,其中每個節點都是表達式。表達式樹通常被用於轉換為其他形式的代碼。例如LINQ to SQL將表達式樹轉譯為SQL。
最基本的幾種表達式
- 常量表達式:Expression.Constant(常量的值);
- 變量表達式:Expression.Parameter(typeof(變量類型), "變量名稱")
- 二元表達式,即需要兩個表達式作為參數進行操作的表達式:Expression.[某個二元表達式的方法,例如加減乘除,模運算等](表達式1, 表達式2);
- Lambda表達式:表達一個方法,可以接受一個代碼段或一個方法調用表達式作為方法,以及一組方法參數。Lambda為一希臘字母,無法翻譯。希臘字母還有很多,例如阿爾法,貝塔等。之所以選擇這個字母是因為來自數學上的原因(數學上有lambda運算)
構建一個最簡單的表達式樹1+2+3
表達式樹是對象構成的樹,其中每個節點都是表達式。可以說,每個表達式都是一個表達式樹,特別的,某些表達式可以看成只有一個節點的表達式樹,例如常量表達式。System.Linq.Expressions命名空間下的Expression類和它的諸多子類就是這一數據結構的實現。Expression類是一個抽象類,主要包含一些靜態工廠方法。Expression類也包含兩個屬性:
- Type:代表表達式求值之后的.net類型,例如Expression.Constant(1)和Expression.Add(Expression.Constant(1), Expression.Constant(2))的類型都是Int32。
- NodeType:代表表達式的種類。例如Expression.Constant(1)的種類是Constant,Expression.Add(Expression.Constant(1), Expression.Constant(2))的種類是Add。
每個表達式都可以表示成Expression某個子類的實例。例如BinaryExpression就表示各種二元運算符(例如加減乘除)的表達式。它需要兩個運算數(注意運算數也是表達式):
public static BinaryExpression Add(Expression left, Expression right);
Expression各個子類的構造函數都是不公開的,要創建表達式樹只能使用Expression類提供的靜態方法。
要創建一個表達式樹,首先我們要畫出這個樹,並找出它需要什么類型的表達式。例如如果我們要創建1 + 2 + 3這個表達式的表達式樹,因為它太簡單而且不包含多於一種運算(如果有加有乘還要考慮優先級),我們可以一眼看出,其只需要兩種表達式,常量表達式(形容1,2,3)和二元表達式(形容加法),所以可以這樣寫:
ConstantExpression exp1 = Expression.Constant(1); ConstantExpression exp2 = Expression.Constant(2); BinaryExpression exp12 = Expression.Add(exp1, exp2); ConstantExpression exp3 = Expression.Constant(3); BinaryExpression exp123 = Expression.Add(exp12, exp3);
這個應該非常好理解。但如果我們想寫出Math.Sin(a)這個表達式的表達式樹怎么辦呢?為了解決這個問題,Lambda表達式登場了,它可以表示一個方法。
使用Lambda表達式表示一個函數
我們的目標是使用Lambda表達式表示Math.Sin(a)這個表達式。Lambda表達式代表一個函數,現在它具有一個輸入a(我們使用變量表達式ParameterExpression來代表,它應該是double類型),以及一個方法調用,這需要MethodCallExpression類型的表達式,方法名為Sin,位於Math類中。我們需要使用反射找出這個方法。
代碼如下:
ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //參數a MethodCallExpression expCall = Expression.Call(typeof(Math).GetMethod("Sin", BindingFlags.Static | BindingFlags.Public), expA); //Math.Sin(a) LambdaExpression exp = Expression.Lambda(expCall, expA); // a => Math.Sin(a)
使用Lambda表達式:通過Expression<TDelegate>
Expression<TDelegate>泛型類繼承了LambdaExpression類型,它的構造函數接受一個Lambda表達式。此處TDelegate指泛型委托,它可以是Func或者Action。泛型類以靜態的方式確定了返回類型和參數的類型。
對於上個例子,我們的輸入和輸出均為一個Double類型,故我們需要的委托類型是Func<double, double>:
Expression<Func<double, double>> exp2 = d => Math.Sin(d);
可以使用Compile方法將Expression<TDelegate>編譯成TDelegate類型(在這個例子中,編譯之后的對象類型為Func<double,double>),這是一個將表達式樹編譯為委托的簡便方法(不需要再一步一步來,並且使用反射了)。編譯器自動實現轉換。
然后就可以直接調用,獲得表達式計算的結果:
Expression<Func<double, double>> exp2 = d => Math.Sin(d); Func<double, double> func = exp2.Compile(); Console.WriteLine(func(0.5));
練習:使用兩種方法構建表達式樹(a, b, m, n) => m * a * a + n * b * b
假定所有的變量類型都是double。
代碼法:
//(a, b, m, n) => m * a * a + n * b * b ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //參數a ParameterExpression expB = Expression.Parameter(typeof(double), "b"); //參數b ParameterExpression expM = Expression.Parameter(typeof(double), "m"); //參數m ParameterExpression expN = Expression.Parameter(typeof(double), "n"); //參數n BinaryExpression multiply1 = Expression.Multiply(expM, expA); BinaryExpression multiply2 = Expression.Multiply(multiply1, expA); BinaryExpression multiply3 = Expression.Multiply(expN, expB); BinaryExpression multiply4 = Expression.Multiply(multiply3, expB); BinaryExpression add = Expression.Add(multiply2, multiply4);
委托法:
Expression<Func<double, double, double, double, double>> exp4 = (a, b, m, n) => m*a*a + n*b*b; var ret = exp4.Compile(); Console.WriteLine(ret.Invoke(1, 2, 3, 4)); // =3*1*1+4*2*2=3+16=19
通過Expression<TDelegate>以及Compile方法,我們可以方便的計算表達式的結果。但如果一步步來,我們還需要手動遍歷這棵樹。既然使用代碼構造表達式如此麻煩,為什么還要這樣做呢?只是因為在手動遍歷和計算表達式結果時,可以插入其他操作。LINQ to SQL就是通過遞歸遍歷表達式樹,將LINQ語句轉換為SQL查詢的,這是委托所不能替代的。
不是所有的Lambda表達式都能轉化成表達式樹。不能將帶有一個代碼塊的Lambda轉化成表達式樹。表達式中還不能有賦值操作,因為在表達式樹中表示不了這種操作。
參考資料:表達式樹上手指南 http://www.cnblogs.com/Ninputer/archive/2009/08/28/expression_tree1.html
擴展方法(Extension Method)
擴展方法可以理解成,為現有的類型(現有類型可以為自定義的類型和.Net 類庫中的類型)擴展(添加)一些功能,附加到該類型中。
當我們要擴展某個類的功能時,有以下幾種方法:一是直接修改類的代碼,這可能會導致向后兼容的破壞(不符合開閉原則)。一是派生子類,但這增加了維護的工作量,而且對於結構和密封類根本不能這么做。擴展方法允許我們在不創建子類,不更改類型本身的情況下,仍然可以修改類型。
擴展方法必須定義於靜態的類型中,且所有的擴展方法必須是靜態的。還是那句話,當你了解了類型對象時,你就很自然的理解了為何擴展方法必須是靜態的。(它自類型對象被創建時就應當在對象的方法表中)
擴展方法的第一個輸入參數要加上this(第一個參數的類型表示被擴展的類型)。擴展方法必須至少要有一個輸入參數。
被擴展的類型的所有子類自動獲得該擴展方法。
當你的工程內有特定邏輯,且其基於一個比較普遍的類時,考慮使用擴展方法。如果你想為類型添加一些成員,但又不能更改類型本身(因為不屬於你)時,考慮使用擴展方法。例如當你需要頻繁判斷字符串是否為Email時,你可以擴展String類,將這個判斷方法單獨置於一個叫做StringExtension的類型中,方便管理。之后你就可以通過調用String.IsEmail來方便的使用這個方法了。
C#中提供了兩個特別醒目的類:Enumerable和Queryable。兩者都在System.Linq命名空間中。在這兩個類中,含有許許多多的擴展方法。Enumerable的大多數擴展的是IEnumerable<T>,Queryable的大多數擴展的是IQueryable<T>。它們賦予了集合強大的查詢能力,共同構成了LINQ的重要基礎。
什么是閉包(Closure)?C#如何實現一個閉包?
閉包是一種語言特性,它指的是某個函數獲取到在其作用域外部的變量,並可以與之互動。Closure這個單詞顯然來自動詞close,有點動詞名詞化的意思。
通過匿名函數或者lambda表達式,我們可以實現一個簡單的閉包:
static void Main(string[] args) { //外部變量 var i = 0; //lambda表達式捕獲外部變量 //在外部變量的作用域內聲明了一個方法 MethodInvoker m = () => { //使用外部變量 i = i + 1; }; m.Invoke(); //打印出1 Console.WriteLine(i); }
此處函數和來自外部的變量i進行了互動。
匿名函數(Anonymous Function)
匿名函數出現於C# 2.0,它允許在一個委托實例的創建位置內聯地指定其操作。
例如我們可以這樣寫:
Compare(c1, c2, delegate(Circle a, Circle b) { if (a.Radius > b.Radius) return 1; if (a.Radius < b.Radius) return -1; return 0; });
匿名方法的語法:先是一個delegate關鍵字,再是參數(如果有的話),隨后是一個代碼塊,定義了對委托實例的操作。逆變性不適用於匿名方法,必須指定和委托類型完全匹配的參數類型(在本例中是兩個Circle類型)。
通過在匿名方法中加入return來獲得返回值。.NET 2中很少有委托有返回值(因為多個委托形成委托鏈之后,前面的返回值會被后面的覆蓋),但LINQ中大部分委托都有返回值(通過Func泛型委托)。
使用匿名方法的主要好處是:不需要為一個函數命名,尤其是那種只用一次的函數,或者很短很簡單的函數。當你了解了lambda表達式之后,就會發現在linq中,到處都是lambda表達式,而里面其實都是匿名函數(即委托)。如果我們在頻繁使用linq的過程中,每次都要在外部建立一個函數,那代碼的體積將會大大增加。
另外匿名函數還有很重要的一點,就是自動形成閉包。匿名函數內定義的變量稱為匿名函數的局部變量,和普通函數不同的是,匿名函數除了可以使用局部變量,傳入的變量之外,還可以使用捕獲變量。當外部的變量被匿名函數在函數方法中使用時,稱為該變量被捕獲(即它成為了一個捕獲變量)。
捕獲的是變量的實例而不是值,也就是說,在匿名函數內的捕獲變量和外部的變量是同一個。當變量被捕獲時,值類型的變量自動“升級”,變成一個密封類。創建委托實例不會導致執行。
捕獲變量(Captured Variable)的作用
捕獲變量可以方便我們在創建匿名方法(或委托)時,獲得所需要的變量。例如如果你有一個整型的列表,並希望寫一個匿名方法篩選出小於某數limit的另一個列表,此時如果沒有捕獲變量,在匿名方法中我們就只能硬編碼Limit的值,或者使用原始的委托,將變量傳入委托的目標方法。
static IEnumerable<int> Filter(List<int> aList, int limit) { //lambda表達式捕獲外部變量Limit return aList.Where(a => a < limit); }
捕獲變量的生存期
只要還有委托引用這個捕獲變量,它就會一直存在。不管這個捕獲變量是值類型還是引用類型,編譯器會為其生成一個額外的類。
public delegate void MethodInvoker(); static void Main(string[] args) { MethodInvoker m = CreateDelegate(); //由於有委托引用a,a將會一直存在 //捕獲變量a不再位於棧上,編譯器將其視為一個額外的類 //CreateDelegate方法擁有對這個額外的類的一個實例的引用 //當委托被回收之前,不會回收這個額外的類 m(); } static MethodInvoker CreateDelegate() { int a = 1; MethodInvoker m = () => { Console.WriteLine(a); a++; }; m(); return m; }
打印出1和2。輸出1是因為在調用CreateDelegate時,變量a是可用的。當CreateDelegate返回之后,調用m,a仍然是可用的,並沒有隨之消失。由於被捕獲而形成閉包,a由一個棧上的值類型變成了引用類型。編譯器生成了一個額外的密封類(名字是比較沒有可讀性的,例如c__DisplayClass1),它擁有一個成員a和一個方法,該方法內部的代碼就是MethodInvoker中的代碼。
CreateDelegate持有一個類型c__DisplayClass1的引用,所以它一直都能使用c__DisplayClass1中的成員a。
internal class Program { public delegate void MethodInvoker(); [CompilerGenerated] private sealed class <>c__DisplayClass1 { public int a; public void <CreateDelegate>b__0() { Console.WriteLine(this.a); this.a++; } } private static void Main(string[] args) { Program.MethodInvoker methodInvoker = Program.CreateDelegate(); methodInvoker(); Console.ReadKey(); } private static Program.MethodInvoker CreateDelegate() { Program.<>c__DisplayClass1 <>c__DisplayClass = new Program.<>c__DisplayClass1(); <>c__DisplayClass.a = 1; Program.MethodInvoker methodInvoker = new Program.MethodInvoker(<>c__DisplayClass.<CreateDelegate>b__0); methodInvoker(); return methodInvoker; } }
面試題:共享和非共享的捕獲變量
在閉包和for循環一起使用時,如果多個委托捕捉到了同一個變量,則會有兩種情況:捕捉到了同一個變量僅有的一個實例,和捕捉到同一個變量,但每個委托擁有自己的一個實例。
static void Main() { int copy; List<Action> actions = new List<Action>(); for (int counter = 0; counter < 10; counter++) { //只有一個變量copy,它在循環開始之前已經創建 //所有的委托共享這個變量 copy = counter; //創建委托時不會執行 actions.Add(() => Console.WriteLine(copy)); } foreach (Action action in actions) { //執行委托時打印copy當前的值 //copy當前的值是9 action(); } Console.ReadKey(); }
在這個例子中,捕獲變量是copy,它只有一個實例(它的定義在外面,被捕獲之后,自動升級為引用類型),所有委托共享這個實例。最后打印出10個9。
static void Main() { int copy; List<Action> actions = new List<Action>(); for (int counter = 0; counter < 10; counter++) { copy = counter; //現在有十個內部變量,每個委托有一個實例,不同委托擁有的實例值是不同的 //從而委托可以輸出0-9 int copy1 = copy; //創建委托時不會執行 actions.Add(() => Console.WriteLine(copy1)); } foreach (Action action in actions) { //執行委托時打印copy1的值 action(); } Console.ReadKey(); }
使用內部變量解決多個委托共享一個捕獲變量實例的問題。下面的代碼中,包含了上面所說的兩種情況,可以思考下最終的打印結果:
static void Main(string[] args) { var list = new List<MethodInvoker>(); for (int index = 0; index < 5; index++) { var counter = index*10; list.Add(delegate { Console.WriteLine("{0}, {1}", counter, index); counter++; }); } list[0](); list[1](); list[2](); list[3](); list[4](); list[0](); list[0](); list[0](); Console.ReadKey(); }
其中循環內部建立了五個MethodInvoker。它們共享一個變量index的實例,但各自有自己的變量counter的實例。所以最終打印的結果中,index的值將總是5,而counter的值則每次都不同。
最后額外執行了第一個委托三次,此時counter的值會使用第一次,第一個委托運行之后counter的值,故會打出1,之后打印2,3同理。如果你額外執行第二個委托一次,將會打出11。這充分說明了每個委托都持有一個counter的實例,且它們是相互獨立的。而無論執行任意一個委托多少次,index的值都是5。
foreach循環中捕獲變量的變化
在C# 5中,foreach循環的行為變了,不會再出現多個委托共享一個變量的行為。所以我們即使不聲明內部變量,方法也會打印出令人容易理解的結果:
static void Main() { List<string> values = new List<string> {"a", "b", "c"}; var actions = new List<Action>(); foreach (string s in values) { //匿名方法捕獲變量s //類比for循環最后的10個9,s最后的值是c //理論上會打印出三個c //但在c# 5中,會打印出a,b,c actions.Add(() => Console.WriteLine(s)); } foreach (Action action in actions) { action(); } Console.ReadKey(); }
但對於for語句,行為和之前一樣,仍然需要注意捕獲變量被共享的問題。