程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> [C#]6.0新特性淺談,

[C#]6.0新特性淺談,

編輯:C#入門知識

[C#]6.0新特性淺談,


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程序員才能真正用上這些神兵利器。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved