获取结构上的跨度,<byte>而无需复制结构



我一直在尝试Span<T>作为ReadOnlySequence<T>和System.IO.Pipelines的一部分。

我目前正在尝试在不使用unsafe代码和复制该struct的情况下获得structSpan<T>

我的结构很简单:

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
public struct Packet
{
public byte TestByte;
}

方法1 - 有效 - 但感觉"不安全">

//
// Method 1 - uses Unsafe to get a span over the struct
//
var packet = new Packet();
unsafe
{
var packetSpan = new Span<byte>(&packet, Marshal.SizeOf(packet));
packetSpan[0] = 0xFF; // Set the test byte
Debug.Assert(packet.TestByte == 0xFF, "Error, packetSpan did not update packet.");
// ^^^ Succeeds
packet.TestByte = 0xEE;
Debug.Assert(packetSpan[0] == 0xEE, "Error, packet did not update packetSpan.");
// ^^^ Succeeds
}

方法 2 - 无法按预期工作,因为它需要副本

//
// Method 2
//
// This doesn't work as intended because the original packet is actually
// coppied to packet2Array because it's a value type
//
// Coppies the packet to an Array of Packets
// Gets a Span<Packet> of the Array of Packets
// Casts the Span<Packet> as a Span<byte>
//
var packet2 = new Packet();
// create an array and store a copy of packet2 in it
Packet[] packet2Array = new Packet[1];
packet2Array[0] = packet2;
// Get a Span<Packet> of the packet2Array
Span<Packet> packet2SpanPacket = MemoryExtensions.AsSpan<Packet>(packet2Array);
// Cast the Span<Packet> as a Span<byte>
Span<byte> packet2Span = MemoryMarshal.Cast<Packet, byte>(packet2SpanPacket);
packet2Span[0] = 0xFF; // Set the test byte
Debug.Assert(packet2.TestByte == 0xFF, "Error, packet2Span did not update packet2");
// ^^^ fails because packet2 was coppied into the array, and thus packet2 has not changed.
Debug.Assert(packet2Array[0].TestByte == 0xFF, "Error, packet2Span did not update packet2Array[i]");
// ^^^ succeeds
packet2.TestByte = 0xEE;
Debug.Assert(packet2Span[0] == 0xEE, "Error, packet2 did not update in packet2Span");
// ^^^ fails because packet2Span is covering packet2Array which has a copy of packet2 
packet2Array[0].TestByte = 0xEE;
Debug.Assert(packet2Span[0] == 0xEE, "Error, packet2 did not update in packet2Span");
// ^^^ succeeds

进一步的研究表明,

Span<T>可以从byte[]隐式强制转换,例如,我可以做

Span<byte> packetSpan = new Packet().ToByteArray();

但是我拥有的任何ToByteArray((的当前实现仍在制作Packet结构的副本。

我不能做这样的事情:

Span<byte> packetSpan = (byte[])packet;
// ^^ Won't compile

没有unsafe就无法在任意结构上获取Span<byte>,因为这样的跨度将允许您以任何方式更改结构的任何位,可能违反类型的不变量 - 这本质上是不安全的操作。

好的,但是ReadOnlySpan<byte>呢?请注意,您必须将StructLayoutAttribute放在结构上才能使代码合理。这应该是一个提示。想象一下,尝试编写一个更简单的方法,该方法为任意T where T : struct返回byte[]。你必须先找出struct的大小,不是吗?那么,你如何找出 C# 中struct的大小?您可以使用sizeof运算符,该运算符需要unsafe上下文,并且需要结构为非托管类型;或者你可以Marshall.SizeOf它很不稳定,并且仅适用于具有顺序或显式字节布局的结构。没有安全、通用的方法,因此你不能这样做。

Span<T>ReadOnlySpan<T>在设计时并没有考虑访问结构字节,而是考虑了数组的跨越片段,这些片段具有已知大小并保证是连续的。

如果你确信你知道自己在做什么,你可以在unsafe的背景下做到这一点——这就是它的用途。但请注意,由于上述原因,使用unsafe的解决方案不会推广到任意结构。

如果打算将结构用作 IO 操作的缓冲区,则可能需要查看固定大小的缓冲区。它们还需要unsafe上下文,但您可以将不安全性封装在结构中,并将Span<byte>返回到该固定缓冲区。基本上,任何处理内存中对象的字节结构的东西都需要在 .NET 中unsafe,因为内存管理是这种"安全"所指的东西。

事实证明,这样做可能的,事实上已经有很长一段时间了......从 dotnet Core 2.1 开始,如果文档是可信的!

神奇的成分是MemoryMarshal.CreateSpan,它"在常规托管对象的一部分上创建一个新的跨度。">


var packet = new Packet() { TestByte = 123 };
var span = MemoryMarshal.CreateSpan<Packet>(ref packet, 1);
var bytes = MemoryMarshal.Cast<Packet, byte>(span);
bytes[0] = 100;

Console.WriteLine(packet.TestByte);

如您所愿地输出100

在这里,您不能做任何太愚蠢的事情,因为如果 to- 或 from-类型包含任何托管引用MemoryMarshal.Cast则会引发ArgumentExceptionMemoryMarshal.CreateSpan附带了一堆警告,并为您提供了一种向自己的脚开枪的新方法,但是嘿......它不需要unsafe所以狂野吧!

应谨慎使用此方法。这是危险的,因为不检查length参数。即使ref被注释为scoped,它也将被存储到返回的 span 中,并且返回的 span 的生存期不会经过安全验证,即使通过跨度感知语言也是如此。

(scoped ref是 dotnet 7/C# 11 功能,但该方法在旧版本的 dotnet 中可用,没有该特定关键字(

简介根本没有提到固定,我相信你不需要担心它。跨度是围绕对基础结构(ref packet位(的托管引用构造的,该结构不是哑指针。因此,我希望结构的重新定位(例如,因为它所属的托管对象被 GC 移动到(将由运行时透明地处理。


只是为了确认,你绝对可以做这样的事情:

var packet = new Packet() { TestByte = 123 };
var span = MemoryMarshal.CreateSpan<Packet>(ref packet, 100);
var bytes = MemoryMarshal.Cast<Packet, byte>(span);
new Random().NextBytes(bytes);

并导致崩溃。因此,虽然这在关键字意义上不是unsafe,但它绝对不安全

最新更新