教學課程:撰寫自定義字串插補處理程式
在本教學課程中,您將瞭解如何:
- 實作字串插補處理程式模式
- 在字串插補作業中與接收者互動。
- 將參數新增至字串插值處理程式
- 瞭解字串插補的新函式庫功能
先決條件
您必須設定電腦以執行 .NET。 C# 編譯程式適用於 Visual Studio 2022 或 .NET SDK。
本教學課程假設您已熟悉 C# 和 .NET,包括 Visual Studio 或 .NET CLI。
您可以撰寫自訂 插補字串處理程式。 插值字串處理器是負責處理插值字串中佔位符表達式的類型。 若沒有自訂處理程式,佔位符的處理方式會與 String.Format相似。 每個佔位元都會格式化為文字,然後串連元件以形成產生的字串。
您可以針對使用所產生字串相關信息的任何案例撰寫處理程式。 會使用嗎? 格式有哪些約束? 一些範例包括:
- 您可能會需要確保所有產生的字串不超過某些限制,例如80個字元。 您可以處理插補字串以填滿固定長度緩衝區,並在達到該緩衝區長度之後停止處理。
- 您可能有表格格式,而且每個佔位符都必須有固定長度。 自定義的處理程式可以實施規則,而不是強迫所有客戶端程式碼都要遵守。
在本教學課程中,您會為其中一個核心效能案例建立字串插補處理程式:日誌庫。 根據設定的記錄層級,不需要建構記錄訊息的工作。 如果日誌功能關閉,就不需要執行從插值字串表達式建構字串的工作。 訊息永遠不會列印,因此可以略過任何字串串連。 此外,佔位元中使用的任何表達式,包括產生堆疊追蹤,都是不必做的。
插值字串處理器可以判斷格式化字串是否會被使用,並且僅在需要時執行必要的工作。
初始實作
讓我們從支援不同層級的基本 Logger
類別開始:
public enum LogLevel
{
Off,
Critical,
Error,
Warning,
Information,
Trace
}
public class Logger
{
public LogLevel EnabledLevel { get; init; } = LogLevel.Error;
public void LogMessage(LogLevel level, string msg)
{
if (EnabledLevel < level) return;
Console.WriteLine(msg);
}
}
此 Logger
支援六個不同的層級。 當訊息未通過記錄層級篩選時,沒有輸出。 記錄器公用 API 接受 (完全格式化) 字串做為訊息。 建立字串的所有工作都已完成。
實作處理程式模式
此步驟是建立 插補字串處理程式, 重新建立目前的行為。 插補字串處理程式是必須具有下列特性的類型:
- 套用至類型的 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute。
- 具有兩個
int
參數的建構函式,literalLength
和formattedCount
。 (允許更多參數)。 - 具有簽章的公用
AppendLiteral
方法:public void AppendLiteral(string s)
。 - 具有簽章的泛型公用
AppendFormatted
方法:public void AppendFormatted<T>(T t)
。
在內部,建立器會建立格式化字串,並提供成員供用戶端擷取該字串。 以下程式碼顯示符合這些要求的 LogInterpolatedStringHandler
類型:
[InterpolatedStringHandler]
public ref struct LogInterpolatedStringHandler
{
// Storage for the built-up string
StringBuilder builder;
public LogInterpolatedStringHandler(int literalLength, int formattedCount)
{
builder = new StringBuilder(literalLength);
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}
public void AppendLiteral(string s)
{
Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
builder.Append(s);
Console.WriteLine($"\tAppended the literal string");
}
public void AppendFormatted<T>(T t)
{
Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
builder.Append(t?.ToString());
Console.WriteLine($"\tAppended the formatted object");
}
internal string GetFormattedText() => builder.ToString();
}
您現在可以將多載新增至 Logger
類別中的 LogMessage
,以嘗試新的插入字串處理程式:
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
您不需要移除原始 LogMessage
方法,當參數是插值字串表達式時,編譯程式會較偏好使用具有插值處理器參數的方法,而不是具有 string
參數的方法。
您可以使用下列程式代碼作為主要程式,確認已叫用新的處理程式:
var logger = new Logger() { EnabledLevel = LogLevel.Warning };
var time = DateTime.Now;
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time}. This won't be printed.");
logger.LogMessage(LogLevel.Warning, "Warning Level. This warning is a string, not an interpolated string expression.");
執行應用程式會產生類似下列文字的輸出:
literal length: 65, formattedCount: 1
AppendLiteral called: {Error Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This is an error. It will be printed.}
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: {Trace Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This won't be printed.}
Appended the literal string
Warning Level. This warning is a string, not an interpolated string expression.
透過輸出追蹤,您可以看到編譯程式如何新增程式代碼來呼叫處理程式並建置字串:
- 編譯程式會新增呼叫來建構處理程式,以格式字串傳遞常值文字的總長度,以及佔位符的數目。
- 編譯程式會針對字面值字串的每個區段以及每個佔位符,將呼叫加入至
AppendLiteral
和AppendFormatted
。 - 編譯程式會使用
CoreInterpolatedStringHandler
作為 自變數,叫用LogMessage
方法。
最後,請注意,最後一個警告不會叫用插入字串處理程式。 自變數是 string
,因此呼叫會使用字串參數叫用其他多載。
重要
本節的 Logger
版本是 ref struct
。
ref struct
會最小化配置,因為它必須儲存在堆疊上。 不過,一般而言,ref struct
類型無法實作介面。 這可能會導致使用 ref struct
類型實作的單元測試框架和模擬類型的相容性問題。
將更多功能新增至處理程式
上述版本的插值字串處理器會實現該模式。 若要避免處理每個佔位元表示式,您需要處理程式中的詳細資訊。 在本節中,您會改善處理程式,使其在建構的字串未寫入記錄檔時執行的工作較少。 使用 System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute 來指定公用 API 參數與處理程式建構函式參數之間的對應。 這提供了處理器必要的資訊來判斷是否應評估插值字串。
讓我們從處理程式的變更開始。 首先,新增欄位以追蹤處理程式是否已啟用。 將兩個參數新增至建構子:一個指定此訊息的日誌層級,另一個是日誌物件的參考:
private readonly bool enabled;
public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel)
{
enabled = logger.EnabledLevel >= logLevel;
builder = new StringBuilder(literalLength);
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}
接下來,使用欄位,讓處理程式只會在使用最終字串時附加常值或格式化物件:
public void AppendLiteral(string s)
{
Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
if (!enabled) return;
builder.Append(s);
Console.WriteLine($"\tAppended the literal string");
}
public void AppendFormatted<T>(T t)
{
Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
if (!enabled) return;
builder.Append(t?.ToString());
Console.WriteLine($"\tAppended the formatted object");
}
接下來,您必須更新 LogMessage
宣告,讓編譯程式將其他參數傳遞至處理程式的建構函式。 這是使用處理程式自變數上的 System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute 來處理:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
這個屬性指定了一個自變數清單,這些自變數對應於在必要的 literalLength
和 formattedCount
參數之後的 LogMessage
參數。 空字串 (“”),會指定接收者。 編譯程式會將 this
所表示之 Logger
物件的值取代為處理程式建構函式的下一個自變數。 編譯程式會將 level
的值取代為下列自變數。 您可以針對您撰寫的任何處理程式提供任意數目的自變數。 您新增的自變數是字串自變數。
您可以使用相同的測試程式代碼來執行此版本。 這次,您會看到下列結果:
literal length: 65, formattedCount: 1
AppendLiteral called: {Error Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This is an error. It will be printed.}
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: {Trace Level. CurrentTime: }
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
AppendLiteral called: {. This won't be printed.}
Warning Level. This warning is a string, not an interpolated string expression.
您可以看到 AppendLiteral
和 AppendFormat
方法正被呼叫,但是沒有執行任何工作。 處理程序判斷不需要最終字串,因此處理程式不會建置它。 還有一些改進需要進行。
首先,您可以新增 AppendFormatted
的多載,將 自變數限制為實作 System.IFormattable的類型。 此多載可讓呼叫端在佔位元中新增格式字串。 進行這項變更時,我們也會將其他 AppendFormatted
和 AppendLiteral
方法的傳回型別從 void
變更為 bool
(如果其中任一種方法有不同的傳回類型,您會收到編譯錯誤)。 該變更啟用了 對的短路。 方法會傳回 false
,表示應該停止插補字串表達式的處理。 傳回 true
表示應該繼續。 在此範例中,當產生的字串不需要時,您可以使用它來停止處理。 縮短線路支援更精細的動作。 一旦表達式達到特定長度,您就可以停止處理運算式,以支援固定長度的緩衝區。 或者有些條件可能表示不需要剩餘的元素。
public void AppendFormatted<T>(T t, string format) where T : IFormattable
{
Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with format {{{format}}} is of type {typeof(T)},");
builder.Append(t?.ToString(format, null));
Console.WriteLine($"\tAppended the formatted object");
}
透過該添加,您可以在插值字串表示式中指定格式字串。
var time = DateTime.Now;
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. The time doesn't use formatting.");
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time:t}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time:t}. This won't be printed.");
第一則訊息中的 :t
指定了當前時間的「簡短時間格式」。 上一個範例顯示您可以為處理程式建立的 AppendFormatted
方法的其中一個多載。 您不需要為格式化的物件指定泛型自變數。 您可能有更有效率的方式可將您建立的類型轉換成字串。 您可以撰寫 AppendFormatted
的多載,以便採用這些類型的參數,而非使用泛型參數。 編譯器會挑選最佳的多載方法。 運行時會使用這項技術將 System.Span<T> 轉換成字串輸出。 您可以新增整數參數來指定輸出 的
立即執行範例,您會看到針對 Trace
訊息,只會呼叫第一個 AppendLiteral
:
literal length: 60, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted called: 10/20/2021 12:18:29 PM is of type System.DateTime
Appended the formatted object
AppendLiteral called: . The time doesn't use formatting.
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:18:29 PM. The time doesn't use formatting.
literal length: 65, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted (IFormattable version) called: 10/20/2021 12:18:29 PM with format {t} is of type System.DateTime,
Appended the formatted object
AppendLiteral called: . This is an error. It will be printed.
Appended the literal string
Error Level. CurrentTime: 12:18 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: Trace Level. CurrentTime:
Warning Level. This warning is a string, not an interpolated string expression.
您可以對處理程式的建構函式進行最後一次更新,以改善效率。 處理程式可以新增最終 out bool
參數。 將參數設定為 false
表示完全不應該呼叫處理程式來處理插補字串表示式:
public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel level, out bool isEnabled)
{
isEnabled = logger.EnabledLevel >= level;
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
builder = isEnabled ? new StringBuilder(literalLength) : default!;
}
該變更表示您可以移除 [enabled
] 字段。 然後,您可以將傳回類型的 AppendLiteral
和 AppendFormatted
變更為 void
。
現在,當您執行範例時,您會看到下列輸出:
literal length: 60, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted called: 10/20/2021 12:19:10 PM is of type System.DateTime
Appended the formatted object
AppendLiteral called: . The time doesn't use formatting.
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. The time doesn't use formatting.
literal length: 65, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted (IFormattable version) called: 10/20/2021 12:19:10 PM with format {t} is of type System.DateTime,
Appended the formatted object
AppendLiteral called: . This is an error. It will be printed.
Appended the literal string
Error Level. CurrentTime: 12:19 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
Warning Level. This warning is a string, not an interpolated string expression.
指定 LogLevel.Trace
時的唯一輸出是建構函式的輸出。 處理程式表示未啟用,因此未叫用任何 Append
方法。
此範例說明內插字串處理程式的重要點,特別是在使用日誌記錄庫時。 佔位元中的任何副作用都可能不會發生。 將下列程式代碼新增至您的主要程式,並查看此行為的運作方式:
int index = 0;
int numberOfIncrements = 0;
for (var level = LogLevel.Critical; level <= LogLevel.Trace; level++)
{
Console.WriteLine(level);
logger.LogMessage(level, $"{level}: Increment index a few times {index++}, {index++}, {index++}, {index++}, {index++}");
numberOfIncrements += 5;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");
您可以看到在迴圈的每次迭代中,index
變數會遞增一次,總計五次。 由於占位元只會針對 Critical
、Error
和 Warning
層級進行評估,而不是針對 Information
和 Trace
,因此 index
的最終值不符合預期:
Critical
Critical: Increment index a few times 0, 1, 2, 3, 4
Error
Error: Increment index a few times 5, 6, 7, 8, 9
Warning
Warning: Increment index a few times 10, 11, 12, 13, 14
Information
Trace
Value of index 15, value of numberOfIncrements: 25
內插字串處理程式提供更大的控制權,決定內插字串如何轉換成字串。 .NET 運行時間小組使用這項功能來改善數個領域的效能。 您可以在自己的程式庫中使用相同的功能。 若要進一步探索,請參閱 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler。 它提供比您在這裡建置的更完整的實作。 您會看到更多適用於 Append
方法的多載。