1. 程式人生 > >[Unity指令碼執行時更新]C#7新特性

[Unity指令碼執行時更新]C#7新特性

洪流學堂,讓你快人幾步!本文首發於洪流學堂微信公眾號。

本文是該系列《Unity指令碼執行時更新帶來了什麼?》的第5篇。
洪流學堂公眾號回覆runtime,獲取本系列所有文章。

Unity2017-2018.2中的4.x執行時已經支援到C#6,之前的文章已經介紹完畢。Unity2018.3將支援到C# 7.3,今天我們先來看看C#7.0新特效能給程式碼帶來什麼吧,不過這些特性得等到Unity2018.3才可以用哦

C#7 新特性

C#7.0增加了許多新功能,並將重點放在資料消耗,程式碼簡化和效能上。最大的更新有:元組,它可以輕鬆獲得多個結果;模式匹配可以簡化以資料型別為條件的程式碼。但是還有許多其他大小不一的新功能。

Out變數

在舊版本的C#中,使用out引數並不是很流暢。在呼叫有out引數的方法之前,首先必須宣告要傳遞給它的變數。由於通常不會初始化這些變數(畢竟它們將被方法覆蓋),也無法使用var來宣告它們,需要指定完整型別:

public void PrintCoordinates(Point p)
{
    int x, y; // 必須要預先宣告
    p.GetCoordinates(out x, out y);
    WriteLine($"({x}, {y})");
}

在C# 7.0中添加了***out變數***,可以直接在作為out引數傳遞的位置宣告變數:

public void PrintCoordinates(Point p)
{
    p.GetCoordinates(out int x, out int y);
    WriteLine($"({x}, {y})");
}

變數的作用範圍是當前程式碼塊內,後續程式碼可以使用它們。

由於out變數直接宣告作為out引數,因此編譯器通常可以告訴它們的型別應該是什麼(除非存在衝突的過載),因此可以使用var而不是明確型別來宣告它們:

p.GetCoordinates(out var x, out var y);

out引數的常見用法是Try…模式,其中布林返回值表示成功,out引數包含獲得的結果:

public void PrintStars(string s)
{
    if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }
    else { WriteLine("Cloudy - no stars tonight!"); }
}

也允許丟棄out引數,使用下劃線_可以忽略你不需要的out引數:

p.GetCoordinates(out var x, out _); // 我只在乎x

模式匹配

C# 7.0引入了*模式的概念,它是一個語法元素,可以測試一個值具有某種“形狀”,並可以從值中提取資訊。

C#7.0中的模式示例如下:

  • 常量模式c(c是C#中的常量表達式),用於判斷輸入值是否等於c
  • 型別模式T x(T是一個型別並且x是一個識別符號),它判斷輸入值的型別是否為T,如果是,則將輸入值提取到x中
  • Var模式var x(其中x是識別符號),它總是匹配,並簡單地將輸入的值放入與輸入x相同型別的新變數中。

這只是一個開始 - 模式是C#中一種新的語法,將來會將更多的元素新增到C#中。

在C#7.0中,模式增強了兩個現有的語言結構:

  • is 表示式現在可以在右側有一個模式,而不僅僅是一個型別
  • case switch語句中的子句現在可以用模式匹配,而不僅僅是常量值

具有模式的is表示式

以下是使用is具有常量模式和型別模式的表示式的示例:

public void PrintStars(object o)
{
    if (o is null) return;     // 常量模式 "null"
    if (!(o is int i)) return; // 型別模式 "int i"
    WriteLine(new string('*', i));
}

模式變數,模式引入的變數,類似於前面描述的out變數,因為它們可以在表示式的中間宣告,並且可以在最近的作用範圍內使用。也像out變數一樣,模式變數是可變的。我們經常將out變數和模式變數共同稱為“表示式變數”。

模式和try方法一起用經常能達到神器的效果:

if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }

使用模式的Switch語句

有了模式以後,switch就更強大了:

  • 可以對任意型別進行switch(不只是原始型別)
  • 模式可以在case子句中使用
  • case可以有額外條件

例如:

switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

關於這個新擴充套件的switch語句有幾點需要注意:

  • case的順序現在很重要:就像catch一樣,case不必是不相交的,匹配的第一個case會被選中。此外,就像使用catch一樣,編譯器會幫你標記永遠無法訪問到的case。C#7.0之前,你不用考慮case的順序,所以這個特性不會改變已有程式碼的行為。
  • default語句會在最後匹配:即使上面的case null是最後一個,也會在default之前匹配。這是為了與現有的switch語法相容,但是良好的做法通常會將default放在最後。
  • 最後的case null不會匹配不到:這是因為is表示式中的型別模式不匹配null,這可以確保不會因為先出現的任何型別模式而意外地捕獲null。你必須明確地處理它們(或將它們保留到default裡處理)。
  • case …:標籤引入的模式變數的作用範圍是對應的case範圍內

Tuple 元組

你可能希望從方法返回多個值,在舊版C#中可能的解決方案有:

  • out引數:使用很笨重(即使有上面的改進),也不能使用非同步方法。
  • System.Tuple<…>返回型別:使用麻煩並需要分配元組物件。
  • 為每個方法自定義返回型別:一個型別需要寫大量程式碼,只是為了臨時分組
  • 通過dynamic返回的匿名型別:效能開銷大,無靜態型別檢查。

這些解決方案都不是很好,為了更好的實現這一目標,C#7.0添加了元組型別和元組語法。

(string, string, string) LookupName(long id) // 返回元組型別
{
    ... // 獲取first,middle,last,程式碼略
    return (first, middle, last); // 元組
}

該方法現在高效地返回三個字串,封裝在元組中。

該方法的呼叫者將接收一個元組,並可以單獨訪問元組中的元素:

var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");

Item1等等是元組元素的預設名字,總是可以使用。但它們不是很具描述性,所以你可以選擇新增更好的名字:

(string first, string middle, string last) LookupName(long id) // tuple elements have names

現在,該元組的接收者可以使用描述性名稱:

var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");

您還可以直接在元組定義中指定元素名稱:

return (first: first, middle: middle, last: last); // named tuple elements in a literal

通常,只要元組各個元素可賦值,元組就可以自由轉換為其他元組型別,無論元組元素的名稱是否一樣。

元組是值型別,它們的元素是公共的,可變的欄位。它們具有值相等性,這意味著如果兩個元組的所有元素對應相等(並且具有相同的雜湊碼),則它們是相等的(並且具有相同的雜湊碼)。

這使得元組可用於多個返回值之外的許多其他情況。例如,如果你需要一個包含多個鍵的字典,可以使用元組作為key。如果你需要在每個位置具有多個值的列表,可以使用元組。

Deconstruction 解構

使用元組的另一種方法是解構後使用。解構宣告可以將元組(或其他值)分割成單獨的部分來接收對應的變數。

(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"found {first} {last}.");

也可以使用var:

(var first, var middle, var last) = LookupName(id1); // var inside

var 也可以放到外面。

var (first, middle, last) = LookupName(id1); // var outside

也可以解構到已有的變數中,稱作解構賦值

(first, middle, last) = LookupName(id2); // deconstructing assignment

解構不僅僅可以用於元組,任何型別都可以被解構,只要它有一個如下型別的解構方法(例項方法或擴充套件方法):

public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }

out引數構成解構所產生的值。
(為什麼使用out引數而不是返回元組?這樣就可以為不同數量的值設定多個過載)。

class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) { X = x; Y = y; }
    public void Deconstruct(out int x, out int y) { x = X; y = Y; }
}

(var myX, var myY) = GetPoint(); // calls Deconstruct(out myX, out myY);

以這種方式使建構函式和解構函式“對稱”是一種常見的模式。

就像輸出變數一樣,解構中可以“丟棄”你不關心的部分:

(var myX, _) = GetPoint(); // I only care about myX

本地函式

有時輔助函式只在使用它的單個方法中有意義。現在可以在函式體內將這些函式宣告為本地函式

public int Fibonacci(int x)
{
    if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
    return Fib(x).current;

    (int current, int previous) Fib(int i)
    {
        if (i == 0) return (1, 0);
        var (p, pp) = Fib(i - 1);
        return (p + pp, p);
    }
}

封閉範圍內的引數和區域性變數在區域性函式內部可用,就像它們在lambda表示式中一樣。

作為示例,作為迭代器實現的方法通常需要非迭代器包裝器方法,以便在呼叫時檢查引數。(迭代器本身在MoveNext呼叫之前不會開始執行)。本地函式非常適合這種情況:

public IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (filter == null) throw new ArgumentNullException(nameof(filter));

    return Iterator();

    IEnumerable<T> Iterator()
    {
        foreach (var element in source) 
        {
            if (filter(element)) { yield return element; }
        }
    }
}

如果Iterator旁邊是私有方法Filter,則其他成員可能意外地直接呼叫(無需引數檢查)。此外,這個私有方法Filter需要接收所有相同的引數,而不是直接使用作用範圍內的引數。

字面值改進

C# 7.0 中可以使用下劃線 _ 作為數字字面值的分隔符:

var d = 123_456;
var x = 0xAB_CD_EF;

可以將它們放在數字之間的任何位置以提高可讀性。它們對值沒有影響。

此外,C#7.0引入了二進位制字面值,因此你可以直接使用位模式,而不必使用心算來計算十六進位制表示。

var b = 0b1010_1011_1100_1101_1110_1111;

Ref返回和臨時變數

就像在C#中通過ref引用(使用修飾符)傳遞內容一樣,現在可以通過引用返回它們,並且還可以通過引用將它們儲存在區域性變數中。

public ref int Find(int number, int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        if (numbers[i] == number) 
        {
            return ref numbers[i]; // return the storage location, not the value
        }
    }
    throw new IndexOutOfRangeException($"{nameof(number)} not found");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // aliases 7's place in the array
place = 9; // replaces 7 with 9 in the array
WriteLine(array[4]); // prints 9

這對於將佔位符傳遞到大資料結構非常有用。例如,遊戲經常把資料儲存在一個大的預分配結構體陣列中(以避免GC造成的效能影響)。方法現在可以直接將引用返回給這樣的結構體,呼叫者可以通過讀取和修改這個結構體。

有一些限制來確保這是安全的:

  • 只能返回“可以安全返回”的引用:一個是傳遞進來的引用,另一個是指向物件中的欄位的引用。
  • Ref本地變數初始化在特定的儲存位置,並且不能改變指向另一個。

非同步返回型別的廣泛支援

到目前為止,C#中的非同步方法必須返回void,Task或者Task。C#7.0允許以特定方式定義其他型別,以便從非同步方法返回它們。

例如,我們現在有一個ValueTask結構體型別。它的是為了防止在Task等待時非同步操作的結果已經可用的情況下再分配物件。例如,對於涉及緩衝的許多非同步場景,這可以大大減少分配數量並導致顯著的效能提升。

你還可以通過許多其他方式建立自定義“類任務”型別。建立它們並不是很簡單,但它們很可能會開始出現在框架和API中,然後呼叫者就可以返回await它們了,就像現在的Task一樣。如果你想具體瞭解:https://github.com/dotnet/roslyn/blob/master/docs/features/task-types.md

更多表達式化的成員體

C# 6.0中引入了表示式化的方法、屬性等,但是沒有允許在所有的成員中使用。C#7.0將訪問器,建構函式和解構函式加入了進來:

class Person
{
    private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
    private int id = GetId();

    public Person(string name) => names.TryAdd(id, name); // constructors
    ~Person() => names.TryRemove(id, out _);              // finalizers
    public string Name
    {
        get => names[id];                                 // getters
        set => names[id] = value;                         // setters
    }
}

丟擲表示式

在表示式中間丟擲異常很容易:只需呼叫執行此操作的方法!但是在C#7.0中,可以直接在表示式某些地方throw:

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
    public string GetFirstName()
    {
        var parts = Name.Split(" ");
        return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
    }
    public string GetLastName() => throw new NotImplementedException();
}

小結

本文講解了C#7的新特性中對Unity程式設計有影響的新特性,不過這些特性得等到Unity2018.3才可以用哦

洪流學堂公眾號回覆runtime,獲取本系列所有文章。

把今天的內容分享給其他Unity開發者朋友,或許你能幫到他。



《鄭洪智的Unity2018課》,傾盡我8年的開發經驗,結合最新的Unity2018,帶你從入門到精通。