1. 程式人生 > 實用技巧 >C#(99):C# 7.0 新特性(.NET Framework 4.7 與 Visual Studio 2017 )

C#(99):C# 7.0 新特性(.NET Framework 4.7 與 Visual Studio 2017 )

C#7.0 於 2017年3月 隨 .NET 4.7 和 VS2017 釋出。

一. out 變數(out variables)

以前我們使用out變數必須在使用前進行宣告,C# 7.0 給我們提供了一種更簡潔的語法 “使用時進行內聯宣告” 。如下所示:

var input = ReadLine();
if (int.TryParse(input, 
out var result))
{
    WriteLine("您輸入的數字是:{0}", result);
}
else
{
    WriteLine("無法解析輸入...");
}

上面程式碼編譯後:

int num;
string s = Console.ReadLine();
if (int.TryParse(s, out num))
{
    Console.WriteLine("您輸入的數字是:{0}", num);
}
else
{
    Console.WriteLine("無法解析輸入...");
}

原理解析:所謂的 “內聯宣告” 編譯後就是以前的原始寫法,只是現在由編譯器來完成。

備註:在進行內聯宣告時,即可直接寫明變數的型別也可以寫隱式型別,因為out關鍵字修飾的一定是區域性變數。

二. 元組(Tuples)

元組(Tuple)在 .Net 4.0 的時候就有了,但元組也有些缺點,如:

   1)Tuple 會影響程式碼的可讀性,因為它的屬性名都是:Item1,Item2.. 。

   2)Tuple 還不夠輕量級,因為它是引用型別(Class)。

   備註:上述所指 Tuple 還不夠輕量級,是從某種意義上來說的或者是一種假設,即假設分配操作非常的多。

C# 7 中的元組(ValueTuple)解決了上述兩個缺點:

   1)ValueTuple 支援語義上的欄位命名。

   2)ValueTuple 是值型別(Struct)。

1. 如何建立一個元組?

var tuple = (1, 2);// 使用語法糖建立元組
var tuple2 = ValueTuple.Create(1, 2);         // 使用靜態方法【Create】建立元組
var tuple3 = new ValueTuple<int, int>(1, 2);  // 使用 new 運算子建立元組

WriteLine($"first:{tuple.Item1}, second:{tuple.Item2}, 上面三種方式都是等價的。");

原理解析:上面三種方式最終都是使用 new 運算子來建立例項。

2. 如何建立給欄位命名的元組?

// 左邊指定欄位名稱
(int one, int two) tuple = (1, 2);
WriteLine($"first:{tuple.one}, second:{tuple.two}");

// 右邊指定欄位名稱
var tuple2 = (one: 1, two: 2);
WriteLine($"first:{tuple2.one}, second:{tuple2.two}");

// 左右兩邊同時指定欄位名稱
(int one, int two) tuple3 = (first: 1, second: 2);    /* 此處會有警告:由於目標型別(xx)已指定了其它名稱,因為忽略元組名稱xxx */
WriteLine($"first:{tuple3.one}, second:{tuple3.two}");

注:左右兩邊同時指定欄位名稱,會使用左邊的欄位名稱覆蓋右邊的欄位名稱(一一對應)。

原理解析:上述給欄位命名的元組在編譯後其欄位名稱還是:Item1, Item2...,即:“命名”只是語義上的命名。

3. 什麼是解構?(不推薦)

解構顧名思義就是將整體分解成部分。

4. 解構元組,如下所示:

var (one, two) = GetTuple();
WriteLine($"first:{one}, second:{two}");

static (int, int) GetTuple()
{
    return (1, 2);
}

原理解析:解構元組就是將元組中的欄位值賦值給宣告的區域性變數(編譯後可檢視)。

備註:在解構時“=”左邊能提取變數的資料型別(如上所示),元組中欄位型別相同時即可提取具體型別也可以是隱式型別,但元組中欄位型別

不相同時只能提取隱式型別。

5. 解構可以應用於 .Net 的任意型別,但需要編寫 Deconstruct 方法成員(例項或擴充套件)。

如下所示:

public class Student
  {
      public Student(string name, int age)
      {
          Name = name;
          Age = age;
      }
  
      public string Name { get; set; }
  
      public int Age { get; set; }
  
      public void Deconstruct(out string name, out int age)
      {
          name = Name;
          age = Age;
      }
  }

使用方式如下:

var(Name, Age) = new Student("Mike", 30);
WriteLine($"name:{Name}, age:{Age}");

原理解析:編譯後就是由其例項呼叫 Deconstruct 方法,然後給區域性變數賦值。

Deconstruct 方法簽名:

// 例項簽名
public void Deconstruct(out type variable1, out type variable2...)
  
// 擴充套件簽名
public static void Deconstruct(this type instance, out type variable1, out type variable2...)

總結:

  1. 元組的原理是利用了成員型別的巢狀或者是說成員型別的遞迴。
  2. 編譯器很牛B才能提供如此優美的語法。

使用 ValueTuple 則需要匯入: Install - Package System.ValueTuple

三. 模式匹配(Pattern matching)

1. is 表示式(is expressions)

如:

static int GetSum(IEnumerable<object> values)
  {
      var sum = 0;
      if (values == null) return sum;
  
      foreach (var item in values)
      {
          if (item is short)     // C# 7 之前的 is expressions
          {
              sum += (short)item;
          }
          else if (item is int val)  // C# 7 的 is expressions
          {
              sum += val;
          }
          else if (item is string str && int.TryParse(str, out var result))  // is expressions 和 out variables 結合使用
          {
              sum += result;
          }
          else if (item is IEnumerable<object> subList)
          {
              sum += GetSum(subList);
          }
      }
  
      return sum;
  }

使用方法:

條件控制語句(obj is type variable)
{
    // Processing...
}

原理解析:此 is 非彼 is ,這個擴充套件的 is 其實是 as 和 if 的組合。即它先進行 as 轉換再進行 if 判斷,判斷其結果是否為 null,不等於 null 則執行

語句塊邏輯,反之不行。由上可知其實C# 7之前我們也可實現類似的功能,只是寫法上比較繁瑣。

2. switch語句更新(switch statement updates)

如:

static int GetSum(IEnumerable<object> values)
  {
      var sum = 0;
      if (values == null) return 0;
  
      foreach (var item in values)
      {
          switch (item)
          {
              case 0:                // 常量模式匹配
                  break;
              case short sval:       // 型別模式匹配
                  sum += sval;
                  break;
              case int ival:
                  sum += ival;
                  break;
              case string str when int.TryParse(str, out var result):   // 型別模式匹配 + 條件表示式
                  sum += result;
                  break;
              case IEnumerable<object> subList when subList.Any():
                  sum += GetSum(subList);
                  break;
              default:
                  throw new InvalidOperationException("未知的型別");
          }
      }
  
      return sum;
  }

使用方法:

switch (item)
  {
      case type variable1:
          // processing...
          break;
      case type variable2 when predicate:
          // processing...
          break;
      default:
          // processing...
          break;
  }

原理解析:此 switch 非彼 switch,編譯後你會發現擴充套件的 switch 就是 as 、if 、goto 語句的組合體。同 is expressions 一樣,以前我們也能實

現只是寫法比較繁瑣並且可讀性不強。

總結:模式匹配語法是想讓我們在簡單的情況下實現類似與多型一樣的動態呼叫,即在執行時確定成員型別和呼叫具體的實現。

四. 區域性引用和引用返回 (Ref locals and returns)

我們知道 C# 的 ref 和 out 關鍵字是對值傳遞的一個補充,是為了防止值型別大物件在Copy過程中損失更多的效能。現在在C# 7中 ref 關鍵字得

到了加強,它不僅可以獲取值型別的引用而且還可以獲取某個變數(引用型別)的區域性引用。如:

static ref
 int GetLocalRef(int[,] arr, Func<int, bool> func)
  {
      for (int i = 0; i < arr.GetLength(0); i++)
      {
          for (int j = 0; j < arr.GetLength(1); j++)
          {
              if (func(arr[i, j]))
              { return ref arr[i, j];              }
          }
      }
  
      throw new InvalidOperationException("Not found");
  }

使用方法:

int[,] arr = { { 10, 15 }, { 20, 25 } };
ref var num = ref GetLocalRef(arr, c => c == 20);
num = 600;

Console.WriteLine(arr[1, 0]);

Print results:

使用方法:

1. 方法的返回值必須是引用返回:

     a)  宣告方法簽名時必須在返回型別前加上 ref 修飾。

     b)  在每個 return 關鍵字後也要加上 ref 修飾,以表明是返回引用。

2. 分配引用(即賦值),必須在宣告區域性變數前加上 ref 修飾,以及在方法返回引用前加上 ref 修飾。

注:C# 開發的是託管程式碼,所以一般不希望程式設計師去操作指標。並由上述可知在使用過程中需要大量的使用 ref 來標明這是引用變數(編譯後其

實沒那麼多),當然這也是為了提高程式碼的可讀性。

總結:雖然 C# 7 中提供了局部引用和引用返回,但為了防止濫用所以也有諸多約束,如:

1. 你不能將一個值分配給 ref 變數,如:

ref int num = 10;   // error:無法使用值初始化按引用變數

2. 你不能返回一個生存期不超過方法作用域的變數引用,如:

public ref int GetLocalRef(int num) => ref num;   // error: 無法按引用返回引數,因為它不是 ref 或 out 引數

3. ref 不能修飾 “屬性” 和 “索引器”。

var list = new List<int>();
ref var n = ref list.Count;  // error: 屬性或索引器不能作為 out 或 ref 引數傳遞

原理解析:非常簡單就是指標傳遞,並且個人覺得此語法的使用場景非常有限,都是用來處理大物件的,目的是減少GC提高效能。

五. 區域性函式(Local functions)

C# 7 中的一個功能“區域性函式”,如下所示:

static IEnumerable<char> GetCharList(string str)
{
    if (IsNullOrWhiteSpace(str))
        throw new ArgumentNullException(nameof(str));

    return GetList();

    IEnumerable<char> GetList()
    {
        for (int i = 0; i < str.Length; i++)
        {
            yield return str[i];
        }
    }
}

使用方法:

[資料型別,void] 方法名([引數])
{
   // Method body;[] 裡面都是可選項
}

原理解析:區域性函式雖然是在其他函式內部宣告,但它編譯後就是一個被 internal 修飾的靜態函式,它是屬於類,至於它為什麼能夠使用上級函

數中的區域性變數和引數呢?那是因為編譯器會根據其使用的成員生成一個新型別(Class/Struct)然後將其傳入函式中。由上可知則區域性函式的聲

明跟位置無關,並可無限巢狀。

總結:個人覺得區域性函式是對 C# 異常機制在語義上的一次補充(如上例),以及為程式碼提供清晰的結構而設定的語法。但區域性函式也有其缺點,

就是區域性函式中的程式碼無法複用(反射除外)。

六. 更多的表示式體成員(More expression-bodied members)

C# 6 的時候就支援表示式體成員,但當時只支援“函式成員”和“只讀屬性”,這一特性在C# 7中得到了擴充套件,它能支援更多的成員:建構函式、解構函式、帶 get,set 訪問器的屬性、以及索引器。如下所示:

public class Student
{
    private string _name;

    // Expression-bodied 建構函式
    public Student(string name) => _name = name;

    // Expression-bodied 解構函式
    ~Student() => Console.WriteLine("Finalized!");

    // Expression-bodied 屬性訪問器
    public string Name
    {
        get => _name;
        set => _name = value ?? "Mike";
    }

    // Expression-bodied 索引器
    public string this[string name] => Convert.ToBase64String(Encoding.UTF8.GetBytes(name));
}

備註:索引器其實在C# 6中就得到了支援,但其它三種在C# 6中未得到支援。

七. Throw 表示式(Throw expressions)

異常機制是C#的重要組成部分,但在以前並不是所有語句都可以丟擲異常的,如:條件表示式(? :)、null合併運算子(??)、一些Lambda表示式。而使用 C# 7 您可在任意地方丟擲異常。如:

public class Student
{
    private string _name = GetName() ?? throw new ArgumentNullException(nameof(GetName));

    private int _age;

    public int Age
    {
        get => _age;
        set => _age = value <= 0 || value >= 130 ? throw new ArgumentException("引數不合法") : value;
    }

    static string GetName() => null;
}

八. 擴充套件非同步返回型別(Generalized async return types)

在之前我們想用“async”、“await”就必須使用Task作為返回值(void特殊情況忽略),但Task是一個引用型別(class),這樣在非常簡單的任務中會造成浪費(記憶體和gc)。

以前非同步的返回型別必須是:Task、Task<T>、void,現在 C# 7 中新增了一種型別:ValueTask<T>,如下所示:

public async ValueTask<int> Func()
{
      await Task.Delay(3000);
      return 100;
}

總結:ValueTask<T> 與 ValueTuple 非常相似,所以就不列舉: ValueTask<T> 與 Task 之間的異同了,但它們都是為了優化特定場景效能而

新增的型別。

使用 ValueTask<T> 則需要匯入: Install - Package System.Threading.Tasks.Extensions

九. 數字文字語法的改進(Numeric literal syntax improvements)

C# 7 還包含兩個新特性:二進位制文字、數字分隔符,如下所示:

var one = 0b0001;
var sixteen = 0b0001_0000;
long salary = 1000_000_000;
decimal pi = 3.141_592_653_589m;

注:二進位制文字是以0b(零b)開頭,字母不區分大小寫;數字分隔符只有三個地方不能寫:開頭,結尾,小數點前後。

總結:二進位制文字,數字分隔符 可使常量值更具可讀性。