在日誌記錄、異常診斷、性能分析、審計追蹤等場景中,自動獲取調用位置信息(方法名、源文件行號、文件路徑)至關重要。C# 提供兩種截然不同的方案:運行時 StackTrace 與編譯期 Caller Information 屬性。本文深度對比兩者原理、性能、適用場景並給出生產級封裝代碼。
using System.Diagnostics;
public void LogWithStackTrace() {
var st = new StackTrace(true); // true = 捕獲文件信息(需 PDB 文件)
var frame = st.GetFrame(1); // 0=當前方法,1=調用者
Console.WriteLine($"方法: {frame.GetMethod().Name}");
Console.WriteLine($"文件: {frame.GetFileName()}");
Console.WriteLine($"行號: {frame.GetFileLineNumber()}");
Console.WriteLine($"列號: {frame.GetFileColumnNumber()}");
}
| 參數/屬性 | 說明 |
|---|---|
new StackTrace(true) | fNeedFileInfo=true,需編譯時生成 PDB(Debug 模式默認,Release 需設置 DebugType=portable 或 full) |
GetFrame(0) | 當前方法幀;GetFrame(1) 為直接調用者;以此類推 |
GetMethod() | 返回 MethodBase,含 Name、DeclaringType、Module、IsStatic 等 |
GetFileName() | 返回源文件絕對路徑(編譯時路徑),Release 模式下可能為 null |
GetFileLineNumber() | 源碼行號,需 PDB 支持 |
GetFileColumnNumber() | 列號,較少用 |
GetFrame(1) 可能非預期調用者。可用 [MethodImpl(MethodImplOptions.NoInlining)] 防止。<DebugType>portable</DebugType> 或 <DebugSymbols>true</DebugSymbols>。/home/...,容器化部署路徑可能無意義。C# 5.0+ 引入,編譯器在調用點自動填充參數值,**零運行時開銷**,**不受內聯/PDB 影響**。
| 屬性 | 類型 | 注入值 |
|---|---|---|
[CallerMemberName] | string | 調用者方法/屬性名(構造函數為 .ctor,屬性為 get_Name/set_Name) |
[CallerFilePath] | string | 調用者源文件完整路徑(編譯時路徑) |
[CallerLineNumber] | int | 調用者源碼行號 |
using System.Runtime.CompilerServices;
public static class LogHelper {
public static void Log(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0) {
// 可選:只保留文件名
var fileName = Path.GetFileName(filePath);
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [{fileName}:{lineNumber} {memberName}] {message}");
}
// 異步版本
public static async Task LogAsync(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0) {
// 寫入文件/發送到日誌服務...
await Task.CompletedTask;
}
}
// 業務代碼調用極其簡潔
public class OrderService {
public void PlaceOrder(OrderDto dto) {
LogHelper.Log("開始下單"); // 自動注入: memberName="PlaceOrder", lineNumber=XX
// ...
LogHelper.Log("下單完成");
}
}
CallerMemberName 不包含類名。結合 StackTrace 輕量級獲取(僅 1 幀):
public static void LogWithType(string message,
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0) {
string typeName = "";
// 僅獲取第 1 幀(調用者),不捕獲文件信息,性能可接受
var frame = new StackTrace(false).GetFrame(1);
if (frame?.GetMethod()?.DeclaringType != null) {
typeName = frame.GetMethod().DeclaringType.Name;
}
Console.WriteLine($"[{typeName}.{member}:{line}] {message}");
}
public static class StructuredLogger {
// 結構化日誌,便於 ELK/Splunk 解析
public static void Info(string eventName, object? payload = null,
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0) {
var log = new {
Timestamp = DateTimeOffset.UtcNow,
Level = "INFO",
Event = eventName,
Caller = new {
Member = member,
File = Path.GetFileName(file),
Line = line
},
Payload = payload
};
// 序列化為 JSON 寫入文件/發送到 Kafka/Seq/Elasticsearch
var json = System.Text.Json.JsonSerializer.Serialize(log);
File.AppendAllText("app.log", json + Environment.NewLine);
}
}
| 場景 | 推薦方案 | 理由 |
|---|---|---|
| 高頻業務日誌(每秒千次+) | Caller Information | 零開銷,編譯期注入,不影響吞吐 |
| 異常堆棧捕獲、完整調用鏈 | StackTrace | 需完整堆棧上下文,Caller 只能獲取直接調用者 |
| 第三方庫/無法修改調用點代碼 | StackTrace | 無需調用方配合屬性參數 |
| 單元測試斷言輔助 | Caller Information | 精準定位失敗行,輸出友好信息 |
| 性能熱點分析 | StackTrace + 采樣 | 統計方法調用頻率、耗時分布 |
| 分布式追蹤 | Caller Information + TraceId | 關聯本地調用位置與全鏈路 TraceId |
<DebugType>portable</DebugType> 使 StackTrace 在生產環境同樣可用,文件大小極小。合理組合兩套方案,構建「開發時可見、生產時可查、性能無損耗」的可觀測體系,是高質量 .NET 應用的標配。