Поделиться через


Примитивы: библиотека расширений для .NET

Из этой статьи вы узнаете о библиотеке Microsoft.Extensions.Primitives. Примитивы в этой статье не следует путать с примитивными типами .NET от BCL или языка C#. Вместо этого типы в библиотеке примитива служат стандартными блоками для некоторых периферийных пакетов NuGet .NET, таких как:

Уведомления об изменениях

Распространение уведомлений при возникновении изменений является фундаментальным понятием в программировании. Наблюдаемое состояние объекта чаще всего не может измениться. Когда происходит изменение, реализации интерфейса Microsoft.Extensions.Primitives.IChangeToken можно использовать для уведомления заинтересованных в нем сторон. Доступны следующие реализации:

Как разработчик вы также можете реализовать собственный тип. Интерфейс IChangeToken определяет несколько свойств:

  • IChangeToken.HasChanged: получает значение, указывающее, произошло ли изменение.
  • IChangeToken.ActiveChangeCallbacks: указывает, будет ли токен инициировать обратные вызовы с упреждением. Если имеет значение false, получатель токена должен опрашивать HasChanged для обнаружения изменений.

Функциональные возможности на основе экземпляра

Рассмотрим следующий пример использования CancellationChangeToken.

CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);

Console.WriteLine($"HasChanged: {cancellationChangeToken.HasChanged}");

static void callback(object? _) =>
    Console.WriteLine("The callback was invoked.");

using (IDisposable subscription =
    cancellationChangeToken.RegisterChangeCallback(callback, null))
{
    cancellationTokenSource.Cancel();
}

Console.WriteLine($"HasChanged: {cancellationChangeToken.HasChanged}\n");

// Outputs:
//     HasChanged: False
//     The callback was invoked.
//     HasChanged: True

В предыдущем примере создается экземпляр CancellationTokenSource и его Token передается в конструктор CancellationChangeToken. Начальное состояние HasChanged записывается в консоль. Создается объект Action<object?> callback, который выполняет операции записи при запуске обратного вызова в консоль. Вызывается метод RegisterChangeCallback(Action<Object>, Object) с учетом callback. В операторе using отменяется cancellationTokenSource. Это активирует обратный вызов, а состояние HasChanged снова записывается в консоль.

Если необходимо выполнить действия из нескольких источников изменений, используйте CompositeChangeToken. Эта реализация объединяет один или несколько токенов изменения и запускает каждый зарегистрированный обратный вызов всего один раз, независимо от того, сколько раз было инициировано изменение. Рассмотрим следующий пример:

CancellationTokenSource firstCancellationTokenSource = new();
CancellationChangeToken firstCancellationChangeToken = new(firstCancellationTokenSource.Token);

CancellationTokenSource secondCancellationTokenSource = new();
CancellationChangeToken secondCancellationChangeToken = new(secondCancellationTokenSource.Token);

CancellationTokenSource thirdCancellationTokenSource = new();
CancellationChangeToken thirdCancellationChangeToken = new(thirdCancellationTokenSource.Token);

var compositeChangeToken =
    new CompositeChangeToken(
        new IChangeToken[]
        {
            firstCancellationChangeToken,
            secondCancellationChangeToken,
            thirdCancellationChangeToken
        });

static void callback(object? state) =>
    Console.WriteLine($"The {state} callback was invoked.");

// 1st, 2nd, 3rd, and 4th.
compositeChangeToken.RegisterChangeCallback(callback, "1st");
compositeChangeToken.RegisterChangeCallback(callback, "2nd");
compositeChangeToken.RegisterChangeCallback(callback, "3rd");
compositeChangeToken.RegisterChangeCallback(callback, "4th");

// It doesn't matter which cancellation source triggers the change.
// If more than one trigger the change, each callback is only fired once.
Random random = new();
int index = random.Next(3);
CancellationTokenSource[] sources = new[]
{
    firstCancellationTokenSource,
    secondCancellationTokenSource,
    thirdCancellationTokenSource
};
sources[index].Cancel();

Console.WriteLine();

// Outputs:
//     The 4th callback was invoked.
//     The 3rd callback was invoked.
//     The 2nd callback was invoked.
//     The 1st callback was invoked.

В предыдущем коде C# создаются три экземпляра объектов CancellationTokenSource и связываются с соответствующими экземплярами CancellationChangeToken. Создание экземпляра составного токена выполняется путем передачи массива токенов в конструктор CompositeChangeToken. Создается объект Action<object?> callback, но на этот раз объект state используется и записывается в консоль как форматированное сообщение. Обратный вызов регистрируется четыре раза, каждый из которых имеет несколько отличающийся аргумент объекта состояния. Код использует генератор псевдослучайных чисел для выбора одного из источников токенов изменения (неважно какого) и вызова его метода Cancel(). Это активирует изменение, что вызывает каждый зарегистрированный обратный вызов всего один раз.

Альтернативный подход static

В качестве альтернативы вызову RegisterChangeCallback можно использовать статический класс Microsoft.Extensions.Primitives.ChangeToken. Рассмотрим следующий шаблон потребления.

CancellationTokenSource cancellationTokenSource = new();
CancellationChangeToken cancellationChangeToken = new(cancellationTokenSource.Token);

IChangeToken producer()
{
    // The producer factory should always return a new change token.
    // If the token's already fired, get a new token.
    if (cancellationTokenSource.IsCancellationRequested)
    {
        cancellationTokenSource = new();
        cancellationChangeToken = new(cancellationTokenSource.Token);
    }

    return cancellationChangeToken;
}

void consumer() => Console.WriteLine("The callback was invoked.");

using (ChangeToken.OnChange(producer, consumer))
{
    cancellationTokenSource.Cancel();
}

// Outputs:
//     The callback was invoked.

Как и в предыдущих примерах, потребуется реализация IChangeToken, созданная changeTokenProducer. Производитель определяется как Func<IChangeToken>. Ожидается, что он будет возвращать новый токен при каждом вызове. consumer является либо Action, если не используется state, либо Action<TState>, где универсальный тип TState проходит через уведомление об изменениях.

Создатели маркеров строк, сегменты и значения

Взаимодействие со строками распространено в разработке приложений. Выполняется анализ или разбивка различных представлений строк либо по ним выполняется итерация. Библиотека примитивов предлагает несколько типов выбора, которые помогают оптимизировать взаимодействие со строками и сделать его более эффективным. Рассмотрим следующие типы.

  • StringSegment: оптимизированное представление substring.
  • StringTokenizer: создает маркер string в экземплярах StringSegment.
  • StringValues: представляет null, ноль, одну или множество строк эффективным способом.

Тип StringSegment.

В этом разделе описано оптимизированное представление substring, известное как тип StringSegment struct. Рассмотрим следующий пример кода C#, демонстрирующий некоторые свойства StringSegment и метод AsSpan.

var segment =
    new StringSegment(
        "This a string, within a single segment representation.",
        14, 25);

Console.WriteLine($"Buffer: \"{segment.Buffer}\"");
Console.WriteLine($"Offset: {segment.Offset}");
Console.WriteLine($"Length: {segment.Length}");
Console.WriteLine($"Value: \"{segment.Value}\"");

Console.Write("Span: \"");
foreach (char @char in segment.AsSpan())
{
    Console.Write(@char);
}
Console.Write("\"\n");

// Outputs:
//     Buffer: "This a string, within a single segment representation."
//     Offset: 14
//     Length: 25
//     Value: " within a single segment "
//     " within a single segment "

Приведенный выше код создает экземпляр StringSegment с заданным значением string, offset и length. StringSegment.Buffer является исходным аргументом строки, а StringSegment.Value — подстрокой, основанной на значениях StringSegment.Offset и StringSegment.Length.

Структура StringSegment предоставляет множество методов для взаимодействия с сегментом.

Тип StringTokenizer.

Объект StringTokenizer представляет собой тип структуры, который разбивает string на экземпляры StringSegment. Разметка больших строк обычно включает разделение строки и итерацию по ней. С учетом сказанного, возможно, стоит обратить внимание на String.Split. Эти API похожи, но в целом StringTokenizer обеспечивает лучшую производительность. Рассмотрим следующий пример.

var tokenizer =
    new StringTokenizer(
        s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
        new[] { ' ' });

foreach (StringSegment segment in tokenizer)
{
    // Interact with segment
}

В предыдущем коде экземпляр типа StringTokenizer создается с учетом 900 автоматически сгенерированных абзацев текста Lorem Ipsum и массива с одним значением символа пробела ' '. Каждое значение в создателе маркеров представлено как StringSegment. Код выполняет итерацию сегментов, позволяя объекту-получателю взаимодействовать с каждым segment.

Сравнение производительности StringTokenizer и string.Split

С учетом различных способов сегментирования строк уместно сравнить два метода с помощью тестирования. Рассмотрим следующие два метода тестирования при использовании пакета NuGet BenchmarkDotNet.

  1. Использование StringTokenizer.

    StringBuilder buffer = new();
    
    var tokenizer =
        new StringTokenizer(
            s_nineHundredAutoGeneratedParagraphsOfLoremIpsum,
            new[] { ' ', '.' });
    
    foreach (StringSegment segment in tokenizer)
    {
        buffer.Append(segment.Value);
    }
    
  2. Использование String.Split.

    StringBuilder buffer = new();
    
    string[] tokenizer =
        s_nineHundredAutoGeneratedParagraphsOfLoremIpsum.Split(
            new[] { ' ', '.' });
    
    foreach (string segment in tokenizer)
    {
        buffer.Append(segment);
    }
    

Оба метода выглядят одинаково в контактной зоне интерфейса API, и они способны разбивать большую строку на фрагменты. Результаты тестирования производительности ниже показывают, что подход StringTokenizer почти в три раза быстрее, но результаты могут отличаться. Как и во всех вопросах производительности, вам следует оценить свой конкретный вариант использования.

Способ Среднее Ошибка StdDev Коэффициент
Tokenizer 3.315 мс 0.0659 мс 0.0705 мс 0,32
Разделение 10.257 мс 0.2018 мс 0.2552 мс 1.00

Условные обозначения

  • Среднее значение: среднее арифметическое всех измерений
  • Ошибка: половина от 99,9 % интервала достоверности
  • Стандартное отклонение: стандартное отклонение всех измерений
  • Медиана: значение, отделяющее большую часть всех измерений (50-й процентиль)
  • Коэффициент: среднее значение распределения коэффициентов (текущее/базовое)
  • Стандартное отклонение коэффициентов: стандартное отклонение распределения соотношения коэффициентов (текущее/базовое)
  • 1 мс: 1 миллисекунда (0,001 с)

Дополнительные сведения о тестировании с помощью .NET см. в разделе BenchmarkDotNet.

Тип StringValues.

Объект StringValues является типом struct, который эффективным способом представляет null, ноль, одну или множество строк. Тип StringValues можно создать с помощью любого из следующих синтаксисов: string? или string?[]?. Используя текст из предыдущего примера, рассмотрим следующий код C#.

StringValues values =
    new(s_nineHundredAutoGeneratedParagraphsOfLoremIpsum.Split(
        new[] { '\n' }));

Console.WriteLine($"Count = {values.Count:#,#}");

foreach (string? value in values)
{
    // Interact with the value
}
// Outputs:
//     Count = 1,799

Приведенный выше код создает экземпляры объекта StringValues с учетом массива значений строк. Записывается StringValues.Count в консоль.

Тип StringValues является реализацией следующих типов коллекций:

  • IList<string>
  • ICollection<string>
  • IEnumerable<string>
  • IEnumerable
  • IReadOnlyList<string>
  • IReadOnlyCollection<string>

Таким образом, можно выполнять итерацию по нему и с каждым value можно взаимодействовать по мере необходимости.

См. также