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
aformattedCount
. (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
aAppendFormatted
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.