Protobuf-net ленивая потоковая десериализация полей

Общая цель: пропустить очень длинное поле при десериализации и при обращении к полю для чтения элементов прямо из потока без загрузки всего поля.

Примеры классов Сериализуемый / десериализуемый объект - FatPropertyClass.

[ProtoContract]
public class FatPropertyClass
{
    [ProtoMember(1)]
    private int smallProperty;

    [ProtoMember(2)]
    private FatArray2<int> fatProperty;

    [ProtoMember(3)]
    private int[] array;

    public FatPropertyClass()
    {

    }

    public FatPropertyClass(int sp, int[] fp)
    {
        smallProperty = sp;
        fatProperty = new FatArray<int>(fp);
    }

    public int SmallProperty
    {
        get { return smallProperty; }
        set { smallProperty = value; }
    }

    public FatArray<int> FatProperty
    {
        get { return fatProperty; }
        set { fatProperty = value; }
    }

    public int[] Array
    {
        get { return array; }
        set { array = value; }
    }
}


[ProtoContract]
public class FatArray2<T>
{
    [ProtoMember(1, DataFormat = DataFormat.FixedSize)]
    private T[] array;
    private Stream sourceStream;
    private long position;

    public FatArray2()
    {
    }

    public FatArray2(T[] array)
    {
        this.array = new T[array.Length];
        Array.Copy(array, this.array, array.Length);
    }


    [ProtoBeforeDeserialization]
    private void BeforeDeserialize(SerializationContext context)
    {
        position = ((Stream)context.Context).Position;
    }

    public T this[int index]
    {
        get
        {
            // logic to get the relevant index from the stream.
            return default(T);
        }
        set
        {
            // only relevant when full array is available for example.
        }
    }
}

Я могу десериализовать так: FatPropertyClass d = model.Deserialize(fileStream, null, typeof(FatPropertyClass), new SerializationContext() {Context = fileStream}) as FatPropertyClass; где model может быть, например:

    RuntimeTypeModel model = RuntimeTypeModel.Create();
    MetaType mt = model.Add(typeof(FatPropertyClass), false);
    mt.AddField(1, "smallProperty");
    mt.AddField(2, "fatProperty");
    mt.AddField(3, "array");
    MetaType mtFat = model.Add(typeof(FatArray<int>), false);

Это пропустит десериализацию array в FatArray<T>. Однако позже мне нужно будет прочитать случайные элементы из этого массива. Я пытался запомнить позицию потока перед десериализацией в BeforeDeserialize(SerializationContext context) методе FatArray2<T>. Как в приведенном выше коде: position = ((Stream)context.Context).Position;. Однако, похоже, это всегда конец потока.

Как я могу запомнить позицию потока, с которой начинается FatProperty2, и как я могу читать из него по случайному индексу?

Примечание. Параметр T в FatArray2<T> может относиться к другим типам, отмеченным [ProtoContract], а не только к примитивам. Также может быть несколько свойств типа FatProperty2<T> на разной глубине в графе объекта.

Метод 2: сериализуйте поле FatProperty2<T> после сериализации содержащего объекта. Итак, сериализуйте FatPropertyClass с префиксом длины, затем сериализуйте с префиксом длины все содержащиеся в нем массивы. Отметьте все эти свойства толстого массива атрибутом, и при десериализации мы сможем запомнить положение потока для каждого из них.

Тогда возникает вопрос, как нам прочитать из него примитивы? Это нормально работает для классов, использующих T item = Serializer.DeserializeItems<T>(sourceStream, PrefixStyle.Base128, Serializer.ListItemTag).Skip(index).Take(1).ToArray();, чтобы получить элемент по индексу index. Но как это работает для примитивов? Кажется, что массив примитивов не может быть десериализован с помощью DeserializeItems.

DeserializeItems с LINQ даже нормально используется? Делает ли он то, что я предполагаю (внутренне пропускает поток к нужному элементу - в худшем случае читает каждый префикс длины и пропускает его)?

С уважением, Юлиан


person Iulian    schedule 20.09.2014    source источник


Ответы (1)


Этот вопрос во многом зависит от актуальной модели - это не тот сценарий, который специально нацелен на то, чтобы сделать его удобным. Я подозреваю, что лучше всего было бы написать читалку вручную, используя ProtoReader. Обратите внимание, что есть некоторые уловки, когда дело доходит до чтения выбранных элементов, если самый внешний объект является List<SomeType> или подобным, но внутренние объекты обычно либо просто читаются, либо пропускаются.

Начав снова с корня документа через ProtoReader, вы можете довольно эффективно перейти к n-му элементу. Я могу привести конкретный пример позже, если хотите (я не вдавался в подробности, если вы не уверены, что это действительно будет полезно). Для справки, причина, по которой позиция потока здесь бесполезна, заключается в следующем: библиотека агрессивно перечитывает и буферизует данные, если вы специально не укажете ей ограничить ее длину. Это связано с тем, что такие данные, как "varint", трудно эффективно читать без большой буферизации, так как в конечном итоге это приведет к частым индивидуальным вызовам ReadByte(), а не просто работе с локальным буфером.


Это полностью непроверенная версия чтения n-го элемента массива подсвойства непосредственно из считывателя; обратите внимание, что было бы неэффективно вызывать это много раз один за другим, но должно быть очевидно, как изменить его для чтения диапазона последовательных значений и т. д .:

static int? ReadNthArrayItem(Stream source, int index, int maxLen)
{
    using (var reader = new ProtoReader(source, null, null, maxLen))
    {
        int field, count = 0;
        while ((field = reader.ReadFieldHeader()) > 0)
        {
            switch (field)
            {
                case 2: // fat property; a sub object
                    var tok = ProtoReader.StartSubItem(reader);
                    while ((field = reader.ReadFieldHeader()) > 0)
                    {
                        switch (field)
                        {
                            case 1: // the array field
                                if(count++ == index)
                                    return reader.ReadInt32();
                                reader.SkipField();
                                break;
                            default:
                                reader.SkipField();
                                break;
                        }
                    }
                    ProtoReader.EndSubItem(tok, reader);
                    break;
                default:
                    reader.SkipField();
                    break;
            }
        }
    }
    return null;
}

Наконец, обратите внимание, что если это большой массив, вы можете использовать «упакованные» массивы (см. Документацию по protobuf, но это в основном сохраняет их без заголовка для каждого элемента). Это было бы намного эффективнее, но учтите, что для этого требуется немного другой код чтения. Вы включаете упакованные массивы, добавляя IsPacked = true к [ProtoMember(...)] для этого массива.

person Marc Gravell    schedule 20.09.2014
comment
Спасибо, что прояснили, что происходит с потоком. Однако у меня есть один вопрос - как ограничить размер буфера? Пример того, как перейти к правильному полю, действительно был бы очень полезен в этом сценарии. - person Iulian; 21.09.2014
comment
@lulian см., например, редактирование, включая ограничение длины; обратите внимание, ограничение длины не является обязательным - person Marc Gravell; 21.09.2014
comment
Это работает. Но у меня есть еще 2 вопроса. 1. Как вы читаете элемент из упакованного массива, используя ProtoReader? 2. Если это не массив примитивов, как вы рекомендуете читать по индексу? Используя DeserializeItems<T>(...).Skip(index) или вручную с ProtoReader? - person Iulian; 21.09.2014
comment
@lulian 2: зависит от нескольких вещей. Обратите внимание, что если вы передадите модель в средство чтения, будут открыты методы десериализации подобъекта, поэтому вам не нужно делать все это вручную. 1: см. Первую часть (где есть тип упакованного провода) здесь: github.com/mgravell/protobuf-net/blob/master/protobuf-net/ - person Marc Gravell; 21.09.2014