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;
  • 中文叫做「分解式」好嗎?