工作中的 C# 實作:Reflection 反射

AdSense

1. 什麼是 Reflection (反射)?

Reflection (反射) 是一種強大的 C# 技術,它允許程式在執行時 (runtime) 動態地檢查和操作自己的中繼資料 (metadata)

你可以把 Reflection 想像成一面特殊的鏡子,你的程式可以透過這面鏡子「看見」自己內部的結構,例如:

  • 有哪些類別 (Classes)。
  • 每個類別有哪些方法 (Methods)、屬性 (Properties) 或欄位 (Fields)。
  • 這些成員的存取修飾詞 (例如 publicprivate)。
  • 以及它們的型別和參數。

更厲害的是,Reflection 不只可以「看」,還能動態地「呼叫」這些成員,甚至是在程式碼編譯時完全不知道它們存在的情況下。

2. 為什麼需要 Reflection?

在一般的程式設計中,我們都是在編譯時就確定要使用哪個類別和方法。例如,我們必須先知道 Person 類別有 Name 屬性,才能寫 person.Name

但有些情境下,你可能無法在編譯時就確定這些資訊,這時 Reflection 就派上用場了。常見的應用場景包括:

  • 動態載入組件 (Assembly):在執行時載入程式碼中未直接引用的 DLL 檔案。
  • 序列化與反序列化:將物件轉換成 JSON 或 XML 字串,或從字串中重建物件。因為要處理的物件型別在設計階段可能不確定,需要動態地遍歷所有屬性來讀取或寫入資料。
  • 讀取註釋 (Attribute):像是單元測試框架會用 Reflection 尋找所有被 [Test] 註釋標記的方法,然後執行它們。

3. Reflection 語法

使用 Reflection 的核心是 System.Type 類別,它包含了任何型別的所有中繼資料。你可以透過以下兩種方式取得 Type 物件:

方式一:使用 typeof() 運算子

當你在編譯時知道型別名稱時,這是最簡單的方式。

1
2
// 假設我們有一個 Person 類別
Type personType = typeof(Person);

方式二:使用 GetType() 方法

當你只有一個物件實例,想在執行時知道它的型別時,可以使用這個方法。

1
2
Person person = new Person();
Type personType = person.GetType();

4. Type.GetProperties() 方法

這是一個非常常用的 Reflection 方法。它用於取得一個型別的所有屬性 (Property) 資訊。它的完整簽名檔通常會帶一個 BindingFlags 參數,用來精確控制你要搜尋的範圍。

例如:

var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);

這裡面的 BindingFlags 是一個位元遮罩 (Bitmask),可以透過 | (位元 OR 運算子) 來組合多個標誌,以過濾出你想要的成員。

常見的 BindingFlags 標誌:

  • BindingFlags.Public:只取得公開 (public) 的成員。
  • BindingFlags.NonPublic:只取得非公開 (private, protected, internal) 的成員。
  • BindingFlags.Instance:只取得屬於物件實例的成員。
  • BindingFlags.Static:只取得靜態 (static) 的成員。

在上面的程式碼中,BindingFlags.Public | BindingFlags.Instance 的意思是:「請給我所有公開的且屬於物件實例的屬性。」

如果我們忽略這個參數,GetProperties() 會預設取得所有公開的靜態和實例屬性。但為了確保精準性,強烈建議總是使用 BindingFlags

5. 範例:動態讀取物件屬性

示範如何使用 Reflection 動態地讀取一個物件的所有公開屬性及其值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Reflection; // 記得要引入這個命名空間

// 步驟 1: 建立一個簡單的類別
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }

// 這是一個私有屬性,Reflection 預設不會找到它
private DateTime LastLoginTime { get; set; }

// 這是一個靜態屬性,沒有 BindingFlags.Static 也不會找到它
public static string SystemVersion { get; set; } = "1.0.0";
}

public class Program
{
public static void Main()
{
// 步驟 2: 建立物件實例
User user = new User { Id = 1, Name = "Leo", Email = "leo@example.com" };

// 步驟 3: 取得物件的 Type 物件
Type userType = user.GetType();

// 步驟 4: 使用 GetProperties() 搭配 BindingFlags 取得屬性資訊
// 這裡我們只取得 public 的實例屬性
PropertyInfo[] properties = userType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

Console.WriteLine($"物件 {userType.Name} 的公開實例屬性:");
Console.WriteLine("-----------------------------------");

// 步驟 5: 遍歷屬性並動態取得名稱和值
foreach (PropertyInfo property in properties)
{
// 取得屬性的名稱
string propName = property.Name;

// 使用 GetValue 方法,傳入物件實例,來取得該屬性的值
object propValue = property.GetValue(user);

Console.WriteLine($"屬性名稱: {propName}, 屬性值: {propValue ?? "null"}");
}
}
}

程式碼解說:

  1. 我們定義了一個 User 類別,裡面有公開、私有和靜態屬性。
  2. Main 方法中,我們用 user.GetType() 取得了 Type 物件。
  3. 接著,GetProperties(BindingFlags.Public | BindingFlags.Instance) 告訴程式我們只對 IdNameEmail 這些公開的實例屬性感興趣。
  4. 最後,我們遍歷 properties 陣列,並使用 property.GetValue(user) 這行程式碼,在執行時user 物件中取出每個屬性的值。

執行結果:

1
2
3
4
5
物件 User 的公開實例屬性:
-----------------------------------
屬性名稱: Id, 屬性值: 1
屬性名稱: Name, 屬性值: Leo
屬性名稱: Email, 屬性值: leo@example.com

從這個結果可以看到,LastLoginTimeSystemVersion 都沒有被列印出來,因為它們不符合我們在 BindingFlags 中指定的篩選條件。這完美地展示了 Reflection 的精準和動態能力。