T# 範本語言
T# 是 Metalama 核心的編譯期範本語言。它看起來像 C# 且使用 C# 語法,但有不同的語意 — 某些程式碼在編譯期執行以產生其他在執行期運行的程式碼。
什麼是 T#?
T# 不是一種獨立的語言。它是 C# 的子集,具有特殊的編譯期語意。當你撰寫 Aspect 範本時,Metalama 編譯器會分析每個運算式和陳述式,以決定它應該:
- 在編譯期執行(用來產生程式碼),或
- 作為執行期程式碼輸出(在應用程式執行時運作)
關鍵洞察
在 T# 範本中,有些行是給編譯器的指令(「產生這段程式碼」),而其他行就是被產生的程式碼本身。相同的 C# 語法同時服務於兩種用途。
meta API
meta 偽關鍵字是你通往編譯期操作的入口。它是一個靜態類別,其屬性和方法僅在編譯期間存在。
meta.Proceed()
呼叫原始(或鏈中下一個)方法實作:
public override dynamic? OverrideMethod()
{
Console.WriteLine("Before");
var result = meta.Proceed(); // → replaced with original method body
Console.WriteLine("After");
return result;
}
meta.ProceedAsync()
meta.Proceed() 的非同步版本:
public override async Task<dynamic?> OverrideAsyncMethod()
{
Console.WriteLine("Before");
var result = await meta.ProceedAsync(); // → awaits original async method
Console.WriteLine("After");
return result;
}
meta.Target
提供對目標宣告中繼資料的編譯期存取:
| 屬性 | 型別 | 說明 |
|---|---|---|
meta.Target.Method | IMethod | 目標方法 |
meta.Target.Method.Name | string | 方法名稱(編譯期常數) |
meta.Target.Method.ReturnType | IType | 回傳型別 |
meta.Target.Parameters | IParameterList | 方法參數 |
meta.Target.Type | INamedType | 包含的型別 |
meta.Target.FieldOrProperty | IFieldOrProperty | 目標欄位/屬性(在屬性 Aspect 中) |
meta.Target.Constructor | IConstructor | 目標建構函式 |
meta.This 和 meta.Base
動態存取實例成員:
public override dynamic? OverrideMethod()
{
// meta.This resolves to 'this' but allows dynamic member access
var name = meta.This.Name; // Access any property of the target instance
// meta.Base calls the base implementation (for virtual overrides)
return meta.Base.MyMethod();
}
meta.Tags
從 BuildAspect() 傳遞資料到範本:
public override void BuildAspect(IAspectBuilder<IMethod> builder)
{
builder.Override(nameof(OverrideMethod),
tags: new { EventName = "CustomEvent" });
}
[Template]
public override dynamic? OverrideMethod()
{
var eventName = (string)meta.Tags["EventName"]!; // "CustomEvent"
Console.WriteLine($"Event: {eventName}");
return meta.Proceed();
}
meta.CompileTime() 和 meta.RunTime()
明確控制程式碼作用域:
public override dynamic? OverrideMethod()
{
// Force a value to be compile-time
var paramCount = meta.CompileTime(meta.Target.Parameters.Count);
// Force a compile-time expression to emit as run-time code
var runtimeValue = meta.RunTime(someCompileTimeExpression);
return meta.Proceed();
}
Template Expansion Lifecycle
meta 的所有屬性、meta.Proceed()、編譯期 foreach / if 都只在編譯期存在。下圖是 Metalama 編譯器把一個 Template(例如 LogAttribute.OverrideMethod())套用到目標方法 DoWork(int x) 時,內部訊息流的順序:
讀圖重點:
| 步驟 | meta API 的角色 |
|---|---|
求值 meta.Target.Method.Name | 編譯期取得目標方法的 metadata,回傳 編譯期常數(會被當成字串字面量嵌入產生的程式碼) |
求值 meta.Target.Parameters | 回傳編譯期集合,後續 foreach 直接由編譯器展開,執行期看不到任何迴圈 |
展開編譯期 foreach | 每個 parameter 都複製一份迴圈主體,產生 N 份獨立的執行期程式碼 |
meta.This.Name | 動態實例存取:回傳一個解析為 this.Name 的執行期表達式(不是值) |
meta.Proceed() | 在當前位置嵌入原始方法的整個 body(或非同步版本 await meta.ProceedAsync()) |
| 寫出 IL | Template 與 meta.* 在輸出中完全消失,只剩展開後的執行期 C# / IL |
編譯期運算式
Metalama 編譯器會根據每個運算式的型別和用法來判斷其作用域。
編譯期型別
任何涉及以下型別的運算式都是編譯期的:
IMethod、IType、IParameter、IField、IProperty(程式碼模型介面)IExpression、IStatement(範本建構區塊)meta.*屬性回傳的值- 以
Metalama.Framework.Code中型別宣告的變數
自動作用域偵測
public override dynamic? OverrideMethod()
{
// ┌─ COMPILE-TIME: meta.Target.Method is a code model object
var method = meta.Target.Method;
// ┌─ COMPILE-TIME: .Name on a code model object returns compile-time string
var name = method.Name;
// ┌─ RUN-TIME: Console.WriteLine is a run-time call
// (but 'name' is interpolated as a compile-time constant)
Console.WriteLine($"Method: {name}");
// ┌─ COMPILE-TIME: Iterating over compile-time collection
foreach (var param in meta.Target.Parameters)
{
// ┌─ RUN-TIME: Generated for each parameter
Console.WriteLine($" {param.Name} = {param.Value}");
}
// └─ The foreach itself disappears; only the unrolled lines remain
return meta.Proceed();
}
編譯期控制流程
編譯期 if
當條件是編譯期運算式時,if 會在編譯期求值。只有符合的分支會被輸出:
public override dynamic? OverrideMethod()
{
// Compile-time if: only ONE branch is generated
if (meta.Target.Method.IsAsync)
{
Console.WriteLine("This is an async method");
}
else
{
Console.WriteLine("This is a sync method");
}
return meta.Proceed();
}
對於同步方法,產生的程式碼僅為:
Console.WriteLine("This is a sync method");
if 陳述式和非同步分支在輸出中完全不存在。
編譯期 foreach
對編譯期集合(如 meta.Target.Parameters)進行迭代,會為每個元素產生一份迴圈主體的副本:
// Template:
foreach (var p in meta.Target.Parameters)
{
Console.WriteLine($"{p.Name} = {p.Value}");
}
// Generated for DoWork(int x, string y):
Console.WriteLine($"x = {x}");
Console.WriteLine($"y = {y}");
編譯期 switch
運作方式與編譯期 if 相同 — 只有符合的 case 會被輸出:
switch (meta.Target.Parameters.Count)
{
case 0:
Console.WriteLine("No parameters");
break;
case 1:
Console.WriteLine($"One parameter: {meta.Target.Parameters[0].Name}");
break;
default:
Console.WriteLine($"Multiple parameters: {meta.Target.Parameters.Count}");
break;
}
dynamic? 回傳型別
Aspect 範本方法回傳 dynamic?,因為它們必須適用於任何回傳型別:
public override dynamic? OverrideMethod()
{
// Works for: void, int, string, Task<Recipe>, ValueTask<bool>, etc.
return meta.Proceed();
}
運作方式
- 對於
void方法:meta.Proceed()回傳null,而return null會被最佳化移除 - 對於值型別:dynamic 會在編譯期解析為實際型別
- 對於參考型別:同上
- 對於
Task<T>:與OverrideAsyncMethod()模式結合使用
你永遠不需要轉型回傳值 — Metalama 會在編譯期處理型別解析。
範本方法
標記為 [Template] 的方法可以在 BuildAspect() 中作為範本使用:
public class MyAspect : TypeAspect
{
public override void BuildAspect(IAspectBuilder<INamedType> builder)
{
// Use the template to override all public methods
foreach (var method in builder.Target.Methods.Where(m => m.Accessibility == Accessibility.Public))
{
builder.With(method).Override(nameof(LogTemplate));
}
}
[Template]
public dynamic? LogTemplate()
{
Console.WriteLine($"Called: {meta.Target.Method.Name}");
return meta.Proceed();
}
}
範本 vs. 覆寫
| 特性 | OverrideMethod() | [Template] 方法 |
|---|---|---|
| 用途 | 簡單 Aspect(單一覆寫) | 複雜 Aspect(BuildAspect) |
| 套用對象 | 自動套用到目標 | 透過 builder.Override() 手動套用 |
| 命名 | 必須命名為 OverrideMethod | 任意名稱 |
| 多重 | 每個 Aspect 一個 | 每個 Aspect 可有多個範本 |
屬性範本
對於 OverrideFieldOrPropertyAspect,範本是一個屬性:
public class TrimAttribute : OverrideFieldOrPropertyAspect
{
public override dynamic? OverrideProperty
{
get => meta.Proceed();
set
{
// Trim string values before setting
if (meta.Target.FieldOrProperty.Type.Is(typeof(string)))
{
meta.Target.FieldOrProperty.Value = value?.Trim();
}
else
{
meta.Proceed();
}
}
}
}
欄位轉屬性提升
當套用到欄位時,Metalama 自動將欄位提升為屬性:
// Source code:
[Trim]
public string Name; // Field
// Generated code:
private string _name; // Backing field (auto-generated)
public string Name // Property (promoted from field)
{
get => _name;
set => _name = value?.Trim();
}
限制:與
ref或out一起使用的欄位無法提升為屬性。Metalama 會回報錯誤。
編譯期輔助方法
你可以使用 [CompileTime] 將編譯期邏輯抽取到輔助方法中:
public class MyAspect : OverrideMethodAspect
{
[CompileTime]
private static bool ShouldLogParameter(IParameter parameter)
{
// This runs at compile time to decide which parameters to log
return !parameter.Attributes.Any(a => a.Type.Name == "SensitiveAttribute");
}
public override dynamic? OverrideMethod()
{
foreach (var param in meta.Target.Parameters)
{
if (ShouldLogParameter(param)) // Compile-time call
{
Console.WriteLine($" {param.Name} = {param.Value}");
}
}
return meta.Proceed();
}
}
常見模式
Try/Finally(保證清理)
public override dynamic? OverrideMethod()
{
var resource = AcquireResource();
try
{
return meta.Proceed();
}
finally
{
ReleaseResource(resource);
}
}
條件式執行
public override dynamic? OverrideMethod()
{
if (!IsAuthorized())
{
throw new UnauthorizedAccessException();
}
return meta.Proceed();
}
值轉換
public override dynamic? OverrideMethod()
{
var result = meta.Proceed();
// Transform the result before returning
return TransformResult(result);
}
吞掉並替換
public override dynamic? OverrideMethod()
{
try
{
return meta.Proceed();
}
catch (SpecificException)
{
return default; // Swallow exception, return default
}
}
常見陷阱
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
對引入的成員使用 nameof() | nameof() 在 Aspect 的編譯期解析,而非目標的編譯期 | 使用字串常值 |
| 在複雜運算式中混合編譯期與執行期 | 編譯器可能無法正確推斷作用域 | 使用 meta.CompileTime() 或 meta.RunTime() 來明確指定 |
在範本中使用 Debugger.Break() | Debugger.Break() 會被輸出為執行期程式碼 | 使用 meta.DebugBreak() 進行編譯期中斷點 |
假設對參數的 foreach 有執行期開銷 | 編譯期 foreach 會被展開 — 執行期不存在迴圈 | 這其實是一個特性,而非陷阱 |
| 將編譯期值存放在實例欄位中 | Aspect 實例在執行期不存在 | 使用 meta.Tags 從 BuildAspect() 傳遞資料到範本 |
總結
| 特性 | 說明 |
|---|---|
| T# | 具有編譯期語意的 C# 語法 |
meta.Proceed() | 呼叫原始方法 |
meta.Target | 存取目標宣告的中繼資料 |
meta.This/meta.Base | 動態實例存取 |
meta.Tags | 從 BuildAspect 傳遞資料到範本 |
編譯期 if | 只有符合的分支會被產生 |
編譯期 foreach | 迴圈在編譯期展開 |
dynamic? 回傳 | 適用於任何方法簽章的通用回傳型別 |
[Template] | 將方法標記為可重複使用的範本 |
[CompileTime] | 將輔助方法標記為僅限編譯期 |
下一篇:Aspect 基底類別 — 各基底類別的詳細指南。