Samouczek: pisanie niestandardowej procedury obsługi interpolacji ciągów
Z tego samouczka dowiesz się, jak wykonywać następujące działania:
- Implementowanie wzorca obsługi interpolacji ciągów
- Wchodź w interakcję z odbiornikiem podczas operacji interpolacji ciągów.
- Dodawanie argumentów do obsługiwacza interpolacji ciągów
- Omówienie nowych funkcji bibliotek na potrzeby interpolacji ciągów
Warunki wstępne
Aby uruchomić platformę .NET, musisz skonfigurować maszynę. Kompilator języka C# jest dostępny w programie Visual Studio 2022 lub .NET SDK.
W tym samouczku założono, że znasz języki C# i .NET, w tym program Visual Studio lub interfejs wiersza polecenia platformy .NET.
Możesz napisać niestandardową interpolowaną procedurę obsługi ciągów. Obsługa ciągów interpolowanych to typ, który przetwarza wyrażenia zastępcze w ciągach interpolowanych. Bez niestandardowego programu obsługi symbole zastępcze są przetwarzane podobnie jak String.Format. Każdy symbol zastępczy jest sformatowany jako tekst, a następnie składniki są łączone w celu utworzenia wynikowego ciągu.
Możesz napisać procedurę obsługi dla dowolnego scenariusza, w którym są używane informacje o wynikowym ciągu. Czy będzie używany? Jakie ograniczenia dotyczą formatu? Oto kilka przykładów:
- Możesz wymagać, aby żaden z wynikowych ciągów znaków nie był większy niż określony limit, na przykład 80 znaków. Możesz przetworzyć ciągi interpolowane, aby wypełnić bufor o stałej długości i zatrzymać przetwarzanie po osiągnięciu tej długości buforu.
- Może istnieć format tabelaryczny, a każdy symbol zastępczy musi mieć stałą długość. Niestandardowy program obsługi może to wymusić, zamiast zmuszać cały kod klienta do dostosowania się.
W tym samouczku utworzysz mechanizm interpolacji ciągów w jednym z kluczowych scenariuszy wydajnościowych: bibliotek do logowania. W zależności od skonfigurowanego poziomu dziennika nie jest wymagana praca w celu utworzenia komunikatu dziennika. Jeśli rejestrowanie jest wyłączone, nie jest potrzebna praca w celu skonstruowania ciągu z wyrażenia ciągu interpolowanego. Komunikat nigdy nie jest drukowany, więc można pominąć łączenie ciągów. Ponadto nie ma potrzeby wykonywania żadnych wyrażeń umieszczonych w symbolach zastępczych, w tym również generowania śladów stosu.
Procedura obsługi ciągów interpolowanych może określić, czy zostanie użyty sformatowany ciąg i wykonać tylko niezbędną pracę w razie potrzeby.
Początkowa implementacja
Zacznijmy od podstawowej klasy Logger
, która obsługuje różne poziomy:
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);
}
}
Ta Logger
obsługuje sześć różnych poziomów. Jeśli komunikat nie przechodzi przez filtr poziomu dziennika, nie ma żadnego wyjścia. Publiczny interfejs API rejestratora akceptuje ciąg (w pełni sformatowany) jako komunikat. Wszystkie prace nad utworzeniem ciągu zostały już wykonane.
Zastosuj wzorzec obsługi
Ten krok polega na utworzeniu interpolowanej procedury obsługi ciągów, która ponownie tworzy bieżące zachowanie. Manipulator ciągów interpolowanych to typ, który musi mieć następujące cechy:
- System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute został zastosowany do typu.
- Konstruktor, który ma dwa parametry
int
,literalLength
iformattedCount
. (Dozwolone są więcej parametrów). - Publiczna metoda
AppendLiteral
z podpisem:public void AppendLiteral(string s)
. - Publiczna ogólna metoda
AppendFormatted
o sygnaturze:public void AppendFormatted<T>(T t)
.
Wewnętrznie konstruktor tworzy sformatowany ciąg i udostępnia go klientowi do pobrania. Poniższy kod przedstawia typ LogInterpolatedStringHandler
spełniający następujące wymagania:
[InterpolatedStringHandler]
public 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();
}
Teraz możesz dodać przeciążenie do LogMessage
w klasie Logger
, aby wypróbować nową procedurę obsługi ciągów interpolowanych.
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Nie musisz usuwać oryginalnej metody LogMessage
, kompilator preferuje metodę z interpolowanym parametrem obsługi dla metody z parametrem string
, gdy argument jest wyrażeniem ciągu interpolowanego.
Możesz sprawdzić, czy nowa procedura obsługi jest wywoływana przy użyciu następującego kodu jako głównego 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.");
Uruchomienie aplikacji generuje dane wyjściowe podobne do następującego tekstu:
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.
Śledzenie danych wyjściowych pozwala zobaczyć, jak kompilator dodaje kod w celu wywołania procedury obsługi i skompilowania ciągu:
- Kompilator dodaje wywołanie do konstruowania programu obsługi, przekazując całkowitą długość tekstu literału w ciągu formatu i liczbę symboli zastępczych.
- Kompilator dodaje wywołania do
AppendLiteral
iAppendFormatted
dla każdej sekcji ciągu literału i dla każdego symbolu zastępczego. - Kompilator wywołuje metodę
LogMessage
przy użyciuCoreInterpolatedStringHandler
jako argumentu.
Na koniec zwróć uwagę, że ostatnie ostrzeżenie nie wywołuje procedury obsługi ciągów interpolowanych. Argument jest string
, więc wywołanie wywołuje inne przeciążenie z parametrem ciągu.
Ważny
Użyj ref struct
do obsługi ciągów interpolowanych tylko wtedy, gdy jest to absolutnie konieczne. Korzystanie z ref struct
będzie miało ograniczenia, ponieważ muszą być przechowywane na stosie. Na przykład nie będą działać, jeśli ciągu interpolowanego zawiera wyrażenie await
, ponieważ kompilator będzie musiał przechowywać program obsługi w implementacji wygenerowanej przez kompilator IAsyncStateMachine
.
Dodawanie większej liczby możliwości do programu obsługi
Poprzednia wersja procedury obsługi ciągów interpolowanych implementuje wzorzec. Aby uniknąć przetwarzania każdego wyrażenia zastępczego, potrzebujesz więcej informacji w obsłudze. W tej sekcji ulepszysz mechanizm obsługi, tak aby wykonywał mniej pracy, gdy skonstruowany ciąg nie jest zapisywany do dziennika. Można użyć System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, aby określić mapowanie między parametrami publicznego interfejsu API a parametrami konstruktora obsługującego. Zapewnia to obsługującemu informacje potrzebne do określenia, czy należy ocenić ciąg interpolowany.
Zacznijmy od zmian w programie obsługi. Najpierw dodaj pole, aby śledzić, czy program obsługi jest włączony. Dodaj dwa parametry do konstruktora: jeden, aby określić poziom dziennika dla tego komunikatu, a drugi odwołanie do obiektu dziennika:
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}");
}
Następnie użyj pola , aby program obsługi dołączał literały lub sformatowane obiekty tylko wtedy, gdy będzie używany końcowy ciąg:
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");
}
Następnie należy zaktualizować deklarację LogMessage
, aby kompilator przekazywał dodatkowe parametry do konstruktora obsługi. Jest to obsługiwane przy użyciu System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute argumentu procedury obsługi:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Ten atrybut określa listę argumentów dla LogMessage
, które odwzorowują się na parametry następujące po wymaganych parametrach literalLength
i formattedCount
. Pusty ciąg (""), określa odbiornik. Kompilator zastępuje wartość obiektu Logger
reprezentowanego przez this
na następny argument konstruktora obsługi. Kompilator zastępuje wartość level
dla następującego argumentu. Można podać dowolną liczbę argumentów dla dowolnej procedury obsługi, którą piszesz. Dodawane argumenty to argumenty ciągu.
Tę wersję można uruchomić przy użyciu tego samego kodu testowego. Tym razem zobaczysz następujące wyniki:
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.
Widać, że metody AppendLiteral
i AppendFormat
są wywoływane, ale nie wykonują żadnej pracy. Obsługujący ustalił, że końcowy ciąg nie jest potrzebny, więc obsługujący go nie buduje. Istnieje jeszcze kilka ulepszeń, które należy wprowadzić.
Najpierw można dodać przeciążenie AppendFormatted
, które ogranicza argument do typu implementującego System.IFormattable. To przeciążenie umożliwia wywołującym dodawanie ciągów formatu w miejscach przeznaczonych na symbole zastępcze. Wprowadzając tę zmianę, zmieńmy również typ zwracany innych metod AppendFormatted
i AppendLiteral
z void
na bool
(jeśli którakolwiek z tych metod ma różne typy zwracane, zostanie wyświetlony błąd kompilacji). Ta zmiana umożliwia zwarcie. Metody zwracają false
, aby wskazać, że przetwarzanie wyrażenia ciągu interpolowanego powinno zostać zatrzymane. Zwracanie true
wskazuje, że powinna ona być kontynuowana. W tym przykładzie używasz go do zatrzymania przetwarzania, gdy wynikowy ciąg nie jest potrzebny. Krótkoczasowe przewodzenie wspiera bardziej szczegółowe działania. Można zatrzymać przetwarzanie wyrażenia po osiągnięciu określonej długości, aby obsługiwać bufory o stałej długości. Lub jakiś warunek może wskazywać, że pozostałe elementy nie są potrzebne.
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");
}
Dzięki temu dodaniu można określić ciągi formatu w wyrażeniu ciągu interpolowanego.
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
w pierwszym komunikacie określa "krótki format czasu" dla bieżącego czasu. W poprzednim przykładzie pokazano jedno z przeciążeń metody AppendFormatted
, którą można utworzyć dla programu obsługi. Nie trzeba określać ogólnego argumentu dla sformatowanego obiektu. Być może masz bardziej wydajne sposoby konwertowania tworzonych typów na ciąg. Można zapisywać przeciążenia AppendFormatted
, które przyjmują te typy zamiast argumentu ogólnego. Kompilator wybiera najlepsze przeciążenie. Środowisko uruchomieniowe używa tej techniki do konwertowania System.Span<T> na wyjście tekstowe. Możesz dodać parametr liczby całkowitej, aby określić wyrównanie danych wyjściowych , z lub bez IFormattable.
System.Runtime.CompilerServices.DefaultInterpolatedStringHandler dostarczany z platformą .NET 6 zawiera dziewięć przeciążeń AppendFormatted do różnych zastosowań. Można go użyć jako odwołania podczas tworzenia programu obsługi do swoich celów.
Uruchom teraz przykład i zobaczysz, że dla komunikatu Trace
wywoływana jest tylko pierwsza 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.
Możesz wprowadzić jedną ostateczną aktualizację konstruktora programu obsługi, która poprawia wydajność. Obslugujący może dodać końcowy parametr out bool
. Ustawienie tego parametru na false
wskazuje, że program obsługi nie powinien być wywoływany w ogóle w celu przetworzenia wyrażenia ciągu interpolowanego:
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!;
}
Ta zmiana oznacza, że można usunąć pole enabled
. Następnie można zmienić zwracany typ AppendLiteral
i AppendFormatted
na void
.
Teraz po uruchomieniu przykładu zobaczysz następujące dane wyjściowe:
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.
Jedynym wyjściem, gdy określono LogLevel.Trace
, jest wyjście konstruktora. Moduł obsługi wskazał, że nie jest aktywny, więc żadna z metod Append
nie została wywołana.
W tym przykładzie przedstawiono ważny punkt obsługi ciągów interpolowanych, szczególnie w przypadku użycia bibliotek rejestrowania. Wszelkie skutki uboczne w symbolach zastępczych mogą nie wystąpić. Dodaj następujący kod do głównego programu i zobacz to zachowanie w akcji:
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}");
Zobaczysz, że zmienna index
jest zwiększana pięć razy w każdej iteracji pętli. Ponieważ symbole zastępcze są oceniane tylko dla poziomów Critical
, Error
i Warning
, a nie dla Information
i Trace
, ostateczna wartość index
nie odpowiada oczekiwaniom.
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
Programy obsługi ciągów interpolowanych zapewniają większą kontrolę nad sposobem konwertowania wyrażenia ciągu interpolowanego na ciąg. Zespół środowiska uruchomieniowego platformy .NET użył tej funkcji do zwiększenia wydajności w kilku obszarach. Możesz korzystać z tej samej funkcji we własnych bibliotekach. Aby dowiedzieć się więcej, rzuć okiem na System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Zapewnia bardziej kompletną implementację niż ta, którą utworzono tutaj. Zobaczysz o wiele więcej przeciążeń, które są możliwe dla metod Append
.