洪流學(xué)堂,讓你快人幾步荆虱!本文首發(fā)于洪流學(xué)堂微信公眾號蒿偎。
本文是該系列《Unity腳本運(yùn)行時(shí)更新帶來了什么?》的第5篇怀读。
洪流學(xué)堂公眾號回復(fù)runtime
诉位,獲取本系列所有文章。
Unity2017-2018.2中的4.x運(yùn)行時(shí)已經(jīng)支持到C#6菜枷,之前的文章已經(jīng)介紹完畢苍糠。Unity2018.3將支持到C# 7.3,今天我們先來看看C#7.0新特性能給代碼帶來什么吧啤誊,不過這些特性得等到Unity2018.3才可以用哦岳瞭。
C#7 新特性
C#7.0增加了許多新功能,并將重點(diǎn)放在數(shù)據(jù)消耗蚊锹,代碼簡化和性能上瞳筏。最大的更新有:元組,它可以輕松獲得多個(gè)結(jié)果牡昆;模式匹配可以簡化以數(shù)據(jù)類型為條件的代碼姚炕。但是還有許多其他大小不一的新功能。
Out變量
在舊版本的C#中丢烘,使用out參數(shù)并不是很流暢柱宦。在調(diào)用有out參數(shù)的方法之前,首先必須聲明要傳遞給它的變量播瞳。由于通常不會初始化這些變量(畢竟它們將被方法覆蓋)掸刊,也無法使用var來聲明它們,需要指定完整類型:
public void PrintCoordinates(Point p)
{
int x, y; // 必須要預(yù)先聲明
p.GetCoordinates(out x, out y);
WriteLine($"({x}, {y})");
}
在C# 7.0中添加了out變量赢乓,可以直接在作為out參數(shù)傳遞的位置聲明變量:
public void PrintCoordinates(Point p)
{
p.GetCoordinates(out int x, out int y);
WriteLine($"({x}, {y})");
}
變量的作用范圍是當(dāng)前代碼塊內(nèi)忧侧,后續(xù)代碼可以使用它們石窑。
由于out變量直接聲明作為out參數(shù),因此編譯器通巢园兀可以告訴它們的類型應(yīng)該是什么(除非存在沖突的重載)尼斧,因此可以使用var而不是明確類型來聲明它們:
p.GetCoordinates(out var x, out var y);
out參數(shù)的常見用法是Try...模式,其中布爾返回值表示成功试吁,out參數(shù)包含獲得的結(jié)果:
public void PrintStars(string s)
{
if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }
else { WriteLine("Cloudy - no stars tonight!"); }
}
也允許丟棄out參數(shù)棺棵,使用下劃線_可以忽略你不需要的out參數(shù):
p.GetCoordinates(out var x, out _); // 我只在乎x
模式匹配
C# 7.0引入了*模式的概念,它是一個(gè)語法元素熄捍,可以測試一個(gè)值具有某種“形狀”烛恤,并可以從值中提取信息。
C#7.0中的模式示例如下:
- 常量模式c(c是C#中的常量表達(dá)式)余耽,用于判斷輸入值是否等于c
- 類型模式T x(T是一個(gè)類型并且x是一個(gè)標(biāo)識符)缚柏,它判斷輸入值的類型是否為T,如果是碟贾,則將輸入值提取到x中
- Var模式var x(其中x是標(biāo)識符)币喧,它總是匹配,并簡單地將輸入的值放入與輸入x相同類型的新變量中袱耽。
這只是一個(gè)開始 - 模式是C#中一種新的語法杀餐,將來會將更多的元素添加到C#中。
在C#7.0中朱巨,模式增強(qiáng)了兩個(gè)現(xiàn)有的語言結(jié)構(gòu):
- is 表達(dá)式現(xiàn)在可以在右側(cè)有一個(gè)模式史翘,而不僅僅是一個(gè)類型
- case switch語句中的子句現(xiàn)在可以用模式匹配,而不僅僅是常量值
具有模式的is表達(dá)式
以下是使用is具有常量模式和類型模式的表達(dá)式的示例:
public void PrintStars(object o)
{
if (o is null) return; // 常量模式 "null"
if (!(o is int i)) return; // 類型模式 "int i"
WriteLine(new string('*', i));
}
模式變量冀续,模式引入的變量琼讽,類似于前面描述的out變量,因?yàn)樗鼈兛梢栽诒磉_(dá)式的中間聲明洪唐,并且可以在最近的作用范圍內(nèi)使用钻蹬。也像out變量一樣,模式變量是可變的凭需。我們經(jīng)常將out變量和模式變量共同稱為“表達(dá)式變量”脉让。
模式和try方法一起用經(jīng)常能達(dá)到神器的效果:
if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }
使用模式的Switch語句
有了模式以后,switch就更強(qiáng)大了:
- 可以對任意類型進(jìn)行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));
}
關(guān)于這個(gè)新擴(kuò)展的switch語句有幾點(diǎn)需要注意:
- case的順序現(xiàn)在很重要:就像catch一樣功炮,case不必是不相交的,匹配的第一個(gè)case會被選中术唬。此外薪伏,就像使用catch一樣,編譯器會幫你標(biāo)記永遠(yuǎn)無法訪問到的case粗仓。C#7.0之前嫁怀,你不用考慮case的順序设捐,所以這個(gè)特性不會改變已有代碼的行為。
-
default語句會在最后匹配:即使上面的
case null
是最后一個(gè)塘淑,也會在default之前匹配萝招。這是為了與現(xiàn)有的switch語法兼容,但是良好的做法通常會將default放在最后存捺。 - 最后的case null不會匹配不到:這是因?yàn)閕s表達(dá)式中的類型模式不匹配null槐沼,這可以確保不會因?yàn)橄瘸霈F(xiàn)的任何類型模式而意外地捕獲null。你必須明確地處理它們(或?qū)⑺鼈儽A舻絛efault里處理)捌治。
- case ...:標(biāo)簽引入的模式變量的作用范圍是對應(yīng)的case范圍內(nèi)岗钩。
Tuple 元組
你可能希望從方法返回多個(gè)值,在舊版C#中可能的解決方案有:
- out參數(shù):使用很笨重(即使有上面的改進(jìn))肖油,也不能使用異步方法兼吓。
- System.Tuple<...>返回類型:使用麻煩并需要分配元組對象。
- 為每個(gè)方法自定義返回類型:一個(gè)類型需要寫大量代碼森枪,只是為了臨時(shí)分組
- 通過dynamic返回的匿名類型:性能開銷大视搏,無靜態(tài)類型檢查。
這些解決方案都不是很好县袱,為了更好的實(shí)現(xiàn)這一目標(biāo)浑娜,C#7.0添加了元組類型和元組語法。
(string, string, string) LookupName(long id) // 返回元組類型
{
... // 獲取first显拳,middle棚愤,last,代碼略
return (first, middle, last); // 元組
}
該方法現(xiàn)在高效地返回三個(gè)字符串杂数,封裝在元組中宛畦。
該方法的調(diào)用者將接收一個(gè)元組,并可以單獨(dú)訪問元組中的元素:
var names = LookupName(id);
WriteLine($"found {names.Item1} {names.Item3}.");
Item1等等是元組元素的默認(rèn)名字揍移,總是可以使用次和。但它們不是很具描述性,所以你可以選擇添加更好的名字:
(string first, string middle, string last) LookupName(long id) // tuple elements have names
現(xiàn)在那伐,該元組的接收者可以使用描述性名稱:
var names = LookupName(id);
WriteLine($"found {names.first} {names.last}.");
您還可以直接在元組定義中指定元素名稱:
return (first: first, middle: middle, last: last); // named tuple elements in a literal
通常踏施,只要元組各個(gè)元素可賦值,元組就可以自由轉(zhuǎn)換為其他元組類型罕邀,無論元組元素的名稱是否一樣畅形。
元組是值類型,它們的元素是公共的诉探,可變的字段日熬。它們具有值相等性,這意味著如果兩個(gè)元組的所有元素對應(yīng)相等(并且具有相同的哈希碼)肾胯,則它們是相等的(并且具有相同的哈希碼)竖席。
這使得元組可用于多個(gè)返回值之外的許多其他情況耘纱。例如,如果你需要一個(gè)包含多個(gè)鍵的字典毕荐,可以使用元組作為key束析。如果你需要在每個(gè)位置具有多個(gè)值的列表,可以使用元組憎亚。
Deconstruction 解構(gòu)
使用元組的另一種方法是解構(gòu)后使用员寇。解構(gòu)聲明可以將元組(或其他值)分割成單獨(dú)的部分來接收對應(yīng)的變量。
(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
也可以解構(gòu)到已有的變量中丁恭,稱作解構(gòu)賦值:
(first, middle, last) = LookupName(id2); // deconstructing assignment
解構(gòu)不僅僅可以用于元組,任何類型都可以被解構(gòu)斋日,只要它有一個(gè)如下類型的解構(gòu)方法(實(shí)例方法或擴(kuò)展方法):
public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
out參數(shù)構(gòu)成解構(gòu)所產(chǎn)生的值牲览。
(為什么使用out參數(shù)而不是返回元組?這樣就可以為不同數(shù)量的值設(shè)置多個(gè)重載)恶守。
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);
以這種方式使構(gòu)造函數(shù)和解構(gòu)函數(shù)“對稱”是一種常見的模式第献。
就像輸出變量一樣,解構(gòu)中可以“丟棄”你不關(guān)心的部分:
(var myX, _) = GetPoint(); // I only care about myX
本地函數(shù)
有時(shí)輔助函數(shù)只在使用它的單個(gè)方法中有意義⊥酶郏現(xiàn)在可以在函數(shù)體內(nèi)將這些函數(shù)聲明為本地函數(shù):
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);
}
}
封閉范圍內(nèi)的參數(shù)和局部變量在局部函數(shù)內(nèi)部可用庸毫,就像它們在lambda表達(dá)式中一樣。
作為示例衫樊,作為迭代器實(shí)現(xiàn)的方法通常需要非迭代器包裝器方法飒赃,以便在調(diào)用時(shí)檢查參數(shù)。(迭代器本身在MoveNext調(diào)用之前不會開始運(yùn)行)科侈。本地函數(shù)非常適合這種情況:
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载佳,則其他成員可能意外地直接調(diào)用(無需參數(shù)檢查)。此外臀栈,這個(gè)私有方法Filter需要接收所有相同的參數(shù)蔫慧,而不是直接使用作用范圍內(nèi)的參數(shù)。
字面值改進(jìn)
C# 7.0 中可以使用下劃線 _ 作為數(shù)字字面值的分隔符:
var d = 123_456;
var x = 0xAB_CD_EF;
可以將它們放在數(shù)字之間的任何位置以提高可讀性权薯。它們對值沒有影響姑躲。
此外盟蚣,C#7.0引入了二進(jìn)制字面值黍析,因此你可以直接使用位模式,而不必使用心算來計(jì)算十六進(jìn)制表示屎开。
var b = 0b1010_1011_1100_1101_1110_1111;
Ref返回和臨時(shí)變量
就像在C#中通過ref引用(使用修飾符)傳遞內(nèi)容一樣橄仍,現(xiàn)在可以通過引用返回它們,并且還可以通過引用將它們存儲在局部變量中。
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
這對于將占位符傳遞到大數(shù)據(jù)結(jié)構(gòu)非常有用侮繁。例如,游戲經(jīng)常把數(shù)據(jù)保存在一個(gè)大的預(yù)分配結(jié)構(gòu)體數(shù)組中(以避免GC造成的性能影響)如孝。方法現(xiàn)在可以直接將引用返回給這樣的結(jié)構(gòu)體宪哩,調(diào)用者可以通過讀取和修改這個(gè)結(jié)構(gòu)體。
有一些限制來確保這是安全的:
- 只能返回“可以安全返回”的引用:一個(gè)是傳遞進(jìn)來的引用第晰,另一個(gè)是指向?qū)ο笾械淖侄蔚囊谩?/li>
- Ref本地變量初始化在特定的存儲位置锁孟,并且不能改變指向另一個(gè)。
異步返回類型的廣泛支持
到目前為止茁瘦,C#中的異步方法必須返回void品抽,Task或者Task<T>。C#7.0允許以特定方式定義其他類型甜熔,以便從異步方法返回它們圆恤。
例如,我們現(xiàn)在有一個(gè)ValueTask<T>結(jié)構(gòu)體類型腔稀。它的是為了防止在Task<T>等待時(shí)異步操作的結(jié)果已經(jīng)可用的情況下再分配對象盆昙。例如,對于涉及緩沖的許多異步場景焊虏,這可以大大減少分配數(shù)量并導(dǎo)致顯著的性能提升淡喜。
你還可以通過許多其他方式創(chuàng)建自定義“類任務(wù)”類型。創(chuàng)建它們并不是很簡單诵闭,但它們很可能會開始出現(xiàn)在框架和API中炼团,然后調(diào)用者就可以返回await它們了,就像現(xiàn)在的Task一樣疏尿。如果你想具體了解:https://github.com/dotnet/roslyn/blob/master/docs/features/task-types.md
更多表達(dá)式化的成員體
C# 6.0中引入了表達(dá)式化的方法瘟芝、屬性等,但是沒有允許在所有的成員中使用润歉。C#7.0將訪問器模狭,構(gòu)造函數(shù)和析構(gòu)函數(shù)加入了進(jìn)來:
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
}
}
拋出表達(dá)式
在表達(dá)式中間拋出異常很容易:只需調(diào)用執(zhí)行此操作的方法!但是在C#7.0中踩衩,可以直接在表達(dá)式某些地方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();
}
小結(jié)
本文講解了C#7的新特性中對Unity編程有影響的新特性嚼鹉,不過這些特性得等到Unity2018.3才可以用哦。
洪流學(xué)堂公眾號回復(fù)runtime
驱富,獲取本系列所有文章锚赤。
把今天的內(nèi)容分享給其他Unity開發(fā)者朋友,或許你能幫到他褐鸥。