隨著Visual Studio 2017,C# 正式來到7.0,這一版與6.0一樣,並沒有特別的主軸,只在語法上做改動,期望達到更直覺、清楚的程式碼設計風格。
如果沒看過上、中集,記得要看一下哦。這篇文章主軸介紹C# 7.0新增的擴展行為。
隨著Visual Studio 2017,C# 正式來到7.0,這一版與6.0一樣,並沒有特別的主軸,只在語法上做改動,期望達到更直覺、清楚的程式碼設計風格。C# 是一個很活躍的語言,出生以來就逐步吸收其他語言的特性,以這次的ValueTuple(函式回傳多值)來說,在其他語言其實出現一段時間了,例如Python、Go、Haskell、Swift等等,這個功能的加入讓C#更具完整性,更現代化。以往這種設計不被加入的原因通常是語言主導者較於保守的緣故,C#從出生就沒這個困擾,秉持著持續改進的宗旨,一直朝向著現代語言的目標邁進。雖說很多看法對於C# 7.0是認定為更多的語法糖,但某幾個改動就我看來,早就超越了語法糖的範圍了,程式碼確實看起來更舒服、更直覺些。
在C# 7.0中out被簡化,現在可以在傳遞時再宣告,這可以減少程式碼的行數,增加可讀性。
static void Sum(int x, int y, out int result)
{
result = x + y;
}
static void Main(string[] args)
{
Sum(10, 5, out int result);
Console.WriteLine(result);
Console.ReadLine();
}
如所見,out可以在傳遞時被宣告即可,不須事先宣告。展開後不意外,編譯器會自動幫你宣告。
有趣的是,ref並未得到同樣的對待,所以下面這樣寫是錯的。
static void Sum(int x, int y, ref int result)
{
result = x + y;
}
static void Main(string[] args)
{
Sum(10, 5, ref int result);
Console.WriteLine(result);
Console.ReadLine();
}
原因是在使用ref的情境下,變數本來就該有原值,所以不需要做到推遲宣告。
另外,你也可以使用var來取代實體型別宣告,編譯器會自行推斷。
class Program
{
static void Sum(int x, int y, out int result)
{
result = x + y;
}
static void Main(string[] args)
{
Sum(10, 5, out var result);
Console.WriteLine(result);
Console.ReadLine();
}
}
在C# 7.0中,ref可以被用在回傳值,例如下面這種寫法。
static ref int Find(int[] data, int value)
{
for (int i = 0; i < data.Length; i++)
{
if (data[i] == value)
return ref data[i];
}
throw new Exception("not found");
}
static void Main(string[] args)
{
int[] data = { 1, 3, 4, 5, 7 };
ref int v = ref Find(data, 5);
Console.WriteLine(v);
v = 10;
Console.WriteLine(data[3]);
Console.ReadLine();
}
前面的ref int v 語意稱為ref local,用來接收後方的ref Find,後方的語意稱為ref return,此例結果如下。
簡單的說,就是傳址的意思,端看接收者的意願,如果用ref int x = ref func()的話,那麼就可以得到一個傳址的變數,當修改這個變數時也連帶地修改原來的值。呼叫者也可以忽略,取得傳值的回傳,如下所示。
static ref int Find(int[] data, int value)
{
for (int i = 0; i < data.Length; i++)
{
if (data[i] == value)
return ref data[i];
}
throw new Exception("not found");
}
static void Main(string[] args)
{
int[] data = { 1, 3, 4, 5, 7 };
int v = Find(data, 5);
Console.WriteLine(v);
v = 10;
Console.WriteLine(data[3]);
Console.ReadLine();
}
當使用ref return時,會被展開成下面這樣。
很有趣的行為,你可以看到ref int那段的宣告,事實上這是無法編譯的程式碼,切換成IL就可以理解了。
IL本身就支援這樣的用法了,另外,ref return的函式會被標上unsafe。
這意味著這個函式是unsafe的,因此才能使用&這個unsafe才有的指令,運用取址方式來處理。特別注意這個手法在許多語言都被認定是evil的,例如C++,所以慎用。
這個擴展動作也忽略了unsafe限制,以往unsafe區段用來操作指標,這通常要特別小心處理,否則會因為記憶體指標操作錯誤而產生無法預期的結果,另外早期的CLR對於使用unsafe區段的程式會有限制,包含unsafe的Assembly也不能執行於部分信任的環境中,不過這個部分信任的機制已經被標示為過時,所以不需考慮這部分了,總結就是這是編譯器擴展的,值得相信。
下面是一個較貼近實務上使用ref return的例子。
public struct Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class TestClass
{
private Person[] _data = {
new Person() { Name = "code6421", Age = 18 },
new Person() { Name = "mary", Age = 18 }
};
public Person this [int index]
{
get
{
return _data[index];
}
}
public ref Person Find(string name)
{
for (int i = 0; i < _data.Length; i++)
{
if (_data[i].Name == name)
return ref _data[i];
}
throw new Exception("not found");
}
}
class Program
{
static void Main(string[] args)
{
var tc = new TestClass();
ref Person v = ref tc.Find("code6421");
Console.WriteLine($"{v.Name}, {v.Age}");
v.Age = 10;
Console.WriteLine(tc[0].Age);
Console.ReadLine();
}
}
整個脈絡看起來更直覺。
使用ref return&ref local要特別小心,尤其對象是物件而不是結構時。
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class TestClass
{
private Person[] _data = {
new Person() { Name = "code6421", Age = 18 },
new Person() { Name = "mary", Age = 18 }
};
public Person this[int index]
{
get
{
return _data[index];
}
}
public ref Person Find(string name)
{
for (int i = 0; i < _data.Length; i++)
{
if (_data[i].Name == name)
return ref _data[i];
}
throw new Exception("not found");
}
public void ShowIt()
{
foreach (var item in _data)
Console.WriteLine(item.Name);
}
}
class Program
{
static void Main(string[] args)
{
var tc = new TestClass();
ref Person v = ref tc.Find("code6421");
v = null;
tc.ShowIt();
Console.ReadLine();
}
}
上例會因為ref的物件被設為null,由於是以傳址方式傳遞,連帶著原始物件也被設為null 而引發例外。
另一個例子就留給各位回味了。
public struct Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class TestClass
{
private Person[] _data = {
new Person() { Name = "code6421", Age = 18 },
new Person() { Name = "mary", Age = 18 }
};
public ref Person this [int index]
{
get
{
return ref _data[index];
}
}
public ref Person Find(string name)
{
for (int i = 0; i < _data.Length; i++)
{
if (_data[i].Name == name)
return ref _data[i];
}
throw new Exception("not found");
}
}
class Program
{
static void Main(string[] args)
{
var tc = new TestClass();
ref Person v = ref tc.Find("code6421");
Console.WriteLine($"{v.Name}, {v.Age}");
v.Age = 10;
Console.WriteLine(tc[0].Age);
tc[0].Age = 16;
Console.WriteLine(v.Age);
Console.ReadLine();
}
}
多數情況下,ref return預期的是與ref local合用,ref local另一種用法是在同一區段中使用ref來取得變數的址,達到&的功能,如下例。
static void Main(string[] args)
{
int s = 12;
ref var v = ref s;
v = 15;
Console.WriteLine(v);
}
這也是透過unsafe區段的&指令達成的,這也是要特別小心慎用的功能之一。
這大概是C# 7.0最令我驚喜的功能,在Delphi時代我就很常使用local function,多半在該function只在該function用到,沒必要切出來成為成員函式 時,當然,另一派說法是沒這個必要性。
static void Main(string[] args)
{
Console.WriteLine(Sum(5, 10));
Console.ReadLine();
int Sum(int x, int y)
{
return x + y;
}
}
不意外,就是被提取成靜態成員函式而已,其實我還蠻意外這樣的結果,因為在7.0之前,可以用Func<> 、Action<>或是delegate + Lambda expression來達到同樣的效果,而且不會弄髒原來的程式碼。
static void Main(string[] args)
{
var Sum = new Func<int, int, int>((x, y) =>
{
return x + y;
});
Console.WriteLine(Sum(5, 10));
Console.ReadLine();
}
唯一能想到的目的就是效能,因為透過Func<>、Action<>、delegate的途徑是間接呼叫,但使用local function則變成了呼叫靜態函式,效能自然比較高,另一個考量是編譯時期檢查。
當然,variable catch機制也保留下來了。
static void Main(string[] args)
{
var p = new List<int>() { 1, 2, 3, 4, 5 };
Console.WriteLine(Sum());
Console.ReadLine();
int Sum()
{
return p.Sum();
}
}
只是這個手法值得深思,因為Lambda expression支援variable catch機制的主因通常是這個函式區段有被移動到其它區段執行的可能性(例如在Thread中),但local function被移動的機率不高。硬要移動也可以,但這只是徒增複雜度而已,除非整體結構已經到達需要取得local function的效能但又想得到Func<>、Action<>、delegate這些所帶來的移動性時。
static void CallDelegate(Func<int> func)
{
Console.WriteLine(func());
}
static void Main(string[] args)
{
var p = new List<int>() { 1, 2, 3, 4, 5 };
CallDelegate(Sum);
Console.ReadLine();
int Sum()
{
return p.Sum();
}
}
在C# 7.0中,解除了函式只能有單一回傳值的限制,這其實有點歷史了,以往函式總是被限定為只能回傳單值,如果需要多值,就得使用out參數,或是使用Tuple類別,但在很多語言例如Python、Go、Swift、Haskell都已經解除了這個限制,一個號稱現代的語言沒理由還固守此規則。這個手法可以追朔到C# 4.0的Anonymous Type。
static void Main(string[] args)
{
var s = new { x = 0, y = 20 };
Console.Write(s.x);
}
這時只限於local,你不能傳遞s,也不能作為函式的回傳值,在C# 7.0這些限制解開了。
需要特別注意的是這個機制運用到一個新的型別: System.ValueTuple,Microsoft決定暫時不將它整合到.NET Framework中,而是以NuGet的方式發佈,要使用這個功能就必須透過NuGet 來新增System.ValueTuple套件至專案。
完成後就能編譯下面的程式碼。
static (int x, int y) GetValue()
{
int s1 = 0, s2 = 0;
return (s1, s2);
}
static void Main(string[] args)
{
var ret = GetValue();
Console.WriteLine(ret.x);
}
展開後可看到ValueTuple的身影。
Item1、Item2回來了,如果想要更明確的宣告也可以,兩者是能互轉的。
static ValueTuple<int,int> GetValue()
{
int s1 = 0, s2 = 0;
return (s1, s2);
}
static void Main(string[] args)
{
ValueTuple<int,int> c = GetValue();
Console.WriteLine(c.Item1);
Console.ReadLine();
}
ValueTuple如同Tuple一樣,多個值也是可以的。
static (int x, int y, int z, int a) GetValue()
{
int s1 = 0, s2 = 0, s3 = 0, s4 = 0;
return (s1, s2, s3, s4);
}
static void Main(string[] args)
{
var ret = GetValue();
Console.WriteLine(ret.x);
}
目前ValueTuple型別參數的極限是7個,當到達極限時,編譯器就會進行再展開。
static (int x, int y, int z, int a, int a1, int a2 ,int a3 ,int a4, int a5) GetValue()
{
int s1 = 0, s2 = 0, s3 = 0, s4 = 0,
a1 = 0, a2 =0, a3 = 0, a4 =0, a5 = 0;
return (s1, s2, s3, s4, a1, a2, a3, a4, a5);
}
如果必要,你也可以直接提取內容(事實上編譯器仍然以ValueTuple擴展後賦值,只是看起來更直覺些),如下所示。
static (int x, int y) GetValue()
{
int s1 = 0, s2 = 0;
return (s1, s2);
}
static void Main(string[] args)
{
var (x, y) = GetValue();
Console.WriteLine(x);
Console.ReadLine();
}
ValueTuple本身是個Value Type,這與Tuple是個類別不同,這意味著任何動作都是傳值的,以下面的例子來說,兩者是完全獨立的個體,這點與Anonymous Type不同。
static void Main(string[] args)
{
var ret = GetValue();
var ret2 = ret;
ret2.x = 100;
Console.WriteLine(ret.x);
Console.ReadLine();
}
ValueTuple也實作了ICompatable介面,可以運用Equals函式來進行值比對。
static void Main(string[] args)
{
var ret = GetValue();
var ret2 = GetValue();
if (ret.Equals(ret2))
Console.WriteLine("same");
Console.WriteLine(ret.x);
Console.ReadLine();
}
不過他並未實做運算子覆載,所以不能用==方式比對,以下寫法無法通過編譯。
static void Main(string[] args)
{
var ret = GetValue();
var ret2 = GetValue();
if (ret == ret2)
Console.WriteLine("same");
Console.WriteLine(ret.x);
Console.ReadLine();
}
ValueTuple以NuGet 發佈的主要目的應該是想讓其他版本都能運用到這個型別,而不是只有C# 7.0可以,雖然在其他版本只能用ValueTuple而無法享受C# 7.0編譯器的簡化,但至少可解除函式只能回傳一個值的限制(當然,用Tuple也可以,但別忘了Tuple是個類別,產生的是物件,這也是C# 7.0沒有使用Tuple來實作多值回傳的原因,因為Tuple以物件參考方式運作,ValueTuple是結構,更適合運用於函式回傳值情境)。
ValueTuple也可以用在程式本文,例如下面的寫法。
static void Main(string[] args)
{
var letters = ("a", "b");
var intvalues = (15, 20);
var nameValues = (x : 15, y : 20);
}
如果需要,也可以進行更名。
static void Main(string[] args)
{
var p = (a1: "a1", a2: "a2");
(string x1, string x2) value = p;
Console.WriteLine(value.x1);
}
也可以應用在參數傳遞上。
static void TestPass((string x, string y) v)
{
Console.WriteLine(v.x);
}
static void Main(string[] args)
{
(string x, string y) value = ("a", "b");
TestPass(value);
}
甚至簡化。
static void TestPass((string x, string y) v)
{
Console.WriteLine(v.x);
}
static void Main(string[] args)
{
TestPass(("a1", "a2"));
}
當然,ref、out修飾子也可以用在ValueTuple身上。
static void TestPass(ref (string x, string y) v)
{
v.x = "c";
}
static void Main(string[] args)
{
var v = (a1: "a", a2: "b");
TestPass(ref v);
Console.WriteLine(v.a1);
}
static void TestPass(out (string x, string y) v)
{
v.x = "c";
v.y = "d";
}
static void Main(string[] args)
{
(string x, string y) v;
TestPass(out v);
Console.WriteLine(v.x);
Console.ReadLine();
}
或是ref 回傳值。
static ref (string x, string y) TestPass(ref (string x1, string y1)[] s)
{
return ref s[1];
}
static void Main(string[] args)
{
(string a1, string a2)[] v = {
(a1: "a1", a2: "a2"),
(a1: "a2", a2: "a3"),
(a1: "a4", a2: "a5"),
};
ref var s = ref TestPass(ref v);
Console.WriteLine(s.x);
Console.ReadLine();
}
特別注意ValueTuple是以內含變數數量來判斷,這意味著即使名稱不同,但同型別同數量,就會被視為可轉換或是賦值,而這些動作都是以位置(index)為依歸,例如下面的例子,結果是20。
static void Main(string[] args)
{
var s = (15, 20, b: 25);
var p = (a: 10, b: 20, s:30);
p = s;
Console.WriteLine(p.b);
Console.ReadLine();
}
static void TestPass((string x, string y) v)
{
Console.WriteLine(v.x);
}
static (string x, string y) TestPass2()
{
return (x: "a", y: "b");
}
static void Main(string[] args)
{
TestPass(TestPass2());
}
class Program
{
static void Main(string[] args)
{
var (x, y, z) = (1, 2, 3);
Console.WriteLine(z);
Console.ReadLine();
}
}
static void Main(string[] args)
{
var (s, x, y, z) = (1L, 2M, 3F, 4);
Console.WriteLine(z);
Console.ReadLine();
}
上例會得到這個結果。
或許CLR可以最佳化,但在這之前的解譯只是徒增效能負擔而已。
就結論上來說,ValueTuple優於Tuple,不管是在記憶體布局還是應用上都比Tuple來的方便,Tuple當初設計時過於傾向物件概念,為了避免以物件參考方式運作而造成的困擾,所以設計成一旦建立就不能修改內值,如下例(這是不能編譯的哦)。
static (int x, int y) GetValue()
{
int s1 = 3, s2 = 0;
return (s1, s2);
}
static void Main(string[] args)
{
var t1 = new Tuple<int, int>(15, 20);
var t2 = t1;
t2.Item1 = 33; //it's readonly
}
想像一下如果Tuple的Item1、2 屬性沒有定義成get only所帶來的問題,ValueTuple則沒有這個困擾,所以也不需限制。
ValueTuple也提供了與Tuple轉換的Extension Method,可以將Tuple直接轉成ValueTuple,如下例。
static void Main(string[] args)
{
var t1 = Tuple.Create(15, 20);
var (x, y) = t1.ToValueTuple();
Console.WriteLine(x);
}
C# 7.0 也可做到隱式的轉換。
static void Main(string[] args)
{
var t1 = Tuple.Create(15, 20);
var (x, y) = t1;
Console.WriteLine(x);
}
public struct MyData
{
public string Name { get; set; }
public int Age { get; set; }
public void Deconstruct(out string name, out int age)
{
name = Name;
age = Age;
}
}
class Program
{
static void Main(string[] args)
{
MyData p = new MyData() { Name = "c", Age = 12 };
var (n, a) = p;
Console.WriteLine(n);
Console.ReadLine();
}
}
會被擴展成下面這樣。
搭配運算子覆載,可以達到互轉的目的。
public struct MyData
{
public string Name { get; set; }
public int Age { get; set; }
public void Deconstruct(out string name, out int age)
{
name = Name;
age = Age;
}
public static implicit operator MyData((string a, int b) v)
{
return new MyData() { Name = v.a, Age = v.b };
}
}
class Program
{
static void Main(string[] args)
{
MyData p = new MyData() { Name = "c", Age = 12 };
var (n, a) = p;
MyData p1 = (a: "d", b: 15);
var s1 = (p1: "s", p2: 3);
Console.WriteLine(p1.Name);
MyData p2 = s1;
Console.WriteLine(p2.Name);
Console.ReadLine();
}
}
在C# 6.0中增加了單行敘述,可套用在get、set及成員的內容,在7.0應用範圍更加廣泛,包含著建構子、解構子,get、set的函式內容。
class Test1
{
private string _s;
Test1(string data) => _s = data;
}
只是展開而已。
get、set也可以用。
class Test1
{
private string _v;
public string v
{
get => _v;
set => _v = value;
}
}
在C# 7.0之前,throw不能與其他語法共用,例如不能在三元運算式(….?...:),或是Null運算式(??)中使用,在7.0這些限制都解開了。
static void Main(string[] args)
{
string s = null;
var s1 = s ?? throw new Exception("s is null");
}
不意外,只是單純展開而已。
在三元運算式中也可以使用。
static void Main(string[] args)
{
string s = null;
var s1 = s != null ? s : throw new Exception("s is null");
}
或者是宣告式。
class Program
{
static string LoadData()
{
return "ssss";
}
private string Data = LoadData() ?? throw new Exception("data is null");
static void Main(string[] args)
{
}
}
總結就是限制都解開了,程式碼看來更簡潔且易讀不是?
C# 7.0中的Pattern matching功能可以大量簡化判斷物件型別所產生的程式碼量,這在實作Dispatcher Pattern時特別有用,如以下例子所示。
public class TestObject
{
public string Show() => "obj1";
}
public class TestObject2
{
public string Show2() => "obj2";
}
public class Program
{
static void ShowIt(List<object> objs)
{
foreach (var item in objs)
{
switch (item)
{
case TestObject o1:
Console.WriteLine(o1.Show());
break;
case TestObject2 o2:
Console.WriteLine(o2.Show2());
break;
case int v:
Console.WriteLine(v);
break;
case List<object> moreObj when moreObj.Any():
ShowIt(moreObj);
break;
case List<object> moreObj:
Console.WriteLine("inner is no value");
break;
case null:
break;
}
}
}
static void Main(string[] args)
{
List<object> data = new List<object>() {
new TestObject(),
new TestObject2(),
12,
new List<object>() { new TestObject(), new TestObject() } };
ShowIt(data);
Console.ReadLine();
}
}
展開後如下。
就是把switch整個轉換為一串if 型別判斷式。除了switch之外,也可以用在一般的判斷式,如下所示。
static void ShowIt(List<object> objs)
{
if (objs.Any() && objs[0] is int v)
{
Console.WriteLine($"root object is int {v}");
}
}
展開後如下。
你大概會好奇這段程式碼,感覺上是完全不同結果,因為num並未被賦值,這是Reflector的問題,轉成IL後就能看到賦值的部分。
或者也可以用另一個產品,dotPeak可以更完整的展現展開的部分。
C# 7.0之前,async只能有一種回傳型別,就是Task型別,但Task型別是類別,意味著其是以物件方式傳遞,在C# 7.0增加對ValueTask<TResult>的支援,與ValueTuple一樣,這尚未整合到.NET Framework中,必須透過NuGet安裝。
用法如下。
private static int? _cacheContent;
public static async ValueTask<int> Test()
{
if (_cacheContent == null)
_cacheContent = (await new HttpClient().GetStringAsync(
"http://www.google.com")).Length;
return _cacheContent.Value;
}
public static async void CallIt()
{
var ret = await Test();
Console.WriteLine(ret);
}
static void Main(string[] args)
{
CallIt();
Console.ReadLine();
}
展開後。
簡略的說,當使用ValueTask<int>做為回傳值時,此函式一開始是透過HttpClient取得Task<string>,由於Task<TResult>是個類別,此時會發生物件初始化的動作,接著將值交給ValueTask<int>,當第二次呼叫時,由於已經快取了回傳值,此時直接產生ValueTask<int>,因為它是一個結構,所以可以避開物件初始化動作,也不需要進行GC(Garbage Collection)達到提升效能及降低記憶體碎片的目的。
特別注意的是ValueTask並不具備Task的完整功能,就如同其出生的目的一樣,僅設計來避開物件初始化及GC的負擔,因此下面的程式碼在Task是正確的,但換成ValueTask,ContinueWith就消失了。
private static int? _cacheContent;
public static async ValueTask<int> Test()
{
if (_cacheContent == null)
_cacheContent = (await new HttpClient().GetStringAsync(
"http://www.google.com")).Length;
return _cacheContent.Value;
}
public static async void CallIt()
{
//wrong.
var ret = Test().ContinueWith((t) =>
{
});
Console.WriteLine(ret);
}
ValueTask設計的目的是用於當值已知(不須再進行async動作)或上次結果已快取時,避開產生Task物件的初始化動作,所以直接把Task轉成ValueTask其實是沒有意義的,反而會增加效能負擔,如下所示。
public static async ValueTask<int> Test()
{
return (await new HttpClient().GetStringAsync(
"http://www.google.com")).Length;
}
當然,ValueTask所帶來的效益很難判斷,畢竟這牽扯到了物件初始化所需要的記憶體及動作,還有GC的部分,但就理論上而言是比較好的,因此在沒有需要後續動作時(例如ContinueWith),而且有快取或是同步動作存在時,使用ValueTask是比較好的選擇。
如果仔細看整個async的過程,會發現除了Task之外,其它內部如TaskAwaiter都是結構,Task會被設計成類別的主要原因應該是想讓他擷取類別可多型、函式可覆載、還有以物件參考傳遞的優點,但同時也付出了需要物件初始化及GC處理的代價。那為何不直接將Task改寫呢?這其實很難,一來會造成break changes,二來整個體系要全部改寫。
好吧,我不是很想提這個東西………………….
public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;
簡單的說就是讓數字更容易一眼看懂(你是來插花的吧…)。
就整個脈絡來看,6.0與7.0都在增加可讀性、降低程式碼量的角度上處理,不過看來7.0則更進一步改進效能及記憶體佈局,在特定情境運用結構來取代物件避開物件初始化及GC負擔,並且逐步引進其他語言的特色,持續朝現代化語言這個目標前進。