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

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

 

C# 6.0

  相較於前幾版,這次的改動不大,主軸有兩個,一是補足5.0未完成的async/await無法使用於catch區段和Exception Filter部分,另一個是簡化程式碼,讓可讀性增加。

 

Initializers for auto-properties

  6.0之後,針對自動完成的屬性(指由編譯器幫你產生get與set函式及對應的變數),你可以這樣寫。

public class Customer
{
     public string First { get; set; } = "Jeffray";
     public string Last { get; set; } = "Huang";
}

簡單的說就是指定那個對應函式的值了。有趣的是編譯器如何擴展,是呼叫產生的set函式來賦值,還是直接設定那個變數的值?有差嗎?當然有,一個有函式呼叫一個沒有(雖然最佳化後可能是一樣的)。

編譯器還是很聰明的。

 

Getter-only auto-properties

  在6.0以前,如果想讓自動屬性對外只公開get機制,那麼會這樣寫。

public class Customer
{
   public string First { get; private set; }
    public string Last { get; private set;  }

    public Customer()
    {
        First = "Test";
    }
}

在6.0可以簡化成下面這樣。

public class Customer
{
     public string First { get; } = "Jane";
     public string Last { get; } = "Doe";
}

除了簡化外,還有差別嗎?有的,編譯器產生的變數變成readonly了。

另外,set函式完全消失了,在6.0前set函式一定會有,只是設成private而已,下面是5.0的。

在5.0這樣寫會出現編譯錯誤。

6.0會變這樣。

 

Expression bodies on method-like members

  這個功能針對單行內文,簡化了函式的敘述。

public class Customer
{
       public string First { get; } = "Jane";
       public string Last { get; } = "Doe";
       public void Print() => Console.WriteLine(First + " " + Last);
}

不難猜出擴展的內容。

也可以用在屬性。

public class Customer
{
     public string First { get; } = "Jane";
     public string Last { get; } = "Doe";
     public string FullName => First + " " + Last;
     public void Print() => Console.WriteLine(First + " " + Last);
}

 

Using static

  透過這個功能,6.­0可以省略類別名稱,直接呼叫靜態函式。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using static System.Console;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteLine("Hello World");
            Read();
        }
    }
}

這種展開算是簡單了。

using static不涵蓋Extension Method,例如下面的例子。

Where是Extension Method,雖然他是static,但因為是Extension Method,所以被排除於using static之外,要照原來的寫法。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using static System.Linq.Enumerable;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            int[] data = { 5, 6, 7, 8 };
            data.Where(a => a > 5);
            Console.Read();
        }
    }
}

 

Null-conditional operators

 很早C# 就有??運算子,例如下面這樣。

int y = x ?? -1;

意思是當x是null的時候,就使用-1這個值,如果不是就使用x的值,這可以擴展應用成下面這樣。

string s = null;
Console.WriteLine(s ?? "nothing");

意思是當s是null時,印出nothing,當s非null時,印出內容。從語言角度來看,??運算子最後的型別是以左方的型別為準,所以下面的寫法是錯誤的。

string s = null;
Console.WriteLine(s ?? 12);

因為??運算子決議出來的型別是string,??右方是int,所以兩個型別不符。

C# 6.0新增了?運算子,運用範圍更加廣泛,印象中最早有這個運算子的語言就是Apple Swift(當然,範圍是我所知道的語言中)。

static void Main(string[] args)
{
      string s = null;
      string d = s?.Substring(1, 2);
      Console.WriteLine(d ?? "nothing");
      Console.ReadLine();
}

結果是nothing,由語言角度上來說,當?運算子左方為null時,右方就不會執行,而以最右方決議出來的型別預設值,以string這種參考型態物件來說是null,如果是int這種value型態物件則是會以Nullable封裝-1,例如下面這樣是錯誤的。

string s = null;
int d = s?.IndexOf("st");

要改成。

string s = null;
int? d = s?.IndexOf("st");

以最初的例子,展開後是這樣。

合併?跟??可以簡化最初的例子。

static void Main(string[] args)
{
     string s = null;
     Console.WriteLine(s?.Substring(1, 2) ?? "nothing");
     Console.ReadLine();
}

如果連續使用?,就會變成這樣。

int? index = s?.Substring(1, 2)?.IndexOf('c');

只要記得使用最右方的?的右方型別為主就是了。在delegate類型上要注意,這樣寫是錯的。

public class Customer
{
     public delegate void NameChangedDelegate(string value);
     private string _name;

     public event NameChangedDelegate NameChanged;
     public string Name
     {
          get
          {
             return _name;
          }
          set
          {
             _name = value;
             NameChanged ? (_name);
          }
     }
}

要改成這樣。

NameChanged?.Invoke(_name);

?這個運算子很方便,但要記住一個準則,一行敘述不管有幾個?,只要有一個是null,那麼回傳值就是null,型別則以最右方?運算子右方的型別為準。

 

 

String interpolation

  簡單的說,就是string.Format的超級簡化版。

static void Main(string[] args)
{
     int r = 15 + 20;
     Console.WriteLine($"final value is {r}");
     Console.Read();
}

展開後。

沒啥特別的,不過15 + 20被變成0x23囉(我是編譯器,我會最佳化哦)。

如果真的要印出{、}符號,用兩次就對了。

Console.WriteLine($"final value is {{r}} {r}");

 

 

nameof expressions

  簡單的說就是取得變數的真正名稱。

public static int Sum(int x, int y)
{
    Console.WriteLine(nameof(x));
    return x + y;
}

static void Main(string[] args)
{
     Console.WriteLine(Sum(15, 20));
     Console.Read();
}

展開後是這樣。

跟你預期的一樣嗎? 老實說我原本預期的是Reflection,看來編譯器越來越聰明了。這功能在處理Exception超好用。

public static int Sum(int x, int y)
{
     if (x == -1) throw new ArgumentException($"{nameof(x)} can't be -1");
     return x + y;
}

static void Main(string[] args)
{
     Console.WriteLine(Sum(-1, 20));
     Console.Read();
}

例外會是這樣。

 

Index initializers

  在C# 6.0之後,你可以更迅速的指定Dictionary、Hashtable特定元素的值。

static void Main(string[] args)
{
    var numbers = new Dictionary<int, string>
    {
       [7] = "seven",
         [9] = "nine",
         [13] = "thirteen"
    };
    Console.WriteLine(numbers[7]);
    Console.Read();
}

結果是seven,展開後。

在Json.NET中,這特別好用。

static void Main(string[] args)
{
     var jobj = new JObject()
     {
          ["Version"] = 1,
          ["Name"] = "smart software"
     };
     Console.WriteLine(jobj?["Version"]);
     Console.Read();
}

 

Exception filters

  指的是你可以在catch 之前呼叫一個傳回boolean值函式,例如下面這樣。

public class TestObject
{
     bool Log(Exception ex)
     {
         Console.WriteLine(ex.Message);
        return true;
     }

     public void Test(string s)
     {
          try
          {
                    s.Substring(1, 6);
            }
            catch(Exception ex) when(Log(ex))
            {
                 Console.WriteLine("invalid");
            }
        }
}

static void Main(string[] args)
{
    TestObject o = new TestObject();
    o.Test(null);
    Console.Read();
}

結果是下面這樣。

當捕捉的例外發生時,Log函式會被呼叫,ex參數會被傳入,如果Log回傳值是true,那麼執行例外處理區段,如果是false,繼續拋出例外,下面為回傳false的狀態。

這東西的展開比較特別,他其實不是展開,所以你反組譯後會像下面這樣,這是無法編譯的。

真實的情況是C# 6.0編譯器支援了IL階級的try..catch..filter..endfilter機制,這個在以前的VB.NET與F#早就支援了。

 

Await in catch and finally blocks

  在C# 5.0時,你不能在catch區段使用async/await,這在C# 6.0支援了。

public static async void Test()
{
      HttpClient client = new HttpClient();
      try
      {               
           var content = await client.GetStringAsync("s://33333");
      }
      catch (Exception)
      {
           var next = await client.GetStringAsync("http://www.google.com");
      }
}

展開的結果如下。

private void MoveNext()
{
    int num = this.<>1__state;
    try
    {
        string result;
        Program.<Test>d__0 d__;
        switch (num)
        {
            case 0:
                break;

            case 1:
                goto Label_0139;

            default:
                this.<client>5__1 = new HttpClient();
                this.<>s__3 = 0;
                break;
        }
        try
        {
            TaskAwaiter<string> awaiter;
            if (num != 0)
            {
                awaiter = this.<client>5__1.GetStringAsync(
                                           "s://33333").GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    this.<>1__state = num = 0;
                    this.<>u__1 = awaiter;
                    d__ = this;
                    this.<>t__builder.AwaitUnsafeOnCompleted<
                               TaskAwaiter<string>, Program.<Test>d__0>(
                                             ref awaiter, ref d__);
                    return;
                }
            }
            else
            {
                awaiter = this.<>u__1;
                this.<>u__1 = new TaskAwaiter<string>();
                this.<>1__state = num = -1;
            }
            result = awaiter.GetResult();
            awaiter = new TaskAwaiter<string>();
            this.<>s__5 = result;
            this.<content>5__4 = this.<>s__5;
            this.<>s__5 = null;
            this.<content>5__4 = null;
        }
        catch (Exception exception)
        {
            this.<>s__2 = exception;
            this.<>s__3 = 1;
        }
        if (this.<>s__3 != 1)
        {
            goto Label_018A;
        }
        TaskAwaiter<string> awaiter2 = 
             this.<client>5__1.GetStringAsync(
                                "http://www.google.com").GetAwaiter();
        if (awaiter2.IsCompleted)
        {
            goto Label_0156;
        }
        this.<>1__state = num = 1;
        this.<>u__1 = awaiter2;
        d__ = this;
        this.<>t__builder.AwaitUnsafeOnCompleted<
                         TaskAwaiter<string>, 
                          Program.<Test>d__0>(ref awaiter2, ref d__);
        return;
    Label_0139:
        awaiter2 = this.<>u__1;
        this.<>u__1 = new TaskAwaiter<string>();
        this.<>1__state = num = -1;
    Label_0156:
        result = awaiter2.GetResult();
        awaiter2 = new TaskAwaiter<string>();
        this.<>s__7 = result;
        this.<next>5__6 = this.<>s__7;
        this.<>s__7 = null;
        this.<next>5__6 = null;
    Label_018A:
        this.<>s__2 = null;
    }
    catch (Exception exception2)
    {
        this.<>1__state = -2;
        this.<>t__builder.SetException(exception2);
        return;
    }
    this.<>1__state = -2;
    this.<>t__builder.SetResult();
}

其實就是AsyncVoidMethodBuilder的串接,5.0不實作這個功能大概是因為展開的過程比較複雜或是時間關係吧。