C#的發展歷程第五 - C# 7開始進入快速迭代道路

C# 7開始,C#加快了迭代速度,多方面的打磨讓C#在易用性,效率等各方面都向完美靠近。另外得益於開源,社區對C#的進步也作了很大共享。下面帶領你們看看C# 7的新特性。其中一部分是博主已經使用過,沒用過的根據官方文檔進行了整理。express

out變量

有必定C#編程經歷的園友必定沒少寫以下這樣的代碼:編程

int speed;
if (int.TryParse(speedStr, out speed))
    speed*=10;

爲了增長程序的健壯性,在進行類型轉換時使用TryXXX方法是很好的實踐。但因爲這樣的寫法實在太顯囉嗦,因此經常咱們認爲轉換必定能正確進行時就會偷懶直接用Parse了事,固然這樣就給程序留下了出現異常的隱患。如今有了out變量支持,能夠以以下方式編寫安全的轉換:緩存

if (int.TryParse(speedStr, out int speed))
    speed*=10;

雖然if還在,但少了孤零零的變量聲明,代碼看起來已經很美觀了,終於能夠快樂的編寫健壯的代碼了。
out變量也支持類型推導,把out後面的int換成var也是徹底能夠了。另外speed變量的做用域不限於if內,在if外也是可使用的。安全

除了Parse類方法,許多Get類方法也從中受益,好比從Dictionary中按key取值:markdown

if (!dstDic.TryGetValue(fileName, out var dstFile))
{
    ...
}

並且自定義的out參數也能夠享受這樣的便利。框架

值元組

值元組是博主認爲C# 7帶來的比較重要的改進之一。由於以前Tuple雖好,但寫起來實在是太羅嗦。而值元組的出現使C#在元組的使用方面有這麼一點點接近Python(雖然在類型方面靈活性還差了這麼一丟丟)。值元組的類型ValueTuple在.NET Framework 4.7中首發,博主一度認爲只有基於.NET Framework 4.7的項目才能享受值元組的便利,甚至爲此感到遺憾(目前工做中大部分項目都是.NET Framework 4.5/4.6或.NETCore1.1/2.0)後來看到官方文檔才知道微軟單獨提供了一份System.ValueTuple的程序集供沒有原生ValueTuple類型的框架使用。
下面將經過與傳統元組對比的方式,讓各位領略值元組的簡練。異步

首先在元組類型對象的建立方面,ValueTuple看起來更簡潔。async

// 建立Tuple的兩種方式
var tuple1 = Tuple.Create(1, "小明");
var tuple2 = new Tuple<int, string>(2, "小明");
// 建立ValueTuple
var valueTuple1 = (1, "小明");

而在使用方面,Tuple使用以Item1,Item2...爲名的屬性來訪問相應位置的對象。ValueTuple也支持一樣的方式:ide

// Tuple
Console.WriteLine($"{tuple1.Item1}-{tuple1.Item2}");
// ValueTuple
Console.WriteLine($"{valueTuple1.Item1}-{valueTuple1.Item2}");

固然ValueTuple之因此好用,就是由於它有更方便的成員的訪問方式,能夠在構造一個ValueTuple時指定相應成員的訪問名稱:

var valueTuple1 = (Id: 1, Name: "小明");
Console.WriteLine($"{valueTuple1.Id}-{valueTuple1.Name}");

在C#7.1中,若是使用變量構造ValueTuple會自動推斷元組成員的名稱,如上面的代碼改造一下:

// C#7.1有效
var Id = 1;
var Name = "小明";
var valueTuple1 = (Id, Name);
Console.WriteLine($"{valueTuple1.Id}-{valueTuple1.Name}");

若是不在構造時指定訪問名稱,也能夠在聲明變量時使用下面這樣的類型:

(int Id, string Name) vt = (1, "小明");
Console.WriteLine($"{vt.Id}-{vt.Name}");

(int Id, string Name)這樣的類型聲明是C#語法爲了對值元組進行支持帶來的最大的改進。在以前,若是不使用var類型推斷則只能使用Tuple<int,string>這樣冗長的類型來進行聲明(值元組也支持ValueTuple<int,string>這樣的類型聲明,但因爲有了新的語法,這種方式基本沒有人用,(int,string)就是ValueTuple<int,string>等價的語法糖)。

另一個語法級的值元組支持就是元組析構(不得不說這種翻譯很容易混淆),如:

(var id, var name) = (1, "小明");
// 也能夠寫成
var (id,name) = (1, "小明");
Console.WriteLine($"{id}-{name}");

能夠在元組析構時,使用_來表示放棄(可能你已經在模式匹配、lambda表達式建立等地方見到過_符號的蹤影),如上面的例子,咱們只須要值元組中的一部分:

(var id, _) = (1, "小明"); // 等效於 var (id, _) = (1, "小明");
Console.WriteLine($"{id}");

C# 7還給自定義類型也帶來擴展相似元組析構功能的方法,下面的例子是來自MSDN(改了一丟丟)

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public void Deconstruct(out double x, out double y) {
        x = this.X;
        y = this.Y;
    }
}
// 析構
var p = new Point(){X=1,Y = 2};
var (x1, y1) = p;

實現的關鍵就是自定義Deconstruct函數,由例子可見x1與out參數x沒有關係,析構過程定義的變量能夠取任意名字。

基本上值元組的用法就是上面所述,值得多說的一點,當值元組應用在方法中時,能夠給返回多個值的操做提供極大的遍歷:

private static (int, string) NewStudent() {
    return (1, "小明");
}
// 使用
(int Id, string Name) = NewStudent();
Console.WriteLine($"{Id}-{Name}");

或者

private static (int Id, string Name) NewStudent() {
    return (1, "小明");
}
var stu = NewStudent();
Console.WriteLine($"{stu.Id}-{stu.Name}");

就是這麼靈活。好了,值元組部分就到這裏。

本地函數

本地函數是一種嵌套在函數內部的函數。在本地函數出現以前要想在函數內實現一個能夠被重複調用的函數通常只能選擇lambda表達式。下面展現一個博主寫代碼時遇到的例子,也是在寫這段代碼時博主才知道了本地函數的存在。

先來看一段代碼,代碼中使用TaskCompletionSource將一段調用噴碼機的EAP異步代碼包裝爲async/await代碼:

值得注意的是咱們須要在異步處理結束前取消事件的註冊,否則當這個函數被再次調用時TaskCompletionSource會拋出System.InvalidOperationException,異常消息爲:「在已經完成任務後,嘗試將任務轉換爲最終狀態。」。
解決辦法就是在事件處理函數中取消事件訂閱,在本地函數出現以前作法如上圖的代碼。
先聲明一下一個lambda表達式,而後去編寫lambda表達式的實現,在實現中取消對lambda的訂閱。必定要把聲明和賦值分開,否則沒法取消訂閱。
這種實現是徹底沒有問題的。那博主是怎麼發現本地函數這個特性的呢。這要感謝大神級插件,站在宇宙第一IDE肩膀上的宇宙第一插件 - ReSharper。仔細觀察在failHandler這個表示lambda表達式的變量下有一條綠色的下劃線,這是ReSharper在提示有更好的寫法。
鼠標焦點定位在failHandler上,並點擊此行前面出現的燈泡圖標,會看到以下提示:

樓主第一次看到也是比較愣。「local function」是什麼東西。試着點擊後發現代碼被改寫爲下面的樣子:

ConnectAsync函數中又出現了一個函數。而後難免一番搜索後發現這是C# 7的一個新特性 - 本地函數。而這段代碼又正好是本地函數一個比較好的使用例子。

最後附上本地函數的文檔地址。裏面詳細的列出了本地函數的語法,可以使用的位置及一些使用場景,值得一看。

模式匹配

C# 6中出現的異常過濾器有一點點模式匹配的味道。
C# 7全面引入的模式匹配,表如今對switch caseis進行了擴展,從而讓C#對類型的處理更加優雅。首先來看一下對switch case的擴展,下面這段方法依然是來自最近完成的一個項目中。
這個方法是一個訪問WebAPI服務獲取數據的本地代理的一部分,受到Jeffcky這篇博文的啓發,決定嘗試使用Polly庫完成超時重試等功能,同時使用Polly優雅的封裝異常,從而使調用方能夠安心的去調用。

var oc = policyRet.Outcome;
if (oc == OutcomeType.Successful)
{
    resultStr = policyRet.Result;
}
else
{
    switch (policyRet.FinalException)
    {
        case WebException _:
        case HttpRequestException _:
            resultStatus = ResultStatus.NetFailed;
            break;
        case HttpStatusErrorException statusEx when statusEx.HttpStatus == 404:
            resultStatus = ResultStatus.PageNotFound;
            break;
        case HttpStatusErrorException statusEx when statusEx.HttpStatus == 500:
            resultStatus = ResultStatus.ServerError;
            break;
        case UriFormatException _:
            resultStatus = ResultStatus.UriError;
            break;
        case TaskCanceledException _:
            resultStatus = ResultStatus.TimeOut;
            break;
        case null:
        default:
            resultStatus = ResultStatus.Unknown;
            resultStr = $"{policyRet.FinalException?.GetType()}:{policyRet.FinalException?.Message}";
            break;
    }
}

oc對象是Polly的執行返回結果,代碼中使用模式匹配加持過的switch來進行異常類型的處理。傳統的switch不能處理除了數值類型和字符串之外的其它類型的對象。而模式匹配的出現使switch成爲一個能夠取代if ... else if ...的存在。
如今case中能夠對switch內傳入的對象進行類型判斷並進入相應的分支,至關於之前這樣的判斷語句:

if (obj is Type) { ... }

如今寫成swtich case的形式,代碼看起來更加簡潔優雅。同時case分支中還新支持使用when關鍵字對類型匹配的對象進行進一步過濾。如上面代碼中第三與第四條case
對於不須要進一步對象屬性過濾的類型判斷,能夠直接使用_做爲佔位符,_佔位符這個語法在lambda表達式等中出現過。
甚至case語句還能夠直接對null進行判斷,咱們能夠放心的把可能爲空的對象直接傳入switch中。(上面代碼因爲default分支的存在,case null:是能夠省略的,這樣寫是爲了展現null的操做)

模式匹配的另外一個方面是體如今對is關鍵字的擴展,以前版本的C#將一個泛化的類型的對象轉爲具體類型的對象免不了寫如下這樣的代碼(僞代碼):

if (obj is TypeA)
{
    (TypeA)obj;
    //或
    var objA = obj as TypeA;
}

如今能夠把類型判斷與轉換合二爲一了:

if (obj is TypeA objA)
   Console.WriteLine(objA.Name);

返回結果引用

提及引用ref,這多是C#出現最先,但博主使用頻率最低的一個特性。因爲這些年寫過的代碼不多是性能敏感型的,不多有意的去注意使用一些ref參數。以前ref只適用於方法的參數,如今7.0版本的C#將ref拓展到方法的返回值。一樣博主目前沒有在任何代碼中用過這個特性,下面所寫的內容也是在學習這個特性的過程當中才瞭解到。
返回結果引用也是爲了在一些性能敏感的場景,仍是以一個例子來給讀者一個初印象:

public class Sample
{
    long[] _bigArr = { 11, 22, 33, 44, 55 };

    private ref long ReturnRef(int idx) {
        if (idx >= _bigArr.Length)
            throw new ArgumentOutOfRangeException();

        return ref _bigArr[idx];
    }

    private void TestReturnRef() {
        ref var refVal = ref ReturnRef(2);
    }
}

例子中ReturnRef就是返回引用的函數,包括調用在內的整段代碼中ref關鍵字一共出現了4次。前兩個出如今方法聲明和return語句中。後兩個分別是引用變量的聲明和表示以引用方式調用函數。它們一個都不能少。
refVal做爲一個引用變量,必須在聲明的時候直接賦值,如上面代碼中那樣,不然是不能經過編譯的。refVal這個引用變量和C++中的引用很是相似,對其賦值會修改其所指向的位置所存儲的值,如:

ref var refVal = ref ReturnRef(2);
refVal = 100;
Console.WriteLine(_bigArr[2]);

這段代碼輸出100,即修改後的值。

若是調用語句寫成以下這樣:

private void TestReturnNoRef() {
    var val = ReturnRef(2);
}

這樣也能夠執行,但這無異於調用一個返回普通值的方法,val只是一個普通的本地變量,修改它不會致使引用位置的值被修改:

ref varval = ref ReturnRef(2);
val = 100;
Console.WriteLine(_bigArr[2]);

這段代碼將輸出33。
MSDN中給出了返回引用的函數的三種使用限制,其中只有第二條,即「不能將引用返回給其生存期不超出方法執行的變量。(英文原文:You cannot return a ref to a variable whose lifetime does not extend beyond the execution of the method. ps.翻譯有問題...)」須要注意,其他兩條會引發編譯失敗能夠不提。那這最難理解的第二條是什麼意思的呢,仍是以上面的例子來描述,咱們返回的引用來自類成員變量,其生命週期大於函數的聲明週期,這是合理的,若是_bigArr是在ReturnRef方法內部聲明的本地變量,雖然不會報錯,但並無實際的使用價值,從而被列入使用限制中。

返回引用的方法這個新特性就介紹到此。更多引用方面的加強見C#7.2部分

一些其它小改進

表達式體可應用於更多場景

在以前介紹C#6的博文的這部分介紹了可使用表達式體(expression-bodied)的一些場景,如方法實現,只讀屬性的實現。C#7中擴展了表達式體可應用的場景:

  • 構造函數及析構函數
  • 屬性和索引器的get/set訪問器

如:

private Dictionary<int,string> _students = new Dictionary<int, string>();

public string this[int id]
{
    get => _students[id];
    set => _students[id] = value ?? "no name";
}

新的異步返回類型 - ValueTask

C#7.0以前異步方法支持TaskTask<T>void三種返回類型,而7.0開始語言層面支持一個新增的ValueTask<T>(須要自行添加Nuget包才能使用)做爲異步方法的返回類型。ValueTask<T>是值類型,在一些須要頻繁建立Task的場景中比引用類型的Task<T>性能更好。如:

public ValueTask<int> GetConstVal() {
    return  new ValueTask<int>(100);
}

相信一個常用異步方法的程序猿確定常遇到須要像上面這樣返回一個值的異步方法。MSDN給出的一個例子是包裝獲取緩存值的異步方法。這時用ValueTask<T>有可能得到更好的性能。

throw表達式

C#中throw做爲做爲最先一批語句出現,用於拋出異常。而C#7中throw多了一個孿生兄弟,做爲表達式的throw
這樣throw就可用於像是以前介紹的表達式體中。值得注意是像是上小節介紹的將表達式體用於構造函數,屬性初始化等場景中,若是用throw表達式拋出異常,則直接會致使對象構造失敗,最壞狀況下會致使全局未處理異常,而使程序意外退出。
一個合理的使用方式以下,改自上面表達式體使用的示例:

public string this[int id]
{
    get => _students[id];
    set => _students[id] = value ?? throw new Exception("name can't be null");
}

數值字面量改進

這個特性包括兩部分,一是使用_做爲數字間的分隔符,以使數值可讀性更好。且能夠同時應用於整數與浮點數。如:

var num1 = 123_456_789L;
var num2 = 0.123_456_789D;

另外一部分,就是C#終於開始支持二進制字面量,使用0b開頭表示這是一個二進制數值。同時也支持_做爲數字間的分隔符。在7.0中不容許0b與數字間有_,C#7.2開始則接受0b與最高位數字間有_(十進制或十六進制數字字面量也能夠以_開頭)。

var num1 = 0b0001_0011;  // C#7.0
var num2 = 0b_0001_0011; // C#7.2
var num5 = _123_456; // C#7.2
var num3 = 0x_00AA_00BB; // C#7.2

C# 7.1

最近一兩年來,C#的步伐明顯加快,C#第一次出現了0.1這樣的小版本號。可能和新的編譯器的出現及開源有關係,另外這也是第一次實現VS與語言版本的分開,以前版本的VS雖然能夠兼容不一樣版本的.NET Framework,但都只對應特定版本的C#(編譯器),從如今開始能夠獨立設置項目所使用C#語言版本。
7.x版本新增的特性與7.0版本相關的部分在上文已經一併介紹了。這裏把其它一些改進也列出來。

首先介紹下如何去設置VS中項目所使用的語言版本。以下圖,在生成選項卡中點高級,彈出的對話框中,經過語言版本的下拉菜單能夠選擇語言版本。默認項是「C#最新主要版本」,主要版本即對應7.0這種大版本。若是選擇色「C#最新的次要版本」則表示使用當前VS支持的最新語言版本,對於博主使用的VS15.4.3來講即7.1。

 

另外這個設置是配置敏感的,也就是說對於Debug和Release要進行一樣的設置,不然在進行不一樣的配置的生成時會出現問題。

異步入口方法

在以前版本的C#中入口方法即Main方法不能是異步的,因此在某些須要在Main中調用異步方法的狀況下就不得不使用Wait().GetAwaiter().GetResult()來將異步方法轉爲同步調用。而在某些狀況下這可能致使死鎖。(控制檯程序能夠直接Wait異步方法而不會死鎖)
因此新版本帶來了支持異步的Main方法,這樣不再用怕異步向上傳播了。

// 無返回值
static async Task Main(string[] args) {
    await DoSomeTask();
}
// 返回int
static async Task<int> Main(string[] args) {
    return await GetTaskResult();
}

加強的default

若是你常須要寫支持泛型的代碼,確定對default這樣的用法不陌生:

public T GetSomething<T>()
{
    return default(T);
}

C#7.1開始極大的簡化了default的使用並擴展了default的因爲範圍。
首先如今default能夠對後面的類型自動自行對推斷,上面的返回語句能夠直接寫爲return default
一樣下面的簡化也能夠:

(int, string) student = default((int,string));
// 簡化爲
(int, string) student = default;

在擴展方面,如今可使用default初始化參數默認值,如:

public NewStudent(int id, string name = default) {
    // ...
}

而在以前,聲明string類型的默認參數只能用""。在調用方法時,能夠傳入default表示相應的參數使用默認值(參數是否聲明爲可選參數沒有關係)。

private static (int Id, string Name) NewStudent(int id, string name) {
    return (id, name);
}
// 調用
NewStudent(1, default);

對於返回默認值的場景也能夠直接簡單的使用default

private (int Id, string Name) NewStudent(int id, string name)
{
    if (!string.IsNullOrEmpty(name))
        return (id, name);
    return default;
}

C# 7.2

值類型的引用加強

in參數

以前版本的C#對於經過引用傳遞值類型的參數提供了refout兩種方式,各位應該也都或多或少的用過,並且也確定知道若是不使用任何修飾來傳入值類型參數則會致使複製從而產生性能開銷。C#7.2開始新提供了in關鍵字來經過引用的方式進行值類型參數的傳遞。inref有必定的類似性,有必定限制,同時也更靈活。
in類型的參數最大的特色(必定意義上也是限制)就是不能在方法內修改in參數的值,也就是說其在方法做用域內是隻讀的,這也是in參數最大特色所在。編譯器能夠保證in參數的只讀,對於in參數的賦值或對於in參數(結構體)成員的賦值都是沒法經過編譯的。in最大的做用是傳遞比較大的結構體時能夠減小內存開支,相對而言好比傳遞int時用不用in參數都沒有更多益處,由於int所佔的空間與引用(地址)所佔的空間差很少。另外in也可用於引用類型參數,但一樣沒有太多益處。
in類型參數相對ref另外一個大不一樣就是隻須要在形參中使用in標明參數類型,在傳入實參時不須要再次添加in關鍵字。
in類型的參數支持默認值,支持實參爲常量或字面量。

in的出現,加上以前的refout,使C#在經過引用傳入傳出值類型方面功能就很完備了。

refout同樣也不能經過in來區分函數的不一樣重載。

ref readonly返回

當但願使用(只讀)引用方式來提供一個值訪問的時候可使用7.2中新增的ref readonly變量。這個特性解釋起來很簡單,直接照搬MSDN的示例代碼:

private static Point3D origin = new Point3D();
public static ref readonly Point3D Origin => ref origin;

代碼中Point3D是一個結構體,Origin就是結構體實例的一個只讀引用。使用這個引用有兩種方式:

var originValue = Point3D.Origin;
ref readonly var originReference = ref Point3D.Origin;

第一行代碼經過拷貝方式生產了一個Origin的副本。而第二行代碼傳遞的爲引用,這也是咱們但願ref readonly所發揮的做用。同時,注意對originReference的訪問都是隻讀的。

readonly struct類型

新增的只讀的struct能夠保證struct中的每一個成員都不可修改,因此只讀的struct很適合與上面介紹的in參數或ref readonly返回共同使用。因爲只讀struct的特性,編譯器不用再作其它工做來保證struct的成員不可變。同時當使用struct的成員時,編譯器會自動採用in參數的處理方式從而節省複製開銷。

只讀struct只須要struct聲明的最前面加上readonly關鍵字便可。

ref struct類型

新增的ref struct類型用於定義保證在棧空間內分配的值類型,這就意味着這中類型不能被裝箱,即不能做爲class的成員等。(更多限制參見MSDN,這些限制都是爲了保證ref struct不被放到堆中 )
ref struct類型的一個做用就是聲明ReadOnlySpan<T>這個類型,ref struct的特性保證了ReadOnlySpan<T>內索引操做的句對安全。關於Span<T>系類型推薦YOYOFx的這篇文章

靈活的命名參數位置

C# 4.0增長的命名參數有一個限制,就是當普通參數與命名參數混合使用時,名命參數必須位於最後。C#7.2中這個限制被放開,命名參數能夠位於任意位置,但要求其它位置的參數必須在恰當的順位上。繼續借用MSDN的示例,來看看對於C#7.2什麼樣的調用是合法什麼是不合法。

// 被調用的函數
static void PrintOrderDetails(string sellerName, int orderNum, string productName) {
    // 省略...
}

// C#7.2以前,混合使用時,只容許命名參數放在最後
PrintOrderDetails("Gift Shop", 31, productName: "Red Mug");

// C#7.2,混合使用,命名參數能夠在任意位置,普通參數需位於恰當的順位
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug"); // 31對應形參orderNum

// C#7.2非法示例
// PrintOrderDetails(productName: "Red Mug", 31, "Gift Shop"); // 31, "Gift Shop"無正確對應的形參,編譯不經過

private protected修飾符

新增的private pretected修飾符表示被修飾的成員能夠在下範圍內訪問:

  • 在其所在的類內部
  • 與其所在類位於同一個程序集的所在類的子類內部

值得注意的就是區分以前存在的protected internal修飾符,後者表示訪問範圍在當前程序集內部全部類以及所在類的子類(主要指那些與所在類不在同一個程序集的子類)。這麼來看它們的區別仍是很大的。

本文到此,截止到7.2版C#,應該是覆蓋了7.0到7.2全部新特性。若是7.x還有0.3或0.4出現會放在後面8.x的文章中。
歡迎各位收藏查閱。走過路過也不妨點個贊。

 

出處:https://www.cnblogs.com/lsxqw2004/p/8508003.html