Передайте буфер char [] в XmlSerializer - PullRequest
2 голосов
/ 13 апреля 2020

У меня есть XML, который хранится в массиве символов - char[] - и у меня есть длина содержимого данных в переменной int. Мне нужно десериализовать данные с помощью XmlSerializer.

Из соображений производительности мне следует избегать выделения строкового объекта, поскольку данные обычно имеют размер> 85 КБ и приводят к объекту Gen2.

Есть ли любой способ передать char [] в XmlSerializer без преобразования его в строку? Он принимает Stream или TextReader, но я не могу найти способ построить его из char[].

Я представляю что-то подобное (за исключением того, что C# не имеет CharArrayStream или CharArrayReader):

public MyEntity DeserializeXmlDocument(char [] buffer, int contentLength) {
    using (var stream = new CharArrayStream(buffer, contentLength))
    {
        return _xmlSerializer.Deserialize(stream) as MyEntity;
    }
}

Точно так же, как некоторая дополнительная информация, мы находимся в момент, когда мы профилируем существующий код и определили болевую точку, так что это не случай «преждевременной оптимизации» или «проблема XY».

Ответы [ 2 ]

1 голос
/ 18 апреля 2020

Подкласс TextReader довольно просто читать из массива символов или его эквивалента. Вот версия, которая принимает ReadOnlyMemory<char>, которая может представлять фрагмент массива символов string или char []:

public sealed class CharMemoryReader : TextReader
{
    private ReadOnlyMemory<char> chars;
    private int position;

    public CharMemoryReader(ReadOnlyMemory<char> chars)
    {
        this.chars = chars;
        this.position = 0;
    }

    void CheckClosed()
    {
        if (position < 0)
            throw new ObjectDisposedException(null, string.Format("{0} is closed.", ToString()));
    }

    public override void Close() => Dispose(true);

    protected override void Dispose(bool disposing)
    {
        chars = ReadOnlyMemory<char>.Empty;
        position = -1;
        base.Dispose(disposing);
    }

    public override int Peek()
    {
        CheckClosed();
        return position >= chars.Length ? -1 : chars.Span[position];
    }

    public override int Read()
    {
        CheckClosed();
        return position >= chars.Length ? -1 : chars.Span[position++];
    }

    public override int Read(char[] buffer, int index, int count)
    {
        CheckClosed();
        if (buffer == null)
            throw new ArgumentNullException(nameof(buffer));
        if (index < 0)
            throw new ArgumentOutOfRangeException(nameof(index));
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count));
        if (buffer.Length - index < count)
            throw new ArgumentException("buffer.Length - index < count");

        return Read(buffer.AsSpan().Slice(index, count));
    }

    public override int Read(Span<char> buffer)
    {
        CheckClosed();

        var nRead = chars.Length - position;
        if (nRead > 0)
        {
            if (nRead > buffer.Length)
                nRead = buffer.Length;
            chars.Span.Slice(position, nRead).CopyTo(buffer);
            position += nRead;
        }
        return nRead;
    }

    public override string ReadToEnd()
    {
        CheckClosed();
        var s = position == 0 ? chars.ToString() : chars.Slice(position, chars.Length - position).ToString();
        position = chars.Length;
        return s;
    }

    public override string ReadLine()
    {
        CheckClosed();
        var span = chars.Span;
        var i = position;
        for( ; i < span.Length; i++)
        {
            var ch = span[i];
            if (ch == '\r' || ch == '\n')
            {
                var result = span.Slice(position, i - position).ToString();
                position = i + 1;
                if (ch == '\r' && position < span.Length && span[position] == '\n')
                    position++;
                return result;
            }
        }
        if (i > position)
        {
            var result = span.Slice(position, i - position).ToString();
            position = i;
            return result;
        }
        return null;
    }

    public override int ReadBlock(char[] buffer, int index, int count) => Read(buffer, index, count);
    public override int ReadBlock(Span<char> buffer) => Read(buffer);

    public override Task<String> ReadLineAsync() => Task.FromResult(ReadLine());
    public override Task<String> ReadToEndAsync() => Task.FromResult(ReadToEnd());
    public override Task<int> ReadBlockAsync(char[] buffer, int index, int count) => Task.FromResult(ReadBlock(buffer, index, count));
    public override Task<int> ReadAsync(char[] buffer, int index, int count) => Task.FromResult(Read(buffer, index, count));
    public override ValueTask<int> ReadBlockAsync(Memory<char> buffer, CancellationToken cancellationToken = default) =>
        cancellationToken.IsCancellationRequested ? new ValueTask<int>(Task.FromCanceled<int>(cancellationToken)) : new ValueTask<int>(ReadBlock(buffer.Span));
    public override ValueTask<int> ReadAsync(Memory<char> buffer, CancellationToken cancellationToken = default) =>
        cancellationToken.IsCancellationRequested ? new ValueTask<int>(Task.FromCanceled<int>(cancellationToken)) : new ValueTask<int>(Read(buffer.Span)); 
}

Затем используйте его с одним из следующие методы расширения:

public static partial class XmlSerializationHelper
{
    public static T LoadFromXml<T>(this char [] xml, int contentLength, XmlSerializer serial = null) => 
        new ReadOnlyMemory<char>(xml, 0, contentLength).LoadFromXml<T>(serial);

    public static T LoadFromXml<T>(this ReadOnlyMemory<char> xml, XmlSerializer serial = null)
    {
        serial = serial ?? new XmlSerializer(typeof(T));
        using (var reader = new CharMemoryReader(xml))
            return (T)serial.Deserialize(reader);
    }
}

Например,

var result = buffer.LoadFromXml<MyEntity>(contentLength, _xmlSerializer);

Примечания:

  • A char [] Массив символов имеет в основном то же содержимое, что и Поток памяти в кодировке UTF-16 без BOM , поэтому можно создать пользовательскую реализацию Stream, напоминающую MemoryStream, которая представляет каждый char в виде двух байтов, как это делается в этот ответ на Как мне сгенерировать поток из строки? от György Kőszeg . Тем не менее, выглядит немного сложно сделать это правильно, поскольку правильное использование всех методов async кажется нетривиальным.

    После этого XmlReader все равно потребуется обернуть пользовательский поток в StreamReader, который «декодирует» поток в последовательность символов, правильно выводя кодировку в процессе (что, как я наблюдал, иногда может быть сделано неправильно, например, когда заявлено кодирование, объявление XML не соответствует фактическому кодированию).

    Я решил создать пользовательский TextReader, а не пользовательский Stream, чтобы избежать ненужного шага декодирования, и поскольку реализация async казалась менее обременительной.

  • Представление каждый char в виде одного байта через усечение (например, (byte)str[i]) повредит XML, содержащий любые многобайтовые символы.

  • Я не выполнял никаких настроек производительности в вышеупомянутой реализации .

Демонстрационная скрипка здесь .

0 голосов
/ 13 апреля 2020

Я переделал код, связанный @ György Kőszeg с классом CharArrayStream. Пока это работает в моих тестах:

public class CharArrayStream : Stream
{
    private readonly char[] str;
    private readonly int n;

    public override bool CanRead => true;
    public override bool CanSeek => true;
    public override bool CanWrite => false;
    public override long Length => n;
    public override long Position { get; set; } // TODO: bounds check

    public CharArrayStream(char[] str, int n)
    {
        this.str = str;
        this.n = n;
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        switch (origin)
        {
            case SeekOrigin.Begin:
                Position = offset;
                break;
            case SeekOrigin.Current:
                Position += offset;
                break;
            case SeekOrigin.End:
                Position = Length - offset;
                break;
        }

        return Position;
    }

    private byte this[int i] => (byte)str[i];

    public override int Read(byte[] buffer, int offset, int count)
    {
        // TODO: bounds check
        var len = Math.Min(count, Length - Position);
        for (int i = 0; i < len; i++)
        {
            buffer[offset++] = this[(int)(Position++)];
        }
        return (int)len;
    }

    public override int ReadByte() => Position >= Length ? -1 : this[(int)Position++];
    public override void Flush() { }
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
    public override string ToString() => throw new NotSupportedException();
}

Я могу использовать это следующим образом:

public MyEntity DeserializeXmlDocument(char [] buffer, int contentLength) {
    using (var stream = new CharArrayStream(buffer, contentLength))
    {
        return _xmlSerializer.Deserialize(stream) as MyEntity;
    }
}

Спасибо, @ György Kőszeg!

...