先说说我对Span的看法。Span是类型安全和内存安全的视图,指向任何连续的内存空间。Span和Memory都是内存缓冲区,它们包装了可以在管道上使用的结构化数据。它们旨在有效地传输管道中的数据。属性解释这里有很多属性,值得仔细琢磨:1.指向任意连续的内存空间:支持托管堆、本机内存、栈。这从Span的几个重载的构造函数就可以看出来。2.类型安全:Span是泛型。3.内存安全:Span[1]是只读的refstruct数据结构。用来表示一块连续内存的key属性设置为只读readonly,保证所有操作只能在这块内存内进行。//从Span源码中截获publicreadonlyrefstructSpan{//表示连续内存的关键属性Pointer&Length只能从构造函数中赋值///一个byref或一个nativeptr.internalreadonlyByReference_reference;///此Span包含的元素数。privatereadonlyint_length;[MethodImpl(MethodImplOptions.AggressiveInlining)]publicSpan(T[]?array){if(array==null){this=default;返回;//返回默认值}if(!typeof(T).IsValueType&&array.GetType()!=typeof(T[]))ThrowHelper.ThrowArrayTypeMismatchException();_reference=newByReference(refMemoryMarshal.GetArrayDataReference(array));_length=array.Length;}}4。View:运算的结果会直接反映在底层的连续内存中。到目前为止,我们已经了解了一个简单的用法,使用span操作指向一段堆栈空间。staticvoidMain(){SpanarraySpan=stackallocbyte[100];//包含指针和Length的只读指针,类似于go中的slicebytedata=0;for(intctr=0;ctr(),生成一个新的slicearraySum=Sum(slice);Console.WriteLine($"求和为{arraySum}");//输出0}[MethodImpl(MethodImplOptions.AggressiveInlining)]staticintSum(Spanarray){intarraySum=0;foreach(数组中的变量值)arraySum+=value;返回数组总和;}这里Span指向一个具体的栈空间,Fill、Clear等操作的效果直接体现在这段内存中。注意Slice的切片方式,其内部本质是生成一个新的Span,也就是一个新的视图,对新Span的操作会体现在原来的底层数据结构中。[MethodImpl(MethodImplOptions.AggressiveInlining)]publicSpanSlice(intstart){if((uint)start>(uint)_length)ThrowHelper.ThrowArgumentOutOfRangeException();returnnewSpan(refUnsafe.Add(ref_reference.Value,(nint)(uint)start/*强制零扩展*/),_length-start);从Slice切片源码可以看出,其实质就是利用原来的ptr&length生成新的ptr&length,在操作来看,ptr其实就是指针的移动,也就是定位到一个新的ptr&length数据块,但它最终在原始数据块内。衍生技能点我们来仔细看看Span的定义。有几个关键词建议大家温故知新。1.readonlystrcut[2]从C#7.2开始,可以对struct应用readonly,表示该struct不可更改。Span被定义为只读结构体,其内部属性自然是只读的。从上面的分析和例子,我们可以对Span所代表的特定连续内存空间进行内容更新操作;如果要限制连续内存空间内容的更新,C#提供了ReadOnlySpan类型,该类型强调的是这块内存是只读的,即没有Span拥有的Fill、Clear等方法。一线码农写了一篇关于【使用span对字符串求和】姿势的文章。大家都说使用span可以高效的操作内存。我们对此用例使用了BenchmarkDotNet压力测试。使用System.Linq;使用System.Text;使用System.Threading.Tasks;使用System.Buffers;使用System.Runtime.CompilerServices;使用BenchmarkDotNet.Attributes;使用BenchmarkDotNet.Running;命名空间ConsoleApp3{公共类程序{staticvoidMain(){varsummary=BenchmarkRunner.Run();}}[MemoryDiagnoser,RankColumn]公共类MemoryBenchmarkerDemo{intNumberOfItems=100000;小字符串对象[Benchmark]publicvoidStringSplit(){for(inti=0;ispan=s.AsSpan();varnum1=int.Parse(span.Slice(0,position));varnum2=int.Parse(span.Slice(position));_=num1+num2;}}}}压测解读:runonstring时间分片不会使用驻留池,所以case1会分配大量的小对象;case2对底层字符串进行切片,虽然会生成不同的透视对象Span,但是实际引用的原内存块的偏移区间不存在分配新内存2.refstruct[3]从C#7.2开始,ref可以作用于struct,表示该类型分配在栈上,不能转义到托管堆。Span,ReadonlySpan对任意连续内存块的透视操作进行了包装,但是只能入栈,不适合一些场景,比如异步调用。.NETCore2.1为此增加了Memory[4]和ReadOnlyMemory,可以用来存储在托管堆上,这个暂时不列出来。最后用一张图来总结一下,这篇文章就写好了,感谢【yiideology】参与讨论。参考链接[1]Span:https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Span.cs[2]readonlystrcut:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#readonly-struct[3]参考结构:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct[4]内存:https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines