网站首页 > 技术文章 正文
简介
在这篇文章中,我会向大家简要介绍一下ASP.NET Core的核心类型之一StringValues。将会探讨StringValues在框架中的使用场景,它的用途,如何实现,以及为什么要这么做。
重复的HTTP头
作为一个ASP.NET Core开发者,我们可能在各种地方遇到过StringValues,尤其是在处理HTTP header的时候。
HTTP的一个特性是,我们可以在某些http header中多次包含相同的键(从规范中可以看出):
Multiple message-header fields with the same field-name MAY be present in a message if and only if the entire field-value for that header field is defined as a comma-separated list [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma.
我们也就不用担心这样做合理不合理了,事实是我们可以这样做,所以ASP.NET Core必须支持它。本质上,这意味着对于请求(或响应)中的每个头名称,我们可以有0个,1个,或多个字符串值:
GET / HTTP/1.1
Host: localhost:5000 # 没有值
GET / HTTP/1.1
Host: localhost:5000
MyHeader: some-value # 一个值
GET / HTTP/1.1
Host: localhost:5000
MyHeader: some-value # 多个值
MyHeader: other-value # 多个值
那么,假设我们在ASP.NET Core团队中,我们需要创建一个“header集合”类型。我们会如何处理呢?
使用数组的简单实现
一个显而易见的解决方案是始终将给定header的所有值存储为一个数组。数组可以轻松处理零([]),一个(["val1"])或更多(["val1", "val2"])的值,而不需要任何复杂性。一个伪实现可能会是这样:
public class Headers : Dictionary<string, string[]> { }
如果我们想要获得给定键(比如MyHeader)的值,那么我们可以像这样获取值:
Headers headers = new(); // 这边只是简单使用了new,但是真实场景应该是通过http request获取
string[] values = headers["MyHeader"] ?? [];
所以,这个API的好处是它不会隐藏同一个header有多个值的事实。
不幸的是,这种简单方法有几个缺点:
- 在绝大多数情况下header都只有一个值,但我们还是得必须处理可能存在多个值的情况,即使这种情况实际上很多余。
- 在数组中存储单个值会增加分配,从而降低性能。
在旧的ASP.NET时代的System.Web中,通过使用NameValueCollection解决了HttpRequest.Headers的问题。这个旧类型的公共API看起来有点像Dictionary<string, string>,但实际上它在数组中存储了值,然后在输出时自动组合它们:
using System.Collections.Specialized;
var nvc = new NameValueCollection();
nvc.Add("Accept", "val1");
nvc.Add("Accept", "val2");
var header = nvc["Accept"];
Console.WriteLine(header); // prints "val1,val2"
注意:根据HTTP规范,使用','来连接header上是正确的组合方式。
从使用者的角度看,这个API的好处是,我们不必担心同一个header是否有多个值,因为它们会自动为我们连接在一起,我们总是得到一个单独的字符串。我们也可以使用GetValues()来获取值作为一个string[]。
不过可惜的是,这种方法仍然有几个缺点:
- 值仍然存储为一个string[](实际上为一个ArrayList),所以即使只有一个值,我们仍然需要额外付出内存分配的代价。
- 当我们使用GetValues()检索值时,会分配另一个string[]。
最后,使用NameValueCollection,在我们提取它之前,无法知道这个header包含多少个值。所以,我们要么选择“安全”地使用GetValues(),这将会导致额外的string[]内存分配,而通常这都是没有必要的。或者我们也可以使用索引器,但这我们就需要承担多个值被连接成一个单独的字符串的风险。
所有这些额外的分配都带来不必要的浪费,这就是为什么我们需要StringValues。
解决方案-StringValues
好了我们再看来看看什么是我们真正想要的?
- 当只有一个值时,写入(和读取)一个字符串,这样我们就不会分配一个不必要的额外数组。
- 当有多个值时,写入(和读取)一个string[]。
- 写入或检索时不额外分配(如果可能)。
ASP.NET Core对这个问题的解决方案,以及这篇文章的重点,就是StringValues。 StringValues是一个只读的结构类型,正如源代码中所说:
Represents zero/null, one, or many strings in an efficient way.
StringValues存储了一个object?,这个object?可以取以下三个值之一,通过这种方式来实现目标:
- null(表示0个header值)
- 字符串(即1个header值)
- string[](任意数量的header值)
在一些早期的实现中,StringValues将string和string[]值存储为单独的字段,但是在这个PR(https://github.com/dotnet/extensions/pull/1283)中,它们被合并到一个单一的object字段中,这使得整个结构只有单指针大小,就如在issue(https://github.com/dotnet/extensions/issues/1290)中讨论的那样,这个改动带来了性能提升。
从用户使用API的角度来看,StringValues有点像string和string[]的缝合怪。它有像IsNullOrEmpty()这样的方法,但它也实现了一系列基于集合的接口和相关方法:
public readonly struct StringValues : IList<string?>, IReadOnlyList<string?>, IEquatable<StringValues>, IEquatable<string?>, IEquatable<string?[]?>
{
}
我们可以使用其中一个构造器来创建一个StringValues对象:
public readonly struct StringValues
{
private readonly object? _values;
public StringValues(string? value)
{
_values = value;
}
public StringValues(string?[]? values)
{
_values = values;
}
}
作为一个只读的结构,StringValues除了包含的字符串或字符串数组外,不需要在堆上分配任何额外的空间。
根据我们的使用场景,我们有不同的方式可以从StringValues实例中提取值。例如,如果我们需要字符串形式的值,我们可以这样做:
StringValues value;
if (value.Count == 1)
{
// 只有一个值,所以可以隐式转换成string,extracted就等于那个值
string extracted = value;
}
else
{
// 当有多个值,自动使用,进行join,就会的到类似"a,b,c",效果和value.ToString() 一样
string extracted = value;
}
或者,如果我们期望有多个值,或者通常想要安全地枚举所有的值,我们可以简单地使用一个foreach循环:
StringValues value;
foreach (string str in value)
{
// 处理逻辑
}
StringValues使用一个自定义的结构枚举器,如果它包含一个单一的字符串,就返回_values字段,否则枚举string?[]的值。
我们也可以调用ToArray(),但是这又回到了我们开始的问题,如果我们只有一个字符串值,这将分配内存,所以我们应该避免这样做。
对于StringValues,没有太多需要担心的,但是一些实现细节还是挺有趣的,所以我将在下面和大家一起来看看。
关于String Values的一些实现背后的原理
IsNullOrEmpty的实现体现了StringValues内部使用的一般模式:模式匹配来检查null以及string或string[],然后一旦我们确定_values到底是什么,就使用Unsafe.As<>进行类型转换。
public static bool IsNullOrEmpty(StringValues value)
{
// Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory
object? data = value._values;
if (data is null)
{
return true;
}
if (data is string[] values)
{
return values.Length switch
{
0 => true,
1 => string.IsNullOrEmpty(values[0]),
_ => false,
};
}
else
{
// Not array, can only be string
return string.IsNullOrEmpty(Unsafe.As<string>(data));
}
}
在Count属性中我们也能看到类似的使用:
public int Count
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (value is null)
{
return 0;
}
if (value is string)
{
return 1;
}
else
{
// Not string, not null, can only be string[]
return Unsafe.As<string?[]>(value).Length;
}
}
}
我们要看的最后一个方法是GetStringValue()。这是一个私有方法,被ToString()(以及其他方法)调用,将值转换为string,无论存储的值是什么。string和null的情况很简单,而string[]则展示了一个与性能相关的很好的例子,那就是使用string.Create()。
private string? GetStringValue()
{
// Take local copy of _values so type checks remain valid even if the StringValues is overwritten in memory
object? value = _values;
if (value is string s)
{
return s;
}
else
{
return GetStringValueFromArray(value);
}
static string? GetStringValueFromArray(object? value)
{
if (value is null)
{
return null;
}
// value is not null or string, so can only be string[]
string?[] values = Unsafe.As<string?[]>(value);
return values.Length switch
{
0 => null,
1 => values[0],
_ => GetJoinedStringValueFromArray(values),
};
}
static string GetJoinedStringValueFromArray(string?[] values)
{
// Calculate final length of the string
int length = 0;
for (int i = 0; i < values.Length; i++)
{
string? value = values[i];
// Skip null and empty values
// I'm not sure why !string.IsNullOrEmpty() isn't used, but seeing
// as Ben Adams wrote it, I'm sure there's a good reason
if (value != null && value.Length > 0)
{
if (length > 0)
{
// Add separator
length++;
}
length += value.Length;
}
}
// Create the new string
return string.Create(length, values, (span, strings) => {
int offset = 0;
// Skip null and empty values
for (int i = 0; i < strings.Length; i++)
{
string? value = strings[i];
if (value != null && value.Length > 0)
{
if (offset > 0)
{
// Add separator
span[offset] = ',';
offset++;
}
value.AsSpan().CopyTo(span.Slice(offset));
offset += value.Length;
}
}
});
}
}
StringValues是一个很好的例子,展示了 ASP.NET Core 如何在不牺牲 API 使用便利性的前提下,精心优化了性能。我们可以像使用 string 或 string[] 一样,轻松地使用 StringValues。
总结
在这篇文章中,我简要地探讨了如何处理HTTP header有多个值的常见问题。我们讨论了 ASP.NET是如何利用 NameValueCollection类型来解决这个问题的,以及 ASP.NET Core 是如何更优雅地使用 StringValues 来处理它的。最后,我们一同看了看 StringValues 是如何实现的,通过使用一个字段来存储 string 或 string[] 对象,并实现各种集合接口,比起仅仅使用string[]的方法,减少了内存分配。
这就是今天这篇文章想要跟大家分享的信息,如果有任何问题,欢迎大家留言评论^_^
猜你喜欢
- 2025-01-13 Java 中 List 分片的 5 种方法
- 2025-01-13 你见过哪些实用到爆的 Java 代码技巧?
- 2025-01-13 手把手教你搭建一个基于Java的分布式爬虫系统「转」
- 2025-01-13 List的扩容机制,你真的明白吗?
- 2025-01-13 C# 基础知识系列- 3 集合数组
- 2025-01-13 去除 List 中的重复元素,你知道几种实现方法?
- 2025-01-13 C#中的List可以存储哪些类型的数据?
- 2025-01-13 java8对List集合根据某一字段进行分组
- 2025-01-13 PCHMI5.5二次开发文档(更新)
- 2025-01-13 Qt QString字符串分割、截取的3种方法
- 02-21走进git时代, 你该怎么玩?_gits
- 02-21GitHub是什么?它可不仅仅是云中的Git版本控制器
- 02-21Git常用操作总结_git基本用法
- 02-21为什么互联网巨头使用Git而放弃SVN?(含核心命令与原理)
- 02-21Git 高级用法,喜欢就拿去用_git基本用法
- 02-21Git常用命令和Git团队使用规范指南
- 02-21总结几个常用的Git命令的使用方法
- 02-21Git工作原理和常用指令_git原理详解
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)