CodeGym /課程 /C# SELF /反射: System.Reflection

反射: System.Reflection

C# SELF
等級 63 , 課堂 0
開放

1. 介紹

反射(英語 Reflection)是程式在執行時檢視自身結構的能力。想像你的程式可以問自己:'我有哪些方法?這個型別有哪些屬性?',並在執行期得到答案。

反射的實際應用範例

  • 建立插件與模組:你不需要事先知道會使用哪個類別——只要載入 DLL 並即時檢查它的方法就可以。
  • 測試自動化:測試框架,例如 xUnit 和 NUnit,透過反射找到測試方法。
  • 序列化:用反射可以自動把任意物件轉成 JSON/XML,而不需要手動列舉屬性。
  • 根據 attributes 做資料驗證:比如你在屬性上加了 [Required],想自動檢查所有這類屬性。

為什麼面試會問到這個

會問反射是因為它能展示你對 .NET 運作方式的理解,以及解決動態需求的能力。如果你想寫自己的框架或擴充功能,反射非常實用。

2. 命名空間與反射的基本類別

C# 的所有反射功能都在命名空間 System.Reflection。檔案開頭別忘了:

using System.Reflection;

反射的主要型別

類別/型別 說明
Type
代表 .NET 中型別的描述(例如 class、interface、enum 等)
PropertyInfo
型別的屬性
MethodInfo
型別的方法
FieldInfo
型別的欄位
ConstructorInfo
型別的建構子
Assembly
代表一個組件(EXE 或 DLL)

類別 Type 的主要方法

方法 說明
GetProperties()
回傳所有 public 的屬性
GetFields()
回傳所有 public 的欄位
GetMethods()
回傳所有 public 的方法
GetConstructors()
回傳所有 public 的建構子
GetInterfaces()
回傳型別實作的所有介面
GetCustomAttributes()
回傳套用到型別上的所有 attributes

通常可以傳入 BindingFlags 到這些方法來存取 private 或 static 成員,但我們先從基本情境開始。

3. 第一個步驟:取得 Type 物件

所有反射技巧都建立在 Type 物件之上。

有幾種方式可以取得這個物件,下面逐一說明:

方式 1:透過物件

string text = "Hello, Reflection!";
Type type1 = text.GetType();
Console.WriteLine(type1.Name); // String

方式 2:透過型別名稱

Type type2 = typeof(int);
Console.WriteLine(type2.FullName); // System.Int32

方式 3:用字串(動態)

Type type3 = Type.GetType("System.Double");
Console.WriteLine(type3); // System.Double

注意!若是其他組件中的自訂類別,需要帶上包含組件名稱的完整型別名稱。

4. 探索型別結構:屬性、方法、欄位和建構子

現在重點來了:取得了 Type 物件後,你可以查到任何你想要的資訊。

取得屬性列表

Type type = typeof(DateTime);
PropertyInfo[] properties = type.GetProperties();

foreach (var prop in properties)
{
    Console.WriteLine($"{prop.PropertyType.Name} {prop.Name}");
}

輸出:

Int32 Day
Int32 Month
Int32 Year
DayOfWeek DayOfWeek
...

取得方法列表

MethodInfo[] methods = type.GetMethods();

foreach (var method in methods)
{
    Console.WriteLine($"{method.ReturnType.Name} {method.Name}()");
}

大型型別的方法可能非常多!通常你不需要全部,而只想要「自己的」方法(例如排除繼承自 object 的方法)。我們稍後會處理這類過濾。

取得欄位列表

FieldInfo[] fields = type.GetFields();

foreach (var field in fields)
{
    Console.WriteLine($"{field.FieldType.Name} {field.Name}");
}

不過大多數常見類別不會有公開欄位,因為封裝是很重要的 :)

取得建構子

ConstructorInfo[] constructors = type.GetConstructors();

foreach (var ctor in constructors)
{
    Console.WriteLine($"建構子: {ctor}");
}

5. 在實務中應用反射:分析我們的應用程式

提醒一下,在我們的教學專案中有一個類別 TaskItem,用來存放任務。我們來透過反射檢查它。

public class TaskItem
{
    public int Id { get; set; }
    public string Title { get; set; }
    public DateTime DueDate { get; set; }
    public bool IsCompleted;
}

下面示範如何在執行期查看型別的「內部」:

Type taskType = typeof(TaskItem);

Console.WriteLine("屬性:");
foreach (var prop in taskType.GetProperties())
    Console.WriteLine($"  {prop.PropertyType.Name} {prop.Name}");

Console.WriteLine("欄位:");
foreach (var field in taskType.GetFields())
    Console.WriteLine($"  {field.FieldType.Name} {field.Name}");

Console.WriteLine("方法:");
foreach (var method in taskType.GetMethods())
    Console.WriteLine($"  {method.ReturnType.Name} {method.Name}()");

試著在類別中新增屬性,看看反射如何看到它們。這就是通用序列化器和物件檢視器的工作方式。

6. 視覺化:Type 物件的結構(示意圖)


+-------------------------+
|         Type            |
+-------------------------+
|  .Name                  |
|  .FullName              |
|  .Namespace             |
|  .Assembly              |
+-------------------------+
|  .GetProperties()       |
|  .GetFields()           |
|  .GetMethods()          |
|  .GetConstructors()     |
|  .GetInterfaces()       |
|  .GetCustomAttributes() |
+-------------------------+

這就是型別的「肖像」——有名稱、組件、成員集合和 attributes。

取得型別的基本資訊

Type 物件可以告訴你這是什麼型別:

Type t = typeof(double);

Console.WriteLine(t.Name);         // Double
Console.WriteLine(t.FullName);     // System.Double
Console.WriteLine(t.Namespace);    // System
Console.WriteLine(t.IsClass);      // False
Console.WriteLine(t.IsValueType);  // True
Console.WriteLine(t.IsEnum);       // False
Console.WriteLine(t.IsPrimitive);  // True

7. 根據名稱查找特定的屬性、方法或欄位

屬性

var prop = taskType.GetProperty("Title");
if (prop != null)
{
    Console.WriteLine($"屬性 'Title' 的型別: {prop.PropertyType}");
}

方法

var method = taskType.GetMethod("ToString");
if (method != null)
{
    Console.WriteLine($"方法 'ToString' 回傳: {method.ReturnType}");
}

欄位

var field = taskType.GetField("IsCompleted");
if (field != null)
{
    Console.WriteLine($"欄位 'IsCompleted' 的型別: {field.FieldType}");
}

8. 用反射做動態存取值

現在不只看,還可以真正「操作」值!

讀取屬性值

TaskItem item = new TaskItem { Id = 1, Title = "測試反射", DueDate = DateTime.Today };
PropertyInfo titleProp = item.GetType().GetProperty("Title");

if (titleProp != null)
{
    object value = titleProp.GetValue(item);
    Console.WriteLine($"標題: {value}");
}

設定屬性值

titleProp.SetValue(item, "已修改的標題");
Console.WriteLine(item.Title);

操作欄位

FieldInfo completedField = item.GetType().GetField("IsCompleted");
completedField.SetValue(item, true);
Console.WriteLine(item.IsCompleted); // true

若要存取 private 欄位或屬性,需要額外的旗標,例如 BindingFlags.NonPublic。我們會在之後介紹。

9. 用反射呼叫方法

你可以在不知道方法細節的情況下呼叫它!

TaskItem task = new TaskItem { Title = "反射很棒!" };
MethodInfo method = task.GetType().GetMethod("ToString");

if (method != null)
{
    object result = method.Invoke(task, null);
    Console.WriteLine(result);
}

若方法有參數,要把參數放在 object 陣列中傳入:

public class Calculator
{
    public int Add(int a, int b) => a + b;
}

var calc = new Calculator();
var addMethod = typeof(Calculator).GetMethod("Add");
object[] parameters = { 5, 3 };
object sumResult = addMethod.Invoke(calc, parameters);
Console.WriteLine(sumResult); // 8

10. 透過建構子動態建立物件

反射可以在沒有 new 的情況下建立物件!

Type taskType = typeof(TaskItem);
object newTask = Activator.CreateInstance(taskType);
// 這仍然是同一個 TaskItem(但型別是 object)
Console.WriteLine(newTask.GetType().Name); // TaskItem

也可以傳入建構子的參數:

public class User
{
    public string Name { get; }
    public User(string name) { Name = name; }
}

Type userType = typeof(User);
object user = Activator.CreateInstance(userType, "艾莉絲");
Console.WriteLine(((User)user).Name); // 艾莉絲

11. 使用 System.Reflection 的常見錯誤

錯誤 #1:在沒有使用 BindingFlags 的情況下存取 private 成員。
預設情況下反射只會看到 public 成員。未使用 BindingFlags.NonPublic 就想存取私有欄位或方法是行不通的。

錯誤 #2:忽略 null 檢查。
GetPropertyGetMethod 等方法在找不到成員時會回傳 null,不做檢查會導致 NullReferenceException

錯誤 #3:沒選對方法的 overload。
當有多個同名的 overload 時,單純用 GetMethod 可能不是你想要的。請使用帶參數型別的 GetMethod 或其他更精確的搜尋方式。

錯誤 #4:在性能敏感的程式中濫用反射。
反射比直接呼叫慢。對於高效能需求的程式,應該快取 metadata 或使用 delegate。

2
任務
C# SELF, 等級 63, 課堂 0
上鎖
取得型別的屬性、方法和欄位清單
取得型別的屬性、方法和欄位清單
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION