共用方式為


教學課程:撰寫自定義字串插補處理程式

在本教學課程中,您將瞭解如何:

  • 實作字串插補處理程式模式
  • 在字串插補作業中與接收者互動。
  • 將參數新增至字串插值處理程式
  • 瞭解字串插補的新函式庫功能

先決條件

您必須設定電腦以執行 .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 參數的建構函式,literalLengthformattedCount。 (允許更多參數)。
  • 具有簽章的公用 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.

透過輸出追蹤,您可以看到編譯程式如何新增程式代碼來呼叫處理程式並建置字串:

  • 編譯程式會新增呼叫來建構處理程式,以格式字串傳遞常值文字的總長度,以及佔位符的數目。
  • 編譯程式會針對字面值字串的每個區段以及每個佔位符,將呼叫加入至 AppendLiteralAppendFormatted
  • 編譯程式會使用 CoreInterpolatedStringHandler 作為 自變數,叫用 LogMessage 方法。

最後,請注意,最後一個警告不會叫用插入字串處理程式。 自變數是 string,因此呼叫會使用字串參數叫用其他多載。

重要

本節的 Logger 版本是 ref structref 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());
}

這個屬性指定了一個自變數清單,這些自變數對應於在必要的 literalLengthformattedCount 參數之後的 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.

您可以看到 AppendLiteralAppendFormat 方法正被呼叫,但是沒有執行任何工作。 處理程序判斷不需要最終字串,因此處理程式不會建置它。 還有一些改進需要進行。

首先,您可以新增 AppendFormatted 的多載,將 自變數限制為實作 System.IFormattable的類型。 此多載可讓呼叫端在佔位元中新增格式字串。 進行這項變更時,我們也會將其他 AppendFormattedAppendLiteral 方法的傳回型別從 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> 轉換成字串輸出。 您可以新增整數參數來指定輸出 的 對齊方式,這可以選擇性地包括或不包括 。 隨附於 .NET 6 的 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler 包含不同用途的九個 AppendFormatted 多載。 您可以參考它來建立符合您目的的處理程序。

立即執行範例,您會看到針對 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] 字段。 然後,您可以將傳回類型的 AppendLiteralAppendFormatted 變更為 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 變數會遞增一次,總計五次。 由於占位元只會針對 CriticalErrorWarning 層級進行評估,而不是針對 InformationTrace,因此 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 方法的多載。