При отладке не видно данные расположенные в 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 шт):

Автор решения: aepot

Наверное дело в том, что 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 с финализатором, чтобы массив точно возвращался в пул, когда сообщение больше не нужно.

→ Ссылка