Kurz: Zápis vlastní obslužné rutiny interpolace řetězců

V tomto kurzu se naučíte:

  • Implementace vzoru obslužné rutiny interpolace řetězců
  • Interakce s příjemcem v operaci interpolace řetězců
  • Přidání argumentů do obslužné rutiny interpolace řetězců
  • Vysvětlení nových funkcí knihovny pro interpolaci řetězců

Požadavky

Budete muset nastavit počítač tak, aby běžel .NET 6, včetně kompilátoru C# 10. Kompilátor C# 10 je k dispozici od sady Visual Studio 2022 nebo .NET 6 SDK.

V tomto kurzu se předpokládá, že znáte C# a .NET, včetně sady Visual Studio nebo rozhraní příkazového řádku .NET.

Nová osnova

C# 10 přidává podporu pro vlastní interpolovanou obslužnou rutinu řetězců. Interpolovaná obslužná rutina řetězce je typ, který zpracovává zástupný výraz v interpolovaném řetězci. Bez vlastní obslužné rutiny se zástupné symboly zpracovávají podobně jako String.Format. Každý zástupný symbol je naformátovaný jako text a potom jsou komponenty zřetězeny tak, aby vytvořily výsledný řetězec.

Obslužnou rutinu můžete napsat pro libovolný scénář, ve kterém použijete informace o výsledném řetězci. Použije se? Jaká omezení jsou ve formátu? Mezi některé příklady patří:

  • Můžete vyžadovat, aby žádný z výsledných řetězců nebyl větší než nějaký limit, například 80 znaků. Interpolované řetězce můžete zpracovat a vyplnit vyrovnávací paměť s pevnou délkou a po dosažení této délky vyrovnávací paměti zastavit zpracování.
  • Je možné, že máte tabulkový formát a každý zástupný symbol musí mít pevnou délku. Vlastní obslužná rutina může tuto rutinu vynutit, a ne vynutit, aby byl veškerý kód klienta v souladu.

V tomto kurzu vytvoříte obslužnou rutinu interpolace řetězců pro jeden ze základních scénářů výkonu: knihovny protokolování. V závislosti na nakonfigurované úrovni protokolu není potřeba vytvořit zprávu protokolu. Pokud je protokolování vypnuté, není potřeba vytvořit řetězec z interpolovaného řetězcového výrazu. Zpráva se nikdy nevytiskne, takže je možné vynechat zřetězení řetězce. Kromě toho není nutné provádět všechny výrazy použité v zástupných symbolech, včetně generování trasování zásobníku.

Interpolovaná obslužná rutina řetězce může určit, jestli se formátovaný řetězec použije, a v případě potřeby provést pouze potřebnou práci.

Počáteční implementace

Začněme základní Logger třídou, která podporuje různé úrovně:

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);
    }
}

To Logger podporuje šest různých úrovní. Pokud zpráva nepřejde filtr na úrovni protokolu, neexistuje žádný výstup. Veřejné rozhraní API pro protokolovací modul přijímá jako zprávu řetězec (plně formátovaný). Všechna práce na vytvoření řetězce už byla provedena.

Implementace vzoru obslužné rutiny

Tento krok spočívá v vytvoření interpolované obslužné rutiny řetězce, která znovu vytvoří aktuální chování. Interpolovaná obslužná rutina řetězce je typ, který musí mít následující vlastnosti:

  • Použitý System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute u typu.
  • Konstruktor, který má dva int parametry, literalLength a formattedCount. (Jsou povoleny další parametry).
  • Veřejná AppendLiteral metoda s podpisem: public void AppendLiteral(string s).
  • Obecná veřejná AppendFormatted metoda s podpisem: public void AppendFormatted<T>(T t).

Tvůrce interně vytvoří formátovaný řetězec a poskytne klientovi člena pro načtení daného řetězce. Následující kód ukazuje LogInterpolatedStringHandler typ, který splňuje tyto požadavky:

[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();
}

Teď můžete do LogMessageLogger třídy přidat přetížení a vyzkoušet novou interpolovanou obslužnou rutinu řetězců:

public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

Původní metodu LogMessage nemusíte odebírat, kompilátor dává přednost metodě s interpolovaným parametrem obslužné rutiny před metodou s parametrem string , pokud je argument interpolovaným řetězcovým výrazem.

Novou obslužnou rutinu můžete ověřit pomocí následujícího kódu jako hlavního programu:

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.");

Spuštění aplikace vytvoří výstup podobný následujícímu textu:

        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.

Trasování výstupem můžete zjistit, jak kompilátor přidá kód pro volání obslužné rutiny a sestavení řetězce:

  • Kompilátor přidá volání pro vytvoření obslužné rutiny, předání celkové délky literálového textu ve formátovacím řetězci a počtu zástupných symbolů.
  • Kompilátor přidává volání do AppendLiteral a AppendFormatted pro každou část literálového řetězce a pro každý zástupný symbol.
  • Kompilátor vyvolá metodu LogMessage pomocí argumentu CoreInterpolatedStringHandler .

Nakonec si všimněte, že poslední upozornění nevolá interpolovanou obslužnou rutinu řetězce. Argument je , stringtakže volání vyvolá druhé přetížení s řetězcový parametr.

Přidání dalších funkcí do obslužné rutiny

Předchozí verze interpolované obslužné rutiny řetězců implementuje vzor. Abyste se vyhnuli zpracování každého zástupného výrazu, budete potřebovat další informace v obslužné rutině. V této části vylepšíte obslužnou rutinu tak, aby méně fungovala, když se vytvořený řetězec nebude zapisovat do protokolu. Slouží System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute k určení mapování mezi parametry na veřejné rozhraní API a parametry na konstruktor obslužné rutiny. To poskytuje obslužné rutině informace potřebné k určení, zda má být interpolovaný řetězec vyhodnocen.

Začněme změnami obslužné rutiny. Nejprve přidejte pole ke sledování, jestli je obslužná rutina povolená. Přidejte do konstruktoru dva parametry: jeden pro zadání úrovně protokolu pro tuto zprávu a druhý odkaz na objekt protokolu:

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}");
}

Dále použijte pole, aby obslužná rutina připojila pouze literály nebo naformátované objekty, když se použije konečný řetězec:

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");
}

Dále budete muset aktualizovat LogMessage deklaraci tak, aby kompilátor předal další parametry konstruktoru obslužné rutiny. To se zpracovává pomocí argumentu System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute obslužné rutiny:

public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

Tento atribut určuje seznam argumentů, které se LogMessage mapují na parametry, které následují za požadovanými literalLength parametry a formattedCount parametry. Prázdný řetězec (""), určuje příjemce. Kompilátor nahradí hodnotu objektu Logger reprezentovanou dalším argumentem this konstruktoru obslužné rutiny. Kompilátor nahradí hodnotu level následujícího argumentu. Můžete zadat libovolný počet argumentů pro jakoukoli obslužnou rutinu, kterou napíšete. Argumenty, které přidáte, jsou řetězcové argumenty.

Tuto verzi můžete spustit pomocí stejného testovacího kódu. Tentokrát uvidíte následující výsledky:

        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.

Vidíte, že se AppendLiteral volají metody a AppendFormat metody, ale neprovádí žádnou práci. Obslužná rutina zjistila, že konečný řetězec nebude potřeba, takže obslužná rutina ho nevytvoře. Stále existuje několik vylepšení.

Nejprve můžete přidat přetížení AppendFormatted tohoto omezení argumentu na typ, který implementuje System.IFormattable. Toto přetížení umožňuje volajícím přidávat do zástupných symbolů formátovací řetězce. Při provádění této změny změníme také návratový typ ostatních AppendFormatted metod a AppendLiteral metod z void toho bool (pokud některé z těchto metod mají různé návratové typy, zobrazí se chyba kompilace). Tato změna umožňuje zkratování. Metody se vrátí false k označení, že zpracování interpolovaného řetězcového výrazu by mělo být zastaveno. Návrat true znamená, že by měl pokračovat. V tomto příkladu ho používáte k zastavení zpracování v případě, že výsledný řetězec není potřeba. Zkratování podporuje jemněji odstupňované akce. Jakmile výraz dosáhne určité délky, můžete zastavit zpracování výrazu, aby podporoval vyrovnávací paměti s pevnou délkou. Nebo některá podmínka může znamenat, že zbývající prvky nejsou potřeba.

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");
}

Kromě toho můžete v interpolovaném řetězci zadat formátovací řetězce:

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.");

První :t zpráva určuje "krátký formát času" pro aktuální čas. Předchozí příklad ukázal jeden z přetížení metody AppendFormatted , kterou můžete vytvořit pro svou obslužnou rutinu. Pro formátovaný objekt nemusíte zadávat obecný argument. Možná máte efektivnější způsoby převodu typů, které vytvoříte na řetězec. Můžete napsat přetížení AppendFormatted , která přebírá tyto typy místo obecného argumentu. Kompilátor vybere nejlepší přetížení. Modul runtime používá tuto techniku k převodu System.Span<T> na výstup řetězce. Můžete přidat celočíselnou parametr pro určení zarovnání výstupu, s nebo bez něj IFormattable. Ta System.Runtime.CompilerServices.DefaultInterpolatedStringHandler , která se dodává s .NET 6, obsahuje devět přetížení AppendFormatted pro různá použití. Můžete ho použít jako referenci při vytváření obslužné rutiny pro vaše účely.

Spusťte ukázku a uvidíte, že pro Trace zprávu se volá pouze první 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.

Můžete provést jednu konečnou aktualizaci konstruktoru obslužné rutiny, která zvyšuje efektivitu. Obslužná rutina může přidat konečný out bool parametr. Nastavením parametru false na označení, že obslužná rutina by neměla být volána vůbec kvůli zpracování interpolovaného řetězcového výrazu:

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!;
}

Tato změna znamená, že pole můžete odebrat enabled . Potom můžete změnit návratový AppendLiteral typ a AppendFormatted na voidhodnotu . Když teď ukázku spustíte, uvidíte následující výstup:

        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.

Jediným výstupem při LogLevel.Trace zadání je výstup z konstruktoru. Obslužná rutina značila, že není povolená, takže nebyla vyvolána žádná z Append metod.

Tento příklad ukazuje důležitý bod pro interpolované obslužné rutiny řetězců, zejména při použití knihoven protokolování. U zástupných symbolů nemusí dojít k žádným vedlejším efektům. Do hlavního programu přidejte následující kód a podívejte se na toto chování v akci:

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}");

Vidíte, že index proměnná se zvýší o pětkrát každou iteraci smyčky. Vzhledem k tomu, že zástupné symboly se vyhodnocují pouze pro Criticala ErrorWarning úrovně, nikoli pro Information a Trace, konečná hodnota index neodpovídá očekávání:

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

Interpolované obslužné rutiny řetězců poskytují větší kontrolu nad tím, jak je interpolovaný řetězcový výraz převeden na řetězec. Tým modulu runtime .NET už tuto funkci použil ke zlepšení výkonu v několika oblastech. Stejnou funkci můžete využít ve svých vlastních knihovnách. Chcete-li prozkoumat dále, podívejte se na System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Poskytuje ucelenější implementaci, než jste zde vytvořili. Uvidíte mnoho dalších přetížení, která jsou pro tyto Append metody možná.