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

初窥C# StringValues

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

简介

在这篇文章中,我会向大家简要介绍一下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[]的方法,减少了内存分配。

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

相关推荐

IDEA界面太丑??尝试一下这几个插件

前言IntelliJIDEA主要用于支持Java、Scala、Groovy等语言的开发工具,同时具备支持目前主流的技术和框架,擅长于企业应用、移动应用和Web应用的开发。IntelliJi...

小巧 Vue 页面滚动进度条组件ScrollProgress

今天给大家分享一个轻量级Vue.js全屏滚动进度条组件VueScrollProgress。vue-scroll-progress一款基于vue.js构建的页面滚动进度条组件,...

基于vue实现可视化拖拽编辑器,页面生成工具,提升前端开发效率

项目介绍基于vue实现的可视化拖拽编辑器,实现页面生成工具,提升前端开发效率。可以基层到移动端项目作为自定义json直接生成UI页面。项目特点功能特点...

优秀 vue+heyui 后端管理系统HeyUI-Admin

今天再给小伙伴们推荐一款成熟的企业中后台管理系统HEYUI-Admin。heyui-admin基于vue.js和heyui组件库构建的后台管理系统。包含基础表单/表格功能,拓展组件(图表、富文本编辑...

响应式 Vue.js 前端组件化框架Xvue-UI

今天给小伙伴们推荐一款超不错的Vue轻量级组件框架XVueUI。xvue-ui基于vue2.x构建的响应式前端组件化框架。轻量级、易于上手,提供...

《基于SpringBoot+Vue的在线视频系统设计与实现》开题报告

【计算机毕业设计案例】基于SpringBoot+Vue的在线视频系统设计与实现_哔哩哔哩_bilibili...

超好用 Vue.js 图片裁切组件Vue-ImgCutter

今天给小伙伴们分享一个超棒的Vue图片任意裁剪插件VueImgCutter。vue-img-cutter基于vue2.x构建的轻量级剪切图片组件。支持移动图像、放大缩小图片、任意移动图片、固定比...

Vue 3 进阶用法:异步组件(vue 异步组件原理)

一、代码分割一个大型前端应用,如果所有代码都放在单一文件,体积会特别大,下载时间长,白屏时间久,用户体验差。...

源码补丁神器—patch-package(源码助手怎么用)

作者:张浩一、背景vue项目中使用vue-pdf第三方插件预览pdf,书写业务代码完美运行,pdf文件内容正常预览无问题。后期需求有变,业务需求增加电子签章功能。这个时候pdf文件的内容可以显示出...

经验分享:Vue2 项目升级 Vue3 + Element Plus,借助Deepseek手动升级

Vue3出来好久了,我开发的项目还在使用Vue2框架,一般情况下不考虑升级,但是最近需要接入工作流程引擎之类的,看了下Vue2生态下操作空间不是很好,那索性尝试升级Vue3吧。一番操作下来,升级成功,...

34K Star!史上最全JavaScript资源库 awesome-javascript

34KStar!史上最全JavaScript资源宝库大揭秘引言在GitHub上,有一个备受瞩目的JavaScript资源仓库,以其全面的内容和精心的分类,成为了众多开发者的必备参考。这个拥有超过...

基于 Vue.js 磁片栅格布局组件VueGridLayout

#头条创作挑战赛#今天给大家分享一个超优秀的vue.js拖拽栅格布局插件VueGridLayout。...

6款高颜值 Vue3 PC端UI组件库(vue3开发组件库)

马上到国庆了,还没学习或者想学习vue3的小伙伴们有安排上没?这次推荐几个比较流行的VUE3UI组件库,合理利用,又或者学习借鉴都是不错的选择。1、element-pluselement-plus...

高性能 vue.js+ztree 树形组件Vue-GiantTree

今天给大家分享一款超棒的Vue海量数据渲染树形组件VueGiantTree。vue-giant-tree基于ztree封装的Vue树形组件。轻松实现大数据高性能渲染,适合海量数据渲染场景。zTr...

【推荐】2024年推荐的6款开源免费 Vue 后台管理系统模板,建议收藏

前言在现今的软件开发领域,...