跳至主要内容

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.MethodIMethod目標方法
meta.Target.Method.Namestring方法名稱(編譯期常數)
meta.Target.Method.ReturnTypeIType回傳型別
meta.Target.ParametersIParameterList方法參數
meta.Target.TypeINamedType包含的型別
meta.Target.FieldOrPropertyIFieldOrProperty目標欄位/屬性(在屬性 Aspect 中)
meta.Target.ConstructorIConstructor目標建構函式

meta.Thismeta.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()
寫出 ILTemplate 與 meta.* 在輸出中完全消失,只剩展開後的執行期 C# / IL

編譯期運算式

Metalama 編譯器會根據每個運算式的型別和用法來判斷其作用域。

編譯期型別

任何涉及以下型別的運算式都是編譯期的:

  • IMethodITypeIParameterIFieldIProperty(程式碼模型介面)
  • IExpressionIStatement(範本建構區塊)
  • 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();
}

限制:與 refout 一起使用的欄位無法提升為屬性。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.TagsBuildAspect() 傳遞資料到範本

總結

特性說明
T#具有編譯期語意的 C# 語法
meta.Proceed()呼叫原始方法
meta.Target存取目標宣告的中繼資料
meta.This/meta.Base動態實例存取
meta.Tags從 BuildAspect 傳遞資料到範本
編譯期 if只有符合的分支會被產生
編譯期 foreach迴圈在編譯期展開
dynamic? 回傳適用於任何方法簽章的通用回傳型別
[Template]將方法標記為可重複使用的範本
[CompileTime]將輔助方法標記為僅限編譯期

下一篇Aspect 基底類別 — 各基底類別的詳細指南。