百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

初窥C# StringValues

csdh11 2025-03-06 13:54 4 浏览

简介

在这篇文章中,我会向大家简要介绍一下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 { }

如果我们想要获得给定键(比如MyHeader)的值,那么我们可以像这样获取值:

Headers headers = new(); // 这边只是简单使用了new,但是真实场景应该是通过http request获取
string[] values = headers["MyHeader"] ?? [];

所以,这个API的好处是它不会隐藏同一个header有多个值的事实。

不幸的是,这种简单方法有几个缺点:

  • 在绝大多数情况下header都只有一个值,但我们还是得必须处理可能存在多个值的情况,即使这种情况实际上很多余。
  • 在数组中存储单个值会增加分配,从而降低性能。

在旧的ASP.NET时代的System.Web中,通过使用NameValueCollection解决了HttpRequest.Headers的问题。这个旧类型的公共API看起来有点像Dictionary,但实际上它在数组中存储了值,然后在输出时自动组合它们:

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, IReadOnlyList, IEquatable, IEquatable, IEquatable
{
}

我们可以使用其中一个构造器来创建一个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(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(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(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 im not sure why string.isnullorempty isnt used but seeing as ben adams wrote it im sure theres a good reason if value 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 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[]的方法,减少了内存分配。

这就是今天这篇文章想要跟大家分享的信息,如果有任何问题,欢迎大家留言评论^_^

相关推荐

音视频命令转换工具 - FFmpeg

随着自媒体兴起,许多人会自拍视频或者找视频素材裁剪,配上背景音乐或解说,加上各种特效边框,处理后再生成新的视频文件,发布到各大平台。生成的原始视频文件都很大,我们需要转换格式或者压缩大小,便于上传或者...

视频剪辑软件,如何批量将h264转换为h265格式的视频

最近有很多朋友在问,因为剪辑、或者上传的原因,需要将视频编码转换成H265格式编码,该怎么操作呢?不知道怎么办的宝贝们,下面请随小编一起来试试吧。需要哪些工具?安装一个媒体梦工厂视频素材若干怎么快速剪...

1080秒变4K,让PotPlayer开启“超分辨率”播放视频文件

大家好,我是大卫呆。1080P的视频能秒变4K视频吗?通过上期节目的实机测试,答案是:...

视频编码H.265与H.264的区别-------深入浅出说监控

我们在购买监控摄像头或者录像机产品的时候,一般情况下,经销商会问你是要H.265编码格式与H.264编码格式的,很多人都会好奇,什么是H.265和H.264?他们的实际效果有什么区别?今天就从定义和作...

H.265已落后!下一代视频技术实现重大突破

来源:快科技下一代视频技术实现重大突破。从阿里云官微获悉,阿里达摩院XG实验室参与制定的新一代国际视频编码标准H.266(VVC)出炉,同等画质下将节省近50%传输流量,清晰度越高,码率节省越多。前不...

VP9 或 H.265 的 6 个比较点

直播很复杂。广播流和通过Internet传输流的整个过程涉及一系列可以采用多种格式的方法。一个重要的组件是用于媒体文件编码和解码的编解码器。编解码器还定义了可用于进行流式传输的工具类型。大大简化流...

融合通信系统播放不了H.265视频怎么解决

在融合通信项目中,视频的融合是很多项目的落地要求,随着技术的进步,需要融合的视频也是多种多样,很多项目中需要接入视频监控,布控球,无人机,视频会议等视频资源。这些视频资源使用不同的技术,不同的传输协议...

别被忽悠了!视频编码H.265与H.264的区别有多大?看完你就懂了

相信大家都听过H.265和H.264这两种编码,也看过专业术语的解释。包括电视机都会标注支持H.265格式4K视频编码,视频监控系统也会标注支持H.265。但是还是有很多人不知道什么是视频编码H.26...

视频行业迎来巨变!H.265将被淘汰,电视及流媒体全受影响

这两天,姐夫自己遇到一个问题,从一些流媒体网站上下载来的视频,比如油管上的视频,居然无法正常在Windows10上播放,必须要重新下载一个视频编码插件才行。甚至于这些视频都无法通过现有的编辑软件去做...

对于Mybaits缓存的理解

...

你居然还去服务器上捞日志,搭个 Graylog 日志收集系统不香么

一、前言...

Java 代理从 0 到彻底搞懂

一、为什么出现代理?咱们先抛开编程,想象一下生活中的场景。假如你是一位大明星,每天都有无数的活动邀约、采访请求,还有各种商务合作的洽谈。要是你亲自去处理这些事情,那你哪还有时间去拍戏、唱歌、提升自己的...

SpringBoot系列——cache缓存

  前言  日常开发中,缓存是解决数据库压力的一种方案,通常用于频繁查询的数据,例如新闻中的热点新闻,本文记录springboot中使用cache缓存。...

Spring的缓存帝国,得益于这 5个注解!

在微服务,分布式的大环境下,缓存绝对是提升系统性能的关键手段,Spring作为Java生态中最流行的企业级应用框架,它是如何实现缓存的呢?这篇文章,我们将深入探讨Spring中5个核心的缓存注解...

JVM缓存EhCache在实际业务系统中的应用及复杂场景探讨

本文将介绍JVM缓存EhCache的基本概念、原理以及在实际业务系统中的使用。文章将重点讨论EhCache在复杂场景下的应用,并提供Java语言实现的示例。1.JVM缓存EhCache简介EhCa...