C# 7.0 之比爾猜測

  • 4233
  • 0
  • 2017-04-17

C# 7.0 新增了許多的新特性支援,這讓我靈光一閃,於是有了這個『比爾猜測』。

隨著 Visual Studio 2017 的出世,C# 7.0 堂堂登場,這次的改版出現了許多新的語言特性,關於這些新特性的描述與使用已經有許多的文章和研討會闡述過,所以對於那些內容不會有太多論述;這篇文章主要是猜測這些新特性之所以會出現的原因。

這個猜測的緣由從出現新的結構型別與支援結構的操作而來。

先來看看此次出現的兩個新的型別 -- ValueTupleValueTask,這兩個新的型別是結構型別 (Structure), 從 C# 2.0 到 C# 6.0 這段期間,記憶中除了在 2.0 時代 出現了個 Nullable<T> 結構以外,幾乎沒有新增過甚麼常用的結構型別 (註:很恰巧 C# 2.0 和 .Net Framework 2.0 是一起出現,其實 Nullable<T> 應該是算在 Framework 身上,但是如果當時語言沒有支援泛型的話,Nullable<T> 也出不來,因為現在要講述的歷史流程以 C# 的演進史為標的,所以就當他們兩個算是難兄難弟),這是第一個奇妙的地方。

Descontruct 根本上就是為了搭配 ValueTuple 而產生的語法,讓我們可以快速地將某個型別內部的屬性或欄位直接使用類似指派運算的方式將他們傳遞給另一個 ValueTuple 型別的變數。

out 引數的新宣告方式ref local/return 這兩個玩意和結構也有莫大的關係,對於參考型別(Reference type)的變數而言,by reference 的意義並不是很大,但對於實值型別(Value type)的變數可就大大的不一樣。以前若是要存取變數的指標,你得要標示為 unsafe mode 才辦的到,像是以下這樣的程式:
 

 static void Main(string[] args)
 {
     int number = 100;
     unsafe
     {
         int* p = &number;
         Console.WriteLine(*p);
         *p = 999;
         Console.WriteLine(number);
     }
     Console.ReadLine();
 }

現在則讓我們可以簡單的方式達到這樣的效果:
 

static void Main(string[] args)
{            
    int number = 100;
    ref int p = ref number;
    Console.WriteLine(p);
    p = 999;
    Console.WriteLine(number);
   Console.ReadLine();
}

我們用 OzCode 來看一下第二個程式碼中的變數內容變化:

最後來看看 local function 和結構又有甚麼關係,我們用以下的例子來說明:
 

static void Main(string[] args)
{
    List<string> list1 = new List<string>() { "1", "2", "3", "4", "5" };
    var list = list1;
    Display();
    List<string> list2 = new List<string>() { "A", "B", "C", "D", "E" };
    list = list2;
    Display();
    Console.ReadLine();
    void Display()
    {
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
    }
}

Display 這個 method 就是 Main method 的 local function,從上述程式碼可以看得出來,在 Display 中所使用的 list 變數是宣告在 Main method 區段中,那 C# 7.0 編譯器是如何處理的呢?這個程式碼編譯之後會產生一些怪東西出來:

[CompilerGenerated]
private struct <>c__DisplayClass0_0
{ 
   public List<string> list;
}
[CompilerGenerated] 
internal static void <Main>g__Display0_0 (ref <>c__DisplayClass0_0 class_Ref1) 
{     foreach (string str in class_Ref1.list)     
       {               
         Console.WriteLine(str);      
       } 
}

很有趣,它產生了一個結構,這個結構是用來塞剛剛那個 list 變數,過往由編譯器產生的型別通常會是類別而不是結構,這點和以前的 C# 設計方式不一樣;然後讓 Display method 獨立出來,更重要的是參數的宣告是 ref ,也就是 by reference 呼叫。

在這篇文章的一開頭曾經闡述過在 C# 2.0 到 C# 6.0 這段期間的改版,對於結構部分變動是很少的,但是這一次卻有這麼多和結構有關的變動,這不禁讓我深思其用意為何。

讓我們回到參考型別和實值型別的基本差異上,如果你  C# 用得夠久,應該知道參考型別的物件會產生在 Heap 中,在區域變數的情境下,實值型別的物件基本上就是塞在變數裡,而區域變數本身是存在於 Thread Stack 的。這就造成了記憶體管理上的差別,生活在 Thread Stack  裡的區域變數它的生命週期是跟著所在的方法一同出生與消滅,但存在於 Heap 中的參考型別物件卻必須等待垃圾收集機制 (garbage collection,以下簡稱 GC) 來處理。GC 很棒,讓我們免去處理記憶體回收的麻煩,但有一個要命的問題,GC 是一個霸道的執行緒,一旦 GC 執行,同一個 Process 中所有其他的執行緒都會暫停,而且我們無法控制 GC 會在甚麼時候耍這麼一下,也無法限制它執行的時間長短。

可是,我們似乎從來沒有被 GC 這樣的行為干擾過的經驗,因為大部分的 C# 程式設計師都是寫商業應用程式,對於這類的程式,GC 就算頓個 0.2 秒,恐怕也沒人會發現;但是對於寫 Game 或是即時影像的開發者來說,這件事可能真會是個大問題。我們想像一個狀況,若是有這麼一個 Game,它的標準是一秒 30 Frames,因為 GC 的干擾,它偶爾會是 25 Frames、偶爾 18 Frames、有時又恢復正常成為 30 Frames,那使用體驗應該不會有多愉快,如果能降低 GC 的運作時間與頻率,即使 Famres 數量不能完全一致,起碼受到的影響會比較少。這也就是說,在這種對 video frames 很敏感的程式,用結構會比用類別來得有優勢,然而結構又有一個麻煩是當它使用指派運算子的時候,它的行為其實是複製整個物件,也因為這樣,ref parameter、out parameter、local ref 與 ref return 就顯得相當重要,這樣才能避免該結構內容會被一直複製而讓 thread stack 爆掉。

經過以上不太科學的辯證後,在此提出比爾猜測這些改變要因應的應該是 Unity3D、Hololens 以及 IoT 之類的,因為 GC 對一般的商業軟體影響並沒有很顯著,而真正會被 GC 搞到的通常都是 Game、影像或是那種記憶體太少的玩意兒。』當然這是個人靠著對 C# 7.0 新特性的分析而產生的猜測,目前沒有任何小道消息顯示或暗示這個猜測的任何證據。不過一年內應該就可以知道這個猜測的正確性有多少,也很可能是零,總之我就是這麼猜了。