基元:适用于 .NET 的扩展库
本文介绍 Microsoft.Extensions.Primitives 库。 本文中的基元不会与来自 BCL 的 .NET 基元类型或 C# 语言的基元类型混淆。 相反,基元库中的类型用作某些外围 .NET NuGet 包的构建基块,例如:
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.FileExtensions
Microsoft.Extensions.FileProviders.Composite
Microsoft.Extensions.FileProviders.Physical
Microsoft.Extensions.Logging.EventSource
Microsoft.Extensions.Options
System.Text.Json
更改通知
当发生更改时传播通知是编程中的基本概念。 对象的观察状态在更多时候是可以改变的。 发生更改时,可以使用 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
,当调用回调时,它会写到控制台。 在给定 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:子字符串的优化表示形式。
- StringTokenizer:将
string
标记为StringSegment
实例。 - StringValues:以有效方式表示
null
、零个、一个或多个字符串。
StringSegment
类型
在本部分中,你将了解称为 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 "
在给定 string
值、offset
和 length
的情况下,前面的代码对 StringSegment
进行实例化。 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
}
在前面的代码中,在给定 900 个自动生成的 Lorem Ipsum 文本段落和一个带有空白字符 ' '
的单个值的数组的情况下,创建了 StringTokenizer
类型的一个实例。 tokenizer 中的每个值都表示为 StringSegment
。 代码循环访问段,允许使用者与每个 segment
进行交互。
对 StringTokenizer
和 string.Split
进行基准比较
由于有各种切分字符串的方法,感觉用一个基准来比较两种方法比较合适。 使用 BenchmarkDotNet NuGet 包,可以考虑以下两个基准方法:
使用 StringTokenizer:
StringBuilder buffer = new(); var tokenizer = new StringTokenizer( s_nineHundredAutoGeneratedParagraphsOfLoremIpsum, new[] { ' ', '.' }); foreach (StringSegment segment in tokenizer) { buffer.Append(segment.Value); }
使用 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
都可以根据需要进行交互。