談C# 編譯器編譯前的程式碼擴展行為 (2017年續 下)

隨著Visual Studio 2017,C# 正式來到7.0,這一版與6.0一樣,並沒有特別的主軸,只在語法上做改動,期望達到更直覺、清楚的程式碼設計風格。

導讀

  如果沒看過中集,記得要看一下哦。這篇文章主軸介紹C# 7.0新增的擴展行為。

C# 7.0

  隨著Visual Studio 2017,C# 正式來到7.0,這一版與6.0一樣,並沒有特別的主軸,只在語法上做改動,期望達到更直覺、清楚的程式碼設計風格。C# 是一個很活躍的語言,出生以來就逐步吸收其他語言的特性,以這次的ValueTuple(函式回傳多值)來說,在其他語言其實出現一段時間了,例如Python、Go、Haskell、Swift等等,這個功能的加入讓C#更具完整性,更現代化。以往這種設計不被加入的原因通常是語言主導者較於保守的緣故,C#從出生就沒這個困擾,秉持著持續改進的宗旨,一直朝向著現代語言的目標邁進。雖說很多看法對於C# 7.0是認定為更多的語法糖,但某幾個改動就我看來,早就超越了語法糖的範圍了,程式碼確實看起來更舒服、更直覺些。

 

out的簡化

  在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();
        }

}

 

ref return & ref local

  在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 local

  多數情況下,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區段的&指令達成的,這也是要特別小心慎用的功能之一。

 

Local functions

  這大概是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();
       }
}

 

Tuple values return&pass

  在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);
}
(你是Javascript吧?)

如果需要,也可以進行更名。

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);
}

 

Deconstruct
  這個機制是一種契約式的設計,只要結構或是類別提供了Deconstruct函式,那麼就可以與ValueTuple互相轉換,以下面的例子來看。
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();
        }

}

 

More expression-bodied members

  在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;
        }

}

 

Throw Exceptions

  在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)
        {
        }

}

總結就是限制都解開了,程式碼看來更簡潔且易讀不是?

 

Pattern matching

  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可以更完整的展現展開的部分。

 

ValueTask

  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,二來整個體系要全部改寫。

 

Numeric literal syntax improvements

  好吧,我不是很想提這個東西………………….

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負擔,並且逐步引進其他語言的特色,持續朝現代化語言這個目標前進。