C# 7 分解式(deconstructor)

C# 7 新增了一種叫做「分解式」(deconstructor)的方法,方法名稱固定為 “Deconstruct”。一旦類別有提供此方法,它就具備了「將物件內的元素逐一分解至多個變數」的能力。我們先用一個 Tuple 範例來看看「分解」實際上指的是什麼。
註 1:Deconstructor 很容易被看成是 destructor。前者是 C# 7 新增的分解方法,後者是所謂的「解構子」(解構函式)。, 
註 2:寫這篇筆記時,我沒有多方查證,而是把 deconstructor 擅自譯為「分解式」。歡迎提供更好的譯法。

沿用上一篇文章的範例,這次只是稍微修改先前的 ShowEmpInfo() 函式:

public void ShowEmpInfo()
{
    var emp = ("王大同", 50);     // tuple literal
    var (empName, empAge) = emp; // deconstruction declaration
    Console.WriteLine($"{empName} {empAge}"); // "王大同 50"
}

請看倒數第 3 行,等號的右邊是一個 Tuple 物件。等號的左邊,則使用了所謂的分解式宣告(deconstruction declaration)的語法。這行程式碼的作用是:把一個 Tuple 物件裡面的元素依序指派給等號左邊的括弧中宣告的變數。然後,我們就可以直接使用這些變數(倒數第 2 行)。

現在讓我們來看看,如何讓自己設計的類別提供「分解」能力。

先前提過,分解方法的名稱必須是 “Deconstruct”。此方法的回傳型別必須是 void,而它所分解出來的東西,是透過輸出參數來提供給呼叫端。請看以下範例:

public class Box 
{ 
    public int Width { get; } // 唯讀的自動屬性 
    public int Height { get; } // 唯讀的自動屬性

    // 建構式(constructor)
    public Box(int width, int height)
    {
        Width = width;
        Height = height;
    }

    // 分解式(deconstructor)
    public void Deconstruct(out int width, out int height)
    {
        width = this.Width;
        height = this.Height;
    }

    // 請留意建構式和分解式兩者的相似與相異處;
    // 在設計你的類別時,這兩種函式可能會成對出現。
}

如此一來,類別 Box 就具備了將本身包含的資訊(寬與高)拆解成兩個 int 變數的能力。底下是應用例:

var box = new Box(20, 50); 
var (width, height) = box; // 將 box 物件分解成兩個變數 
Console.WriteLine($"寬={width},={height}");

其中第 2 行完成了件事:宣告兩個 int 變數(width 和 height),然後呼叫 Box 的 Deconstruct 方法來分別設定那兩個變數的初始值。

上例的分解式不一定要寫成一行,你也可以將物件分解至事先宣告的變數,像這樣:

int width; 
int height; 
(width, height) = box; // 將 box 物件分解成兩個變數

此範例的 .NET Fiddle 連結:https://dotnetfiddle.net/VKwB83

分解式可以多載

一個類別可以有多個分解式,像這樣:

public void Deconstruct(out int width, out int height) 
{ 
    () 
}

public void Deconstruct( 
    out int left, out int top, out int right, out int bottom) 
{ 
    () 
}

既然與一般的方法宣告相同,那我們當然也可以直接呼叫它:

var box = new Box(20, 50);
box.Deconstruct(out int width, out int height);
// 上一行使用了本章稍早介紹過的 `out` 變數的宣告語法。


分解式可以寫成擴充方法

分解式也能寫成擴充方法,而不一定要寫在類別裡面。同樣延續先前的範例,假設我們無法取得 Box 類別的原始碼,我們依然能透過擴充方法來為它提供分解式:

public static class BoxExtensions
{
    public static void Deconstruct(this Box box, out int width, out int height) 
    {
        width = box.Width;
        height = box.Height;
    }
}

如果類別本身已經提供了分解式,你又以擴充方法另外寫了相同 signature 的擴充方法 ,此時編譯器會選擇使用類別本身提供的分解式。

OK,現在我們看得很清楚了:`Deconstruct` 方法與一般的 C# 方法沒有太大差異,只是方法名稱得按規定,不能隨便取。這裡要再特別指出的是,實際上會呼叫哪一個分解式,是在編譯時期就決定的,這表示 dynamic 變數無法使用物件的分解式。因此,底下的寫法無法通過編譯:

dynamic box = new Box(10, 10);
box.Deconstruct(out int width, out int height); // 編譯失敗!



巢狀分解

C# 7 的分解式還支援巢狀分解。為了解釋這個稍稍複雜一點的語法,這裡仍沿用先前的例子,把 Box 類別改寫一下

public class Box
{
    public int Width { get; }
    public int Height { get; }

    public Box(int width, int height)
    {
        Width = width;
        Height = height;
    }

    // 一號分解式
    public void Deconstruct(out int width, out int height)
    {
        width = this.Width;
        height = this.Height;
    }

    // 二號分解式
    public void Deconstruct(out int width, out int height, out Box box)
    {
        width = this.Width;
        height = this.Height;
        box = new Box(width / 2, height / 2); // 內盒是外盒的一半大小
    }
}

現在 Box 類別的分解式有兩個多載版本,而新加入的分解式(註解標示「二號分解式」)允許傳入三個參數,這第三個參數的型別也是 Box(當然也可以是其他型別,這裡用現成的類別只是方便解釋)。

那麼,在應用時,可以這樣寫:

var box = new Box(20, 50); 
var (width, height, (innerWidth, innerHeight)) = box; // 巢狀分解 
Console.WriteLine($"內盒寬高 = {innerWidth} x {innerHeight}");

其中的第 2 行做了兩次分解:
  1. 分解 var 宣告的最外層括弧,我們可以用簡短代號,把它看成 (w, h, (x)),可能會比較好理解。也就是說,第一層括弧裡面有三個變數,因此這裡會先呼叫的是帶有三個參數的那個,也就是二號分解式。
  2. 承上,(w, h, (x)) 中的 x 還需要進一步拆解成兩個 int 變數,所以接著要再呼叫帶有兩個參數的分解式,即一號分解式。

以上範例的 .NET Fiddle 連結:https://dotnetfiddle.net/qUqCqZ

小結

整理 deconstructor 的一些要點:
  • deconstructor 不是 destructor;deconstructor 的函式名稱是 Deconstruct。
  • deconstructor 不只能用來分解 Tuple;只要類別有提供合適的 Deconstruct 方法就可以分解。
  • deconstructor 可以多載,也可以寫成擴充方法。由於是編譯時期進行方法解析,故 dynamic 變數無法使用 Deconstruct 方法(即無法通過編譯)。
  • 可巢狀分解。範例:var (x, (y, z)) = anObject;
  • 中文叫做「分解式」好嗎?

(2017-04-17:本文已加入《C# 本事》,往後如有增補,將更新於電子書)

C# 7 新增的 Tuple 語法


C# 函式如果要有多個回傳值,大抵離不開以下幾種作法:
  • 使用輸出參數。
    亦即透過參數列的 out 修飾詞來定義輸出參數。
  • 傳回一個 dynamic 物件。
    此作法的缺點是效能較差,而且沒有編譯時期的型別安全檢查。
  • 使用自訂型別。
    亦即寫一個新類別,把要返回的多項資訊包在這個類別裡面,然後讓函式返回這個自訂類別的物件。
  • 使用 .NET 現成的 System.Tuple

如果只是比較簡單、或者用完即丟的場合,就沒必要寫一個新類別,而可以選擇 .NET framework 提供的 Tuple。底下示範 C# 7 之前的 Tuple 用法:

public Tuple<string, int> GetEmpInfo()  // 指定回傳型別
{
    // 建立回傳的 Tuple 物件
    return Tuple.Create("王大同", 50); 
}

public void ShowEmpInfo()
{
    var emp = GetEmpInfo();
    // 使用 Tuple 物件的內容
    Console.WriteLine($"{emp.Item1} {emp.Item2}"); // "王大同 50"
}

你可以看到,函式 GetEmpInfo 返回的是泛型 Tuple<T1, T2>,並且使用 Tuple.Create() 方法來建立返回的 Tuple 物件。.NET framework 為 Tuple 定義了八個版本的泛型方法,讓你可以輕鬆建立包含多達八個、甚至更多個數值的物件。

使用 Tuple 物件時,是以 Item1Item2ItemN 的方式來取得其內部的物件。從這裡可以明顯看得出來,無法用名稱、而只能用順序編號的方式來存取其內部元素,這種寫法很難稱得上高可讀性。C# 7 在這方面做了改進。

底下是上述範例的 C# 7 版本:


你可以看到,跟先前的版本比起來,有三個地方不一樣:
  • 在宣告函式的回傳型別時(第 2 行),語法已經能夠像參數列那樣指定參數的名稱。這裡使用的是 C#7 新增的 tuple type 語法。
  • 建立 Tuple 物件的寫法更簡單了(第 6 行)。這裡不是用 new 來建立物件,而是 C# 7 的 tuple literal 語法。
  • 使用 Tuple 物件時,可以用名稱來存取內部元素(倒數第 2 行)。

以上範例的 .NET Fiddle 連結:https://dotnetfiddle.net/9Hv9PZ

註:如果你的 Visual Studio 在編譯此範例程式時出現錯誤:

Cannot define a class or member that utilizes tuples because the compiler required type ‘System.Runtime.CompilerServices.TupleElementNamesAttribute’ cannot be found. Are you missing a reference? 

請用 NuGet 加入這個組件參考:System.ValueTuple

《我該如何閱讀》筆記

2017-02-05 更新:從圖書館看到這本書,借回家後,正準備在女兒床邊唸給她聽,她卻說,我的部落格曾經寫過這本書的書摘!要不是我當時沒認真讀這本書,就是我的記憶力嚴重退化了(或兩者皆有)。既然借來了,就再讀一遍吧。順便把這篇筆記更新一下。

用 Chrome 開啟 SSRS 報表管理員時出現權限不足的錯誤

SSRS 新手筆記:第一次設定好 SQL Server Reporting Service 之後,以瀏覽器開啟報表管理員,結果出現權限不足的錯誤。

Copyright © 2012. Huan-Lin 學習筆記 - All Rights Reserved
Powered by Blogger
Template Design by Cool Blogger Tutorials
Published by Templates Doctor