基元:适用于 .NET 的扩展库

本文介绍 Microsoft.Extensions.Primitives 库。 本文中的基元不会与来自 BCL 的 .NET 基元类型或 C# 语言的基元类型混淆。 相反,基元库中的类型用作某些外围 .NET NuGet 包的构建基块,例如:

更改通知

当发生更改时传播通知是编程中的基本概念。 对象的观察状态在更多时候是可以改变的。 发生更改时,可以使用 Microsoft.Extensions.Primitives.IChangeToken 接口的实现将上述更改通知到相关各方。 可用的实现如下:

作为开发人员,你也可以自由地实现自己的类型。 IChangeToken 接口会定义几个属性:

基于实例的功能

请看下面 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,当调用回调时,它会写到控制台。 在给定 callback 的情况下,调用该令牌的 RegisterChangeCallback(Action<Object>, Object) 方法。 在 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.

与前面的示例非常类似,你需要一个由 changeTokenProducer 生成的 IChangeToken 的实现。 生成方被定义为 Func<IChangeToken>,预计每次调用都会返回一个新的令牌。 当不使用 state 时,consumer 是一个 Action,或者是一个 Action<TState>,其中泛型类型 TState 流经更改通知。

字符串 tokenizer、段和值

在应用程序开发中,与字符串交互是非常常见的。 对字符串的各种表示形式进行分析、拆分或循环访问。 基元库提供了几种选择类型,有助于使与字符串的交互更加完善和高效。 来看看以下类型:

StringSegment 类型

在本部分中,你将了解称为 StringSegmentstruct 类型的子字符串的优化表示形式。 来看看以下 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 "

在给定 string 值、offsetlength 的情况下,前面的代码对 StringSegment 进行实例化。 StringSegment.Buffer 是原始字符串参数,StringSegment.Value 是基于 StringSegment.OffsetStringSegment.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
}

在前面的代码中,在给定 900 个自动生成的 Lorem Ipsum 文本段落和一个带有空白字符 ' ' 的单个值的数组的情况下,创建了 StringTokenizer 类型的一个实例。 tokenizer 中的每个值都表示为 StringSegment。 代码循环访问段,允许使用者与每个 segment 进行交互。

StringTokenizerstring.Split 进行基准比较

由于有各种切分字符串的方法,感觉用一个基准来比较两种方法比较合适。 使用 BenchmarkDotNet NuGet 包,可以考虑以下两个基准方法:

  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 方法几乎快了三倍,但结果可能会有所不同。 与所有性能注意事项一样,应评估具体用例。

方法 平均值 错误 标准偏差 比率
分词器 3.315 毫秒 0.0659 毫秒 0.0705 毫秒 0.32
Split 10.257 毫秒 0.2018 毫秒 0.2552 毫秒 1.00

图例

  • 均值:所有度量值的算术平均值
  • 错误:99.9% 的置信区间的一半
  • 标准偏差:所有度量值的标准偏差
  • 中值:此值将所有度量值中较高的一半分隔开(第 50 个百分位)
  • 比率:比率分布的平均值(当前/基线)
  • 比率标准偏差:比率分布的标准偏差(当前/基线)
  • 1 ms: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 都可以根据需要进行交互。

另请参阅