C#6.0出來也有很長一段時間了,雖然新的特性和語法趨於穩定,但是對於大多數程序猿來說,想在工作中用上C#6.0估計還得等上不短的一段時間。
所以現在再來聊一聊新版本帶來的新特性可能也還不算晚吧?
一、nameof關鍵字
這絕對是整個新版本最讓我期待的內容,它給代碼重構帶來了巨大的便利。
先來看一下它是怎麼使用的吧:
string s; Console.WriteLine(nameof(s)); s = nameof(s.Length); Console.WriteLine(nameof(String)); Console.WriteLine(nameof(string.Length)); Console.WriteLine(nameof(string.Substring));
運行結果:
s
String
Length SubString
通過上面的示例,可以看出來以下幾點:
1.它不在乎變量是否已經初始化
2.它構成了一個運算結果為字符串的(編譯時)表達式
3.它可以用於取得類型名,但是nameof(string)是不能通過編譯的,小寫的string是關鍵字而不是類型(這一點很值得吐槽。。。)
4.它的括號裡面可以直接從類型取得實例屬性
5.它可以取得方法名
然後看這段代碼的IL:
IL_0000: ldstr "s" IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ldstr "String" IL_000f: call void [mscorlib]System.Console::WriteLine(string) IL_0014: ldstr "Length" IL_0019: call void [mscorlib]System.Console::WriteLine(string) IL_001e: ldstr "Substring" IL_0023: call void [mscorlib]System.Console::WriteLine(string) IL_0028: ret
編譯後看不到nameof的痕跡,編譯器把nameof的運算結果硬編碼了,所以說它是一個"編譯時運算符"。
適用場景:
1.空引用異常信息構成
2.ToString方法
3.IList數據綁定的列名
主要吐槽一下第三條吧,這是我最近工作裡遇到的很鬧心的一個事情,什麼時候用上了6.0就能徹底解決這個麻煩了。。。
想象一下以前綁定一個自定義類型的List到ListBox吧,要設定DisplayMember和ValueMember的話就只能是硬編碼,像是這樣:
listBox1.DisplayMember = "ID"; listBox1.ValueMember = "Content";
一旦要對這個綁定類型的屬性名稱進行更改,工作量簡直不敢想象。。。好一點的做法是用一套常量來代替硬編碼,但是這樣帶來的麻煩是還得記著常量名。
不過以後用上了nameof就爽快了,一個Ctrl+R,R通通搞定~
二、[.?]空引用判斷操作符
這算是一個用於簡潔代碼的語法糖吧,個人覺得實用價值一般般。
先看怎麼用的吧:
string s = null; s = s?.Substring(1); // string expr_07 = this.s; // this.s = ((expr_07 != null) ? expr_07.Substring(0) : null); Console.WriteLine(s == null);
第二行代碼與第三行被注釋掉的部分,在編譯過後是完全相等的。
同時也就是說一旦用了[.?],返回值就有可能是null,所以對於原本返回值類型的成員,只能賦值給Nullable<?>了,比如這樣:
string s = null; int? i = s?.IndexOf("."); int j = s.IndexOf(".");
至於之後再要用到變量i,很多情況下仍然需要對是否空值進行判斷。。。
同時這個語法糖也帶來了歧義,比如這樣:
object tag = form?.Tag;
由於Form和Tag都是引用類型,都可能為null,如果變量tag是null,這時候是沒辦法知道到底是form還是Tag返回了null(除非再判斷一次。。。)。
三、字符串嵌入值
同樣是一個用於簡潔代碼的語法糖,先看怎麼用吧:
int i = 1; Console.WriteLine($"{nameof(i)} + 1 = {i + 1}"); Console.WriteLine($"{i + 1} * {i + 1} = 4");
運行結果:
i + 1 = 2
2 * 2 = 4
然後是IL:
IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: ldstr "{0} + 1 = {1}" IL_0007: ldstr "i" IL_000c: ldloc.0 IL_000d: ldc.i4.1 IL_000e: add IL_000f: box [mscorlib]System.Int32 IL_0014: call string [mscorlib]System.String::Format(string, object, object) IL_0019: call void [mscorlib]System.Console::WriteLine(string) IL_001e: ldstr "{0} * {1} = 4" IL_0023: ldloc.0 IL_0024: ldc.i4.1 IL_0025: add IL_0026: box [mscorlib]System.Int32 IL_002b: ldloc.0 IL_002c: ldc.i4.1 IL_002d: add IL_002e: box [mscorlib]System.Int32 IL_0033: call string [mscorlib]System.String::Format(string, object, object) IL_0038: call void [mscorlib]System.Console::WriteLine(string) IL_003d: ret
可以看出來以下幾點:
1.大括號可以用於包裹表達式
2.相同的表達式需要計算兩次
介於第二條,對於資源消耗較多的運算,還是用一個中間變量放到$字符串中更好,要麼直接使用String.Format。
同時需要注意的是,$和@同時使用的時候必須把$寫在@之前,而在正則表達式中的大括號中的內容會被優先當做C#表達式計算一遍,比如:
Regex.IsMatch("AAA", $@"A{3}"); Regex.IsMatch("AAA", String.Format("A{0}", 3))
上下兩行的編譯結果是一樣的,然而這樣的編譯結果顯然不是我們想要的,所以我建議在正則表達式上不要使用字符串嵌入值。
四、lambda方法體
仍然是用於簡潔代碼的特性,如下:
private void LambdaMethod() => Console.WriteLine(nameof(LambdaMethod)); private string LambdaProperty => nameof(LambdaProperty);
任何用一句話就能搞定的方法從此都可以扔掉大括號和return關鍵字了。注意第二行的內容,能且僅能實現屬性的get方法,所以這構成了一個只讀屬性。
上面這兩行內容其實就是相當於這樣的:
private void LambdaMethod() { Console.WriteLine("LambdaMethod"); } private string LambdaProperty { get { return "LambdaProperty"; } }
在以前的版本我也可能這麼寫:
private Action LambdaMethod = () => Console.WriteLine(nameof(LambdaMethod));
這種寫法對於方法還好說,屬性想要這麼寫就不行了。。。當然,這種寫法總的來說是不可取的。
五、屬性初始化器
這個特性算是盼星星盼月亮終於盼來了,雖然說重要性可能不是那麼大,但是以前版本的C#居然不這麼設計著實讓我有些難以理解。。。
用法就像是在字段前加get set器,在屬性後加賦值:
private string InitedProperty { get; set; } = "InitedProperty";
和上一條特性中的lambda屬性看起來有點像,但是其實是有很大不同的:
1.帶屬性初始化器的屬性就和自動set get器屬性一樣,是有自動生成的字段的;而lambda屬性是不會自動生成私有字段的
2.屬性初始化器的等號後只能是靜態成員;而實例lambda屬性中可以是任何表達式
3.屬性初始化器等號後的表達式只會在類型加載時運算一次;而lambda屬性的表達式會在每一次調用屬性時即時運算
4.屬性初始化器不影響屬性可寫性;而lambda屬性就只能讀了
基於以上第三條,如果初始化表達式耗費資源較多,應該使用屬性初始化器而不是lambda屬性。
六、索引初始化器
可以說這個語法糖是集合初始化器的升級版,讓基於索引的集合初始化更加合理了。
現在初始化一個Dictionary可以這麼寫:
new Dictionary<int, string> { [1] = "a", [5] = "e" };
鍵值關系一目了然,而原來要初始化一個Dictionary得這麼寫:
new Dictionary<int, string> { {1, "a"}, {5, "b"} };
光是一堆大括號就實在惹人吐槽。。。需要注意,集合初始化器與索引初始化器不能混合使用,當然我相信也沒人會這麼去做。。。
另外,下面這段代碼也能夠通過編譯,不過運行時會出錯:
new List<string> { [0] = "a" };
因為對於Dictionary,編譯器知道該調用Add方法,而對於List,編譯器只知道蠢蠢地對索引器進行賦值。。。
當然,不支持List的索引初始化一方面是因為集合初始化器的語法可以應付這種情況,另一方面也是因為可能出現這樣的情況:
new List<string> { [0] = "a", [2] = "c" };
很顯然List的Add方法沒辦法完成這項工作。。。
七、異常過濾器
這個算是新特性中較為重要也是改動很大的一個部分,先來看看怎麼用的:
try { throw new IOException("Not Throw"); } catch (IOException ex) when (ex.Message != "Need Throw") { Console.WriteLine(ex.Message); } catch (NullReferenceException ex) { Console.WriteLine(ex.Message); throw; }
運行結果:
Not Throw
這種過濾如果放在以前就得寫得非常難看了:
try { throw new IOException("Not Throw"); } catch (IOException ex) { if (ex.Message != "Need Throw") { Console.WriteLine(ex.Message); } else if (ex is NullReferenceException) { Console.WriteLine(ex2.Message); throw; }
else
{
throw
} }
關鍵在於以前在catch塊中捕獲的異常沒法傳給下一個catch塊了。
看一下新版代碼的IL吧:
.try { IL_0000: ldstr "Not Throw" IL_0005: newobj instance void [mscorlib]System.IO.IOException::.ctor(string) IL_000a: throw } // end .try filter { IL_000b: isinst [mscorlib]System.IO.IOException IL_0010: dup IL_0011: brtrue.s IL_0017 IL_0013: pop IL_0014: ldc.i4.0 IL_0015: br.s IL_002b IL_0017: stloc.0 IL_0018: ldloc.0 IL_0019: callvirt instance string [mscorlib]System.Exception::get_Message() IL_001e: ldstr "Need Throw" IL_0023: call bool [mscorlib]System.String::op_Inequality(string, string) IL_0028: ldc.i4.0 IL_0029: cgt.un IL_002b: endfilter } // end filter catch { IL_002d: pop IL_002e: ldloc.0 IL_002f: callvirt instance string [mscorlib]System.Exception::get_Message() IL_0034: call void [mscorlib]System.Console::WriteLine(string) IL_0039: leave.s IL_0047 } // end handler catch [mscorlib]System.NullReferenceException { IL_003b: callvirt instance string [mscorlib]System.Exception::get_Message() IL_0040: call void [mscorlib]System.Console::WriteLine(string) IL_0045: rethrow } // end handler
好像看到了什麼不得了的東西,居然出現了一個filter塊。看來第一段代碼try塊構造的異常完全沒有進catch塊,這一點與以前的處理完全不一樣了。
同時注意到在filter塊下面還有一個未標明異常類型的catch塊,從內容來看就是對應到C#代碼的when後第一個大括號。
filter塊中大概是這麼個流程:
1.檢驗異常類型,true時走下一步,false時進入空引用異常的catch塊
2.對when中表達式進行計算
3.endfilter判斷上一步的結果,true時進入對應的catch塊,false時進入空引用異常的catch塊
可以看到,when的作用就是在catch塊前插入一個filter塊,而endfilter指令做的事情就是依據堆棧頂的值選擇進入這個catch塊還是將控制轉移到異常處理程序。
八、靜態成員引用
這個特性很久以前就在Java中出現了,而C#6.0也終於將其引入。
其實早在引入擴展方法的時候就已經破壞了定義類型可知性,然而擴展方法帶來的好處實在太大了。
使用方法如下:
using static System.String; ... Console.WriteLine(Concat("a", "b"));
注意到Concat方法是來自於String類型,也就是說靜態引用針對的是成員而不是類型,using static後面不一定是靜態類型。
這個特效帶來的好處當然就是方便省事咯,壞處也很明顯,就是比擴展方法有過之而無不及的對定義類型可知性的破壞,所以在使用這個特性的時候還是需要非常謹慎。
適用的成員必須是所有人都很清楚來由的,比如WriteLine、Format,一看就能知道方法是在Console和String類型中定義,而不是當前類型。
九、catch、finally中的await
終於可以在異常處理中愉快地使用異步編程語法糖了:
private async void Test() { try { await new Task<int>(() => { return 1; }); } catch { await new Task<int>(() => { return 1; }); } finally { await new Task<int>(() => { return 1; }); } }
最後祝願Win10能趕緊普及起來,這樣廣大的.Net程序員才能真正用上這些神兵利器。