Sdílet prostřednictvím


Návod: Jak napsat vlastní obslužnou rutinu pro interpolaci řetězců

V tomto kurzu se naučíte:

  • Implementovat vzorec obslužné rutiny interpolace řetězců
  • Interagujte s přijímačem v operaci řetězcové interpolace.
  • Přidejte argumenty do handleru interpolace řetězců
  • Seznamte se s novými funkcemi knihovny pro interpolaci řetězců

Požadavky

Musíte nastavit počítač tak, aby běžel na platformě .NET. Kompilátor jazyka C# je k dispozici se sadou Visual Studio 2022 nebo s .NET 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.

Můžete napsat vlastní obsluhu interpolovaných řetězců . Interpolovaná obslužná rutina řetězce je typ, který zpracovává zástupný výraz v interpolovaném řetězci. Bez vlastního handleru se zástupné symboly zpracovávají stejně 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 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ů. Můžete zpracovat interpolované řetězce, aby vyplnily vyrovnávací paměť s pevnou délkou, a zpracování zastavit, jakmile je této délky dosaženo.
  • 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 podmínku vynutit, místo aby vynucovala, že veškerý kód klienta musí být 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);
    }
}

Tato Logger podporuje šest různých úrovní. Pokud zpráva nepřejde filtrem 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á ve vytvoření interpolované obslužné rutiny pro řetězce, která reprodukuje aktuální chování. Handlery interpolovaných řetězců jsou typy, které musí mít následující vlastnosti:

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

Tvůrce interně vytvoří formátovaný řetězec a poskytne klientovi metodu pro načtení tohoto řetězce. Následující kód ukazuje typ LogInterpolatedStringHandler, 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 definovat přetížení pro LogMessage ve třídě Logger, abyste vyzkoušeli nový interpolovaný obslužný mechanismus řetězce.

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.

Při procházení výstupem můžete vidět, jak kompilátor přidává kód k volání obslužné rutiny a sestavení řetězce.

  • Kompilátor přidá volání pro vytvoření zpracovatele, přičemž předá celkovou délku literálového textu ve formátovacím řetězci a počet zástupných symbolů.
  • Kompilátor přidá volání 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í CoreInterpolatedStringHandler jako argumentu.

Nakonec si všimněte, že poslední upozornění nevyužívá interpolovanou obslužnou rutinu řetězce. Argument je string, takže volání vyvolá druhou přetíženou funkci s řetězcovým parametrem.

Důležitý

Verze Logger pro tuto část je ref struct. ref struct minimalizuje alokace paměti, protože musí být uloženo v zásobníku. Obecně ale ref struct typy nemůžou implementovat rozhraní. Může to způsobit problémy s kompatibilitou pro jednotkové testovací frameworky a mockovací typy, které k implementaci používají typy ref struct.

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

Předchozí verze obslužné rutiny pro interpolované řetězce implementuje vzor. Abyste se vyhnuli zpracování každého zástupného výrazu, potřebujete další informace v obslužném programu. V této části vylepšíte obslužnou rutinu tak, aby méně fungovala, když se vytvořený řetězec nezapíše do protokolu. Pomocí System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute určíte mapování mezi parametry veřejného rozhraní API a parametry konstruktoru 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 ve Handleru. 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 je potřeba aktualizovat deklaraci LogMessage tak, aby kompilátor předává další parametry konstruktoru obslužné rutiny. To se zpracovává pomocí System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute na argumentu handleru:

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 mají LogMessage mapovat na parametry, které následují za požadovanými literalLength a formattedCount parametry. Prázdný řetězec (""), určuje příjemce. Kompilátor nahradí další argument konstruktoru obslužné rutiny hodnotou objektu Logger reprezentovanou pomocí this. Kompilátor nahradí hodnotu level následujícím argumentem. 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 se zobrazí 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 volají AppendLiteral a AppendFormat metody, ale neprovádí žádnou práci. Obslužná rutina zjistila, že konečný řetězec není 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, které omezuje argument 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 a AppendLiteral metod z void na bool (pokud některé z těchto metod mají jiné návratové typy, zobrazí se chyba kompilace). Tato změna umožňuje zkrat . Metody vracejí false označující, že zpracování interpolovaného řetězcového výrazu by mělo být zastaveno. Vrácení true značí, že se má 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. Můžete zastavit zpracování výrazu, jakmile dosáhne určité délky, a tím podpořit 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");
}

Díky tomuto rozšíření můžete ve výrazu interpolovaného řetězce specifikovat řetězce formátu.

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 v první zprávě označuje "krátký časový formát" pro aktuální čas. Předchozí příklad ukázal jedno z přetížení metody AppendFormatted, které 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 vytvářet přetížení AppendFormatted, která přijímají tyto typy místo generické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číselný parametr k určení zarovnání výstupu, s nebo bez IFormattable. System.Runtime.CompilerServices.DefaultInterpolatedStringHandler, která se dodává s rozhraním .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 zprávu Trace 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 poslední aktualizaci konstruktoru zpracovatele, která zvyšuje efektivitu. Obslužná rutina může přidat konečný parametr out bool. Nastavení parametru na false znamená, že obslužná rutina by neměla být volána vůbec, aby bylo možné zpracovat interpolovaný řetězcový výraz:

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 enabled můžete odebrat. Potom můžete změnit návratový typ AppendLiteral a AppendFormatted na void. Když teď ukázku spustíte, zobrazí se 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 zadání LogLevel.Trace je výstup z konstruktoru. Zpracovatel uvedl, že není povolen, takže nebyla vyvolána žádná z metod Append.

Tento příklad ukazuje důležitý bod pro interpolaci řetězců, zejména při použití knihoven pro logová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 proměnná index se zvýší o pětkrát každou iteraci smyčky. Vzhledem k tomu, že zástupné symboly se vyhodnocují jenom pro úrovně Critical, Error a Warning, 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é zpracovatele řetězců poskytují větší kontrolu nad tím, jak je interpolovaný výraz řetězce převeden na řetězec. Tým modulu runtime .NET 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. Pokud chcete prozkoumat další informace, 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 možné pro Append metody.