Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этом руководстве описано, как:
- Реализуйте шаблон обработчика интерполяции строк.
- Взаимодействие с приемником в операциях интерполяции строк.
- Добавьте аргументы в обработчик интерполяции строк.
- Ознакомьтесь с новыми функциями библиотеки для интерполяции строк.
Необходимые условия
Настройте компьютер для запуска .NET. Компилятор C# доступен через Visual Studio или пакет SDK для .NET.
В этом руководстве предполагается, что вы знакомы с C# и .NET, включая Visual Studio или Visual Studio Code и C# DevKit.
Можно написать настраиваемый интерполированный обработчик строк. Интерполированный обработчик строк — это тип, обрабатывающий выражение заполнителя в интерполированной строке. Без пользовательского обработчика система обрабатывает заполнители наподобие 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,literalLengthиformattedCount. (Разрешено больше параметров). - Открытый метод
AppendLiteralс сигнатурой:public void AppendLiteral(string s). - Универсальный открытый
AppendFormattedметод с сигнатурой:public void AppendFormatted<T>(T t).
Внутри построителя создается форматированная строка и предоставляется метод, который позволяет клиенту получить эту строку. В следующем коде показан тип LogInterpolatedStringHandler, соответствующий этим требованиям:
[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");
}
public override string ToString() => builder.ToString();
}
Замечание
Если интерполированное строковое выражение является константой времени компиляции (т. е. не имеет заполнителей), компилятор использует целевой тип string вместо вызова пользовательского интерполированного обработчика строки. Это поведение означает, что интерполированные строки полностью обходят пользовательские обработчики.
Теперь можно добавить перегрузку для LogMessage в классе Logger, чтобы попробовать новый обработчик интерполированной строки.
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.ToString());
}
Вам не нужно удалять исходный 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.
В процессе трассировки результата можно увидеть, как компилятор добавляет код для вызова обработчика и создания строки.
- Компилятор добавляет вызов для создания обработчика, передав общую длину литерального текста в строке формата и количество заполнителей.
- Компилятор добавляет вызовы функций
AppendLiteralиAppendFormattedдля каждого раздела строки литерала и для каждого заполнителя. - Компилятор вызывает метод
LogMessageс помощьюCoreInterpolatedStringHandlerв качестве аргумента.
Наконец, обратите внимание, что последнее предупреждение не вызывает интерполированный обработчик строк. Аргумент представляет собой string, поэтому вызов вызывает другую перегрузку строковым параметром.
Важный
Используйте ref struct для интерполированных обработчиков строк, только если это необходимо.
ref struct типы имеют ограничения, поскольку они должны сохраняться в стеке. Например, они не работают, если интерполированное строковое отверстие содержит await выражение, так как компилятор должен хранить обработчик в реализации, созданной IAsyncStateMachine компилятором.
Расширьте возможности обработчика
Предыдущая версия интерполированного обработчика строк реализует шаблон. Чтобы избежать обработки каждого выражения замещающего элемента, вам нужна дополнительная информация в обработчике. В этом разделе вы улучшаете обработчик, чтобы он выполнял меньше работы, если созданная строка не записывается в журнал. Вы используете 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.ToString());
}
Этот атрибут задает список аргументов для LogMessage, которые сопоставлены с параметрами, следующими после обязательных параметров literalLength и formattedCount. Пустая строка (""), указывает приемник. Компилятор подставляет значение объекта Logger, который представлен как this, для следующего аргумента конструктора обработчика. Компилятор заменяет значение level для следующего аргумента. Вы можете указать любое количество аргументов для любого обработчика, который вы пишете. Добавляемые аргументы являются строковыми аргументами.
Замечание
InterpolatedStringHandlerArgumentAttribute Если список аргументов конструктора пуст, поведение совпадает с тем, что атрибут был опущен полностью.
Эту версию можно запустить с помощью того же тестового кода. На этот раз вы увидите следующие результаты:
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.
Вы видите, что вызываются методы AppendLiteral и AppendFormat, но они не выполняют никакой работы. Обработчик определил, что окончательная строка не нужна, поэтому обработчик не создает его. Есть еще несколько улучшений, чтобы сделать.
Сначала можно добавить перегрузку AppendFormatted, которая ограничивает аргумент типом, реализующим System.IFormattable. Эта перегрузка позволяет пользователям добавлять форматные строки в заполнители. При внесении этого изменения также измените тип возврата других AppendFormatted и AppendLiteral методов на voidbool. Если любой из этих методов имеет разные типы возвращаемых значений, возникает ошибка компиляции. Это изменение позволяет выполнять короткое замыкание. Методы возвращают 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");
}
public void AppendFormatted<T>(T t, int alignment, string format) where T : IFormattable
{
Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with alignment {alignment} and format {{{format}}} is of type {typeof(T)},");
var formatString =$"{alignment}:{format}";
builder.Append(string.Format($"{{0,{formatString}}}", t));
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> в строковые выходные данные. Можно добавить целочисленный параметр, чтобы указать выравнивание выходных данных, с IFormattableили без IFormattable.
System.Runtime.CompilerServices.DefaultInterpolatedStringHandler, который поставляется в комплекте с .NET 6, содержит девять перегрузок 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. Затем можно изменить тип возвращаемого значения AppendLiteral и AppendFormatted на 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 {index++}");
numberOfIncrements++;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");
Вы можете видеть, что переменная index увеличивается в каждой итерации цикла. Поскольку заполнители вычисляются только для уровней Critical, Error и Warning, а не для Information и Trace, итоговое значение index не соответствует ожиданиям.
Critical
Critical: Increment index 0
Error
Error: Increment index 1
Warning
Warning: Increment index 2
Information
Trace
Value of index 3, value of numberOfIncrements: 5
Интерполированные обработчики строк обеспечивают больший контроль над преобразованием интерполированного строкового выражения в строку. Команда среды выполнения .NET использовала эту функцию для повышения производительности в нескольких областях. Вы можете использовать ту же возможность в собственных библиотеках. Дополнительные сведения см. в System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Это обеспечивает более полную реализацию, чем вы создали здесь. Вы видите много дополнительных перегрузок, которые возможны для методов Append.