При отладке не видно данные расположенные в Span
Я написал структуру в которую можно вместить до 2048 байт данных (для передачи через Pipe).
Дело в том, что можно посмотреть данные которые находятся в buffer.Message.Data, но при этом данных в buffer.Bytes не видно.
Может я что-то не правильно описал в структуре, или же я просто никак не смогу посмотреть эти данные через отладчик, т.к. при попытке формирования строки из buffer.Bytes, все хорошо отрабатывает?
минимальный воспроизводимый пример:
public class Program
{
static readonly byte[] handShakeMessage
= new byte[] {
0xE3, 0x81, 0x93,
0xE3, 0x81, 0x8D,
0xE3, 0x82, 0x93,
0xE3, 0x83, 0xAC,
0xE3, 0x83, 0xB3,
0xE3, 0x83, 0x80,
0xE3, 0x83, 0xA9,
0xE3, 0x83, 0xBC,
0x2F, 0x31, 0x2E,
0x30, 0x2E, 0x30
};
public static void Main()
{
KokinBuffer buffer = new();
buffer.Message.Data = handShakeMessage.AsSpan();
buffer.Message.Type = MessageType.HandShake;
Console.WriteLine(Encoding.UTF8.GetString(buffer.Message.Data));
Console.WriteLine(Encoding.UTF8.GetString(buffer.Bytes));
}
}
public enum MessageType
: uint
{
Unknown,
HandShake,
Update,
Status,
Disconnect
}
[StructLayout(LayoutKind.Sequential)]
public unsafe struct KokinMessage
{
public MessageType Type
{
get
{
fixed (KokinMessage* pData = &this)
{
return *(MessageType*)pData;
}
}
set
{
fixed (KokinMessage* pData = &this)
{
MessageType* pType = (MessageType*)pData;
*pType = value;
}
}
}
public int Length
{
get
{
fixed (void* pData = &this)
{
return *(int*)((byte*)pData + sizeof(MessageType));
}
}
set
{
fixed (void* pData = &this)
{
*(int*)((byte*)pData + sizeof(MessageType)) = value;
}
}
}
public Span<byte> Data
{
get
{
fixed (void* pData = &this)
{
const int offset = sizeof(MessageType) + sizeof(int);
return new((byte*)pData + offset, KokinBuffer.Size - offset);
}
}
set
{
fixed (void* pBuffer = &this)
{
byte* pData = (byte*)pBuffer;
const int offset = sizeof(MessageType) + sizeof(uint);
Span<byte> data = new(pData + offset, KokinBuffer.Size - offset);
if (!value.TryCopyTo(data))
{
throw new Exception();
}
Length = data.Length;
}
}
}
}
[StructLayout(LayoutKind.Explicit, Size = Size)]
public unsafe struct KokinBuffer
{
public const int Size = 2048;
public Span<byte> Bytes
{
get
{
fixed (KokinBuffer* pBuffer = &this)
{
return new(pBuffer, Size);
}
}
set
{
fixed (KokinBuffer* pBuffer = &this)
{
Span<byte> data = new(pBuffer, Size);
if (!value.TryCopyTo(data))
{
throw new Exception();
}
}
}
}
public ref KokinMessage Message
{
get
{
fixed (KokinBuffer* pBuffer = &this)
{
return ref *((KokinMessage*)pBuffer);
}
}
}
public ReadOnlySpan<byte> Zip()
{
return Bytes;
}
public static implicit operator ReadOnlySpan<byte>(KokinBuffer buffer)
{
return buffer.Bytes;
}
public static implicit operator KokinBuffer(Span<byte> span)
{
return new KokinBuffer { Bytes = span };
}
}
Ответы (1 шт):
Наверное дело в том, что buffer - это значимый тип, и когда отладчик пытается получить экземпляр, он фактически создает новый внутри себя с учетом полей данных и копирует все значимые типы. Но здесь за счет небезопасного кода используется область памяти, выделенная под структуру в LayoutKind, но не выделенная под данные.
Видимо отладчику от этого становится плохо и он просто не клонирует байты в свой новый экземпляр этой структуры, при чем если симотреть в само свойство, то данные есть, если же через инспектор смотреть в это же свойство, не упоминаемое в коде, то данных нет. Может быть, это баг студии, я до конца не понял.
В любом случае, в рантайме все прекрасно работает. Могу только сказать, что вы "слегка" перемудрили, но мысль верная, и мне понравился этот хоть и небезопасный, но подход.
Я решил немного упростить код
public class Program
{
public static void Main()
{
MyMessage buffer = new();
byte[] message = Encoding.UTF8.GetBytes("Привет, мир!");
buffer.Data = message;
buffer.Type = MessageType.HandShake;
Console.WriteLine(Marshal.SizeOf<MyMessage>());
Console.WriteLine(buffer.Type);
ReadOnlySpan<byte> bytes = buffer;
Console.WriteLine(bytes.Length);
Console.WriteLine(Encoding.UTF8.GetString(buffer.Data));
Console.WriteLine(Encoding.UTF8.GetString(bytes));
}
}
public enum MessageType
: uint
{
Unknown,
HandShake,
Update,
Status,
Disconnect
}
[StructLayout(LayoutKind.Sequential, Size = size)]
public unsafe struct MyMessage
{
public const int size = 2048;
public const int dataOffset = sizeof(int) + sizeof(MessageType);
public MessageType Type;
private int _length;
public ReadOnlySpan<byte> Data
{
get => GetSpan(dataOffset, _length);
set
{
value.CopyTo(GetSpan(dataOffset, size - dataOffset));
_length = value.Length;
}
}
private ReadOnlySpan<byte> Bytes
{
get => GetSpan(0, _length + dataOffset);
set => value.CopyTo(GetSpan(0, size));
}
private Span<byte> GetSpan(int offset, int length)
{
fixed (MyMessage* src = &this)
{
return new((byte*)src + offset, length);
}
}
public ReadOnlySpan<byte> AsSpan()
=> Bytes;
public static implicit operator ReadOnlySpan<byte>(MyMessage buffer)
=> buffer.Bytes;
public static implicit operator MyMessage(ReadOnlySpan<byte> span)
=> new() { Bytes = span };
}
UPD: Немного доработал, теперь длина спана, получаемого из неявного преобразования или через AsSpan() будет равна длине полезной нагрузки. Это чтобы можно было сразу слать спан в сеть "как есть". При этом поле _length содержит длину блока данных, что валидно для формата передачи данных, чтобы принимающий сервер смог прочитать данные так же в эту же структуру. То есть не обзязательно слать ровно 2 килобайта. Правда принимающей стороне придется все-таки смотреть в это поле, потом вычитывать данные согласно указанной длине и только потом закидывать их в структуру. Я думаю, здесь идея должна быто прозрачна. Если я конечно правильно понял назначение этого всего.
Отправить
myStream.Write(buffer);
Получить
byte[] bytes = new byte[2048];
myStream.Read(bytes, 0, 8);
int length = BitConverter.ToInt32(bytes, 4);
myStream.Read(bytes, 8, length);
MyMessage buffer = bytes;
Скриншот ниже относится к одной из прошлых версий кода, но я его не стал менять, потому что он свою суть показыват верно.
Вывод в консоль
2048
HandShake
29
Привет, мир!
☺§Привет, мир!
Если свойство Bytes сделать публичным, то можно увидеть следующее:

А вот так может выглядеть управляемая версия, я все-такие ее больше рекомендую, так как она не содержит ограничения в 2 килобайта. И занимает ровно столько памяти, сколько требуется для хранения данных.
public record MyMessage
{
private const int headerSize = 8;
private const int lengthOffset = 4;
private readonly byte[] _bytes;
public MessageType Type
{
get => (MessageType)BitConverter.ToUInt32(_bytes, 0);
set => BitConverter.TryWriteBytes(_bytes, (uint)value);
}
public int Length => _bytes.Length;
public ReadOnlySpan<byte> Data => _bytes.AsSpan(headerSize);
public MyMessage(MessageType type, ReadOnlySpan<byte> data)
{
_bytes = new byte[data.Length + headerSize];
data.CopyTo(_bytes.AsSpan(headerSize));
Type = type;
BitConverter.TryWriteBytes(_bytes.AsSpan(lengthOffset), data.Length);
}
private MyMessage(ReadOnlySpan<byte> bytes)
{
int length = BitConverter.ToInt32(bytes[lengthOffset..]);
_bytes = new byte[length + headerSize];
bytes[.._bytes.Length].CopyTo(_bytes);
}
public ReadOnlySpan<byte> AsSpan()
=> _bytes;
public static implicit operator ReadOnlySpan<byte>(MyMessage message)
=> message._bytes;
public static implicit operator MyMessage(ReadOnlySpan<byte> span)
=> new(span);
}
Я сделал ее немутабельной, поэтому тестовый код немного изменился
MyMessage message = new(MessageType.HandShake, Encoding.UTF8.GetBytes("Привет, мир!"));
Console.WriteLine(message);
ReadOnlySpan<byte> bytes = message;
Console.WriteLine(Encoding.UTF8.GetString(message.Data));
Console.WriteLine(Encoding.UTF8.GetString(bytes));
Вывод в консоль
MyMessage { Type = HandShake, Length = 29, Data = System.ReadOnlySpan<Byte>[21] }
Привет, мир!
☺§Привет, мир!
Приятный бонус record, у нее встроенный .ToString(), отображающий ее содержимое.
Можно добавить еще производительности с помощью ArrayPool<byte> и Buffer.BlockCopy, но код станет значительно сложнее, придется адаптироваться к тому, что ArrayPool выдает массив не точно того размера, который нужен, и реализовать IDisposable с финализатором, чтобы массив точно возвращался в пул, когда сообщение больше не нужно.
