Проблемы передачи списка перечислений или Почему абстракции «текут»

    Все нетривиальные абстракции дырявы

    Джоэл Спольски – Закон дырявых абстракций


    А иногда дырявятся и довольно простые абстракции

    Автор этой статьи



    Большинство современных разработчиков знакомы с «законом дырявых абстракций» по знаменитой заметке Джоэла Спольски с одноименным названием. Заключается этот закон в том, что как бы ни был хорош протокол взаимодействия, фреймворк или набор классов, моделирующих предметную область, рано или поздно нам приходится спускаться на уровень ниже и разбираться с тем, как же эта абстракция устроена. Внутреннее устройство абстракции должно быть проблемой самой абстракции, но возможно это только в наиболее общих случаев и лишь до тех пор, пока все идет хорошо (*).

    Когда-то давно, в «небольшой» мелкомягкой компании решили, а почему бы нам не «абстрагироваться» от местоположения объекта и сделать сам факт того, является ли объект локальным или удаленным, лишь «деталью реализации». Так появились технологии DCOM и ее наследник .NET Remoting, которые скрывали от разработчика, является ли объект удаленным или нет. При этом появились все эти «прозрачные прокси», которые позволяли работать с удаленным объектом, даже не зная об этом. Однако, со временем выяснилось, что эта информация архиважна для разработчика, поскольку удаленный объект может генерировать совершенно другой перечень исключений, да и стоимость работы с ним несравнимо выше, чем взаимодействие с локальным объектом.



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

    Подобных примеров, когда нам нужно знать не только видимое поведение (абстракцию), но и понимать внутреннее устройство (реализацию), довольно много. В большинстве языков программирования работа с разными типами коллекций делается очень похожим образом. Коллекции могут «прятаться» за базовыми классами или интерфейсами (как в .NET), или использовать какой-то другой способ обощения (как, например, в языке С++). Но, несмотря на то, что мы можем работать с разными коллекции практически одинаково, мы не можем полностью «отвязать» наши классы от конкретных типов коллекций. Несмотря на видимое сходство, нам нужно понимать, что лучше использовать в данный момент: вектор или двусвязный список, hash-set или sorted set. От внутренней реализации коллекции зависят сложности основных операций: поиска элемента, вставки в середину или в конец коллекции и знать о таких различиях просто необходимо.

    Давайте рассмотрим конкретный пример. Все мы знаем, что такие типы как List<T> (или std::vector в С++) реализованы на основе простого массива. Если коллекция уже заполнена, то при добавлении нового элемента будет создан новый внутренний массив, при этом он «вырастит» не на один элемент, а несколько сильнее (**). Многие знают о таком поведении, но в большинстве случаев мы можем не обращать на это никакого внимания: это является «личной проблемой» класса List<T> и нам до нет никакого дела.

    Но давайте предположим, что нам нужно передать список перечислений (enum-ов) через WCF или просто сериализовать такой список с помощью классов DataContractSerializer или NetDataContractSerializer(***). При этом перечисление объявлено следующим образом:

    public enum Color
    {
      Green = 1,
      Red,
      Blue
    }

    * This source code was highlighted with Source Code Highlighter.


    Не обращайте внимания на то, что это перечисление не помечено никакими атрибутами, это не является помехой для NeDataContractSerializer-а. Главная особенность этого перечисления заключается в том, что в нем нет нулевого значения; значения перечислений начинаются с 1.

    Особенность сериализации перечислений в WCF заключается в том, что вы не сможете сериализовать значение, не принадлежащее этому перечислению.

    public static string Serialize<T>(T obj)
    {
      // Используем именно NetDataContractSerializer, хотя в данном случае
      // поведение DataContractSerializer аналогичным
      var serializer = new NetDataContractSerializer();
      var sb = new StringBuilder();
      using (var writer = XmlWriter.Create(sb))
      {
        serializer.WriteObject(writer, obj);
        writer.Flush();
        return sb.ToString();
      }
    }
    Color color = (Color) 55;
    Serialize(color);

    * This source code was highlighted with Source Code Highlighter.


    При попытке выполнить этот код, мы получим следующее сообщение об ошибке: Enum value '55' is invalid for type Color' and cannot be serialized.. Такое поведение является вполне логичным, ведь таким способом мы защищаемся от передачи неизвестных значений между разными приложениями.

    Теперь давайте попробуем передать коллекцию из одного элемента:

    var colors = new List<Color> {Color.Green};
    Serialize(colors);

    * This source code was highlighted with Source Code Highlighter.


    Однако этот, с виду вполне безобидный код, также приводит к ошибке времени выполнения с тем же самым содержанием и отличие заключается лишь в том, что сериализатор не может справиться со значением перечисления, равным 0. На что за … Откуда мог вообще взялся 0? Мы ведь пытаемся передать простую коллекцию с одним элементом, при этом значение этого элемента абсолютно корректно. Однако DataContractSerializer/NetDataContractSerializer, как и старая добрая бинарная сериализация, использует рефлексию для получения доступа ко всем полям. В результате чего, все внутреннее представление объекта, которое содержится как в открытых, так и закрытых полях, будет сериализовано в выходной поток.

    Поскольку класс List<T> построен на основе массива, то при сериализации будет сериализован массив целиком, не зависимо от того, сколько элементов содержится в списке. Так, например, при сериализации коллекции из двух элементов:

    var list = new List<int> {1, 2};
    string s = Serialize(list);

    * This source code was highlighted with Source Code Highlighter.


    В выходном потоке мы получим не два элемента, как мы могли бы ожидать, а 4 (т.е. количество элементов, соответствующих свойству Capacity, а не Count):

    <ArrayOfint>
      <_items z:Id="2" z:Size="4">
        <int>1</int>
        <int>2</int>
        <int>0</int>
        <int>0</int>
      </_items>
      <_size>2</_size>
      <_version>2</_version>
    </ArrayOfint>

    * This source code was highlighted with Source Code Highlighter.


    В таком случае, причина сообщения об ошибке, которое возникает при сериализации списка перечислений, становится понятной. Наше перечисление Color не содержит значение, равного 0, а именно таким значением заполняются элементы внутреннего массива списка:

    image

    Это еще один пример «протекания» абстракции, когда внутренняя реализация даже такого простого класса, как List<T> может помешать нам его нормально сериализовать.

    Решение проблемы



    Решений у этой проблемы несколько, при этом каждое из решений обладает своими недостатками.

    1. Добавление значения по умолчанию


    Самым простым решением этой проблемы является добавление в перечисление значения, равного 0 либо изменить значение одного из существующих элементов:

    public enum Color
    {
      None = 0,
      Green = 1, // или Green = 0
      Red,
      Blue
    }

    * This source code was highlighted with Source Code Highlighter.


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

    2. Передача коллекции без «пустых» элементов


    Вместо того чтобы делать что-либо с перечислением, можно добиться того, чтобы коллекция не содержала таких пустых элементов. Сделать это можно, например, таким образом:

    var li1 = new List<Color> { Color.Green };
    var li2 = new List<Color>(li1);

    * This source code was highlighted with Source Code Highlighter.


    В этом случае, переменная li1 будет содержать три дополнительных пустых элемента (при этом Count будет равен 1, а Capacity4), а переменная li2 – нет (внутренний массив второго списка будет содержать только 1 элемент).

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

    3. Использование других типов коллекций в интерфейсе сервисов


    Использование других структур данных, таких как массив, либо вместо DataContractSerializer-а использовать XML-сериализацию, которая использует только открытые члены, решит эту проблему. Но насколько это удобно или нет, решать уже вам.

    Абстракции текут, точка. Вот почему рыться во внутренней реализации разных библиотек весьма полезно. Даже если эта библиотека отлично скрывает все свои детали, рано или поздно вы столкнетесь с ситуацией, когда без знаний внутреннего ее устройства вы не сможете решить свою проблему. Дебажьте, разбирайтесь с внутренним устройством и не бойтесь, что оно изменится в будущем; не факт, что это вам понадобится, но как минимум, это интересно!

    З.Ы. Кстати, дважды подумайте, чтобы передавать значимые типы через WCF в типе List<T>. Если у вас будет коллекция из 524-х элементов, то будут переданы еще 500 дополнительных объектов значимого типа!



    (*) Джоэл далеко не первый и не последний автор, предложивший отличную метафору для этих целей. Так, например, Ли Кэмпбел однажды отлично сказал об этом же, но несколько другими словами: «Вы должны понимать как минимум на один уровень абстракции ниже того уровня, на котором кодируете». Подробности в небольшой заметке: О понимании нужного уровня абстракции.

    (**) Обычно подобные структуры данных увеличивают свой внутренний массив в два раза. Так, например, при добавлении элементов в List<T>, «емкость» будет изменяться таким образом: 0, 4, 8, 16, 32, 64, 128, 256, 512, 1024 …

    (***) Разница между двумя основными типами сериализаторов WCF достаточно важна. NetDataContractSerializer в отличие от DataContractSerializer, нарушает принципы SOA и добавляет информацию о CLR типе в выходной поток, что нарушает «кроссплатформенность» сервис-ориентированной парадигмы. Подробнее об этом можно почитать в заметках: Что такое WCF или Декларативное использование NetDataContractSerializer.
    Поделиться публикацией
    Комментарии 47
      +41
      Во-первых, абстракция нужна не для того, чтобы скрыть низкоуровневые детали, а для того, чтобы начать работать в терминах более высокого уровня. Хорошая абстракция — это замкнутая система с определёнными гарантиями. Например, алгебра — это абстракция над множеством предметов реального мира, позволяющая проводить некоторые операции без учёта свойств нижележащих реальных предметов. Если у вас есть 6 яблок, то вы можете поделить их между 2 людьми просто разделив число 6 на 2, без учёта цвета яблок, их вкуса, размера и т.д. Алгебраическая абстракция в данном случе не предоставляет гарантии, что люди останутся довольны тем, какие яблоки им достались. Но если вам нужно максимально удовлетворить потребности потребности обоих людей, то простая алгебра является просто неподходящей абстракицей — это задачи оптимизации, и она требует соответствующих моделей.
      Точно так же и с технологиями: если .Net Remoting скрывает от разработчика, где именно будет выполняться процедура (локально или удалённо), а для разработчика эта информация важна, то это значит только то, что данная абстракция не подходит для данного случая. Не сама абстракция плоха, а выбор этой абстракции для данной конкретной задачи — эта абстракция не даёт тех гарантий и инструментов, которые нужны программисту.

      Во-вторых, стоит вспомнить об уровнях абстракции, которые позволяют строить «слоистые» приложения. Здесь удобно вспомнить операционные системы. Ядро ОС пишется одно для всех возможных платформ (опустим отдельные оптимизации и всякие ответвления типа мобильных версий, etc.), а непосредственно на железо ставится через hardware abstraction layer — уровень абстракции над «железом». Мы знаем, что все низкоуровневые операции типа физического чтения из памяти и переключения потоков выполняются на любом железе за примерно одинаковое время (а если и нет, то мы всё равно ничего не можем с этим поделать), поэтому мы абстрагируемся от железа и получаем возможность заменять нижележащую реализацию без внесения изменений в текущий уровень.

      Обобщая: абстракция — это совокупность понятий/инструментов и связанных с ними гарантий. Абстрактный список гарантирует хранение коллекции объектов и определённый набор операций, но не гарантирует асимптотическую скорость этих операций. TCP гарантирует доставку пакетов, но не описывает гарантий по времени этой доставки. Алгебра гарантирует равномерное распределение яблок между людьми, но не гарантирует, что люди останутся довольны.
        +4
        Ваш комментарий отлично дополняет статью. ТС как раз и говорит о том, что абстракции это в принципе хорошо и пользоваться ими надо, но в случае возникновения ошибок (когда абстракции начинают течь), нужно лезть на более низкий уровень, разбираться как это устроено и возвращаться обратно наверх с прокаченным скиллом.

        Это как если бы вы плыли (пардоньте, «шли») на корабле (неплохая такая абстракция над досками), и в трюме образовалась бы течь, то вам пришлось бы спутиться в трюм и разобраться в какой именно доске пробоина и заделать ее. А потом можно вернуться к штурвалу и пользоваться абстракцией дальше.
          0
          Тут вопрос частично в терминологии, частично в отношении ко всему «стеку» используемых технологий.

          На мой взгляд, неправильно говорить, что абстракция «потекла» — это как бы намекает, что абстракция сама по себе плоха, когда на самом деле речь идёт просто об использовании её не к месту.

          Опять же, по моему личному убеждению знать нижележащие абстракции надо всегда. Абстракция — это не избавление от необходимости что-то изучать (хотя из-за вечной нехватки времени это, конечно, тоже имеет смысл, но тут вопрос не в том, надо или не надо, тут вопрос исключительно в приоритетах), это возможность не держать в голове все детали в каждый конкретный момент времени. Если вы управляете кораблём, то думаете о парусах, ветре, картах и маршрутах. У вас нет физической возможности держать в голове ещё и всю конструкцию коробля, все элементы трюма, доски и сваи, и уж тем более вы не сможете эффективно вести корабль, если будете думать о свойствах древесины, плотности натяжения парусины, химическом составе смолы и т.д. Тем не менее, это не значит, что вы не должны всё это знать — парусина изнашивается, и во время стоянки стоит проверить её целостность, доски гниют, и нужно периодически смазывать их смолой и т.д. Дело не в возможной течи в трюме, делов том, что без этих знаний вы всё равно не сможете долго содержать корабль. Однако пока вы стоите у штурвала, абстракция управления кораблём позволяет вам держать в голове только самое главное, вытеснив неактульные на данный момент детали в долгосрочную память.
          +1
          Спасибо за развернутый комментарий. Просто напомню, что в CS и, в частности, в ООП также существует понятие абстракции, которое семантически очень схоже с общепринятым и приведенным вами, но имеет и дополнительный, важный оттенок.

          Вот определение абстракции от Буча:

          Абстракция выделяет существенные характеристики некоторого объекта, отличающие его от всех других видов объектов и, таким образом, четко определяет его концептуальные границы с точки зрения наблюдателя.

          И дальше:
          Абстрагирование концентрирует внимание на внешних особенностях объекта и позволяет отделить самые существенные особенности поведения от несущественных.


          Вот у нас и получается, что хотя класс List и абстрагирует (т.е. прячет) от пользователя несущественные детали реализации, такие внутреннее устройство, и в большинстве случаев, нас это вполне устраиват, но иногда бывают случаи, когда эти несущественные детали реализации «пролазят» и становятся «существенными». Вот именно тогда и говорят, что абстракция «течет».
            0
            Опять же, вопрос терминологии — если вам в данной конкретной ситуации важная реализация объекта, то вы мыслите в терминах внутренней структуры List-а. Массив, связный список, дерево — вот абстракции, которые вы ищете, а не обобщённую абстракцию списка. Потому что в данной ситуации структура является существенной характеристикой.
          +1
          Этот вариант вполне работоспособный, но весьма «хрупкий»: сломать работающий код не составит никакого труда. Безобидное изменение со стороны вашего коллеги в виде удаления ненужной промежуточной коллекции и все, приплыли.

          Есть ещё List.TrimExcess().
            0
            Если не изменяет память, то конструктор List принимает int, задающий capacity.
            т.о.
            var c = new List (1) { Color.Green };

            решит проблему
              0
              Да, но это вводит дикую связность в коде. Получается, что код, добавляющий этот элемент должен четко знать, какую проблему он решает и должен быть уверенным в том, что никто другой не будет добавлять никаких других элементов. Поскольку, опять таки, добавление новых элементов, сломает работу системы.
                +1
                наверное, возможна ситуация, когда:
                var c = new List();
                c.append(Color.Green);
                c.append(Color.Red);
                c.append(Color.Blue);
                c.append(Color.Green);


                А потом в функции типа SaveColorsToFile мы получаем этот список и пытаемся его сериализовать. Строить новый список смысла не вижу, а тримануть можно (хоть по стоимости это примерно одно и то же).
              +1
              Вообще в данном случае казалось бы проблема с тем что сериализация у листа пишет что-то лишнее. Ну и это в сочетании с тем что enum на особых правах (not nullable, но я не знаю C# так что не могу гарантировать что понимаю эту часть до конца).
              Так что не особо и дырявая абстракция, лишь ее реализация.
                0
                Напомню, что дырявость асбтракции и означает, что детали реализации «протекают» и пользователь больше не может принимать правильные решения только на основе публичной информации, ему нужно понимать и внутренние детали также.
                +7
                Сергей, спасибо большое за статью. По поводу вашего стиля есть замечание.

                Вы делаете сноски, ок. Но вспомните, сноски хороши когда читаешь бумажную книжку — увидел сноску, глаза вниз в конец страницы, почитал, глаза вверх. Т.е. для бумажной книжки/журнала это работает.

                У вас в статьях сноску видишь, но не понимаешь, чего с ней сделать. Ну то есть, понятно, что надо pg-down-ить куда-то вниз, читать, а потом возвращаться назад. Но это гораздо неудобнее, чем в предыдущем варианте.

                В результате сноску читаешь как часть статьи, отдельно от контекста, в котором вы ее представляли. Пофиксите это, плиз!
                  –1
                  Что бы минимизировать дырявость, нужно абстракцию стоить на основе ортогонального базиса элементов, каждый из которых можно легко подменить или улучшить, добившись таким образом максимальной расширяемости. В данном случае все три решения кажутся костылями, имхо самое логичное, если проблема с сериализацией списка, значит нужно написать кастомное правило которое для списков будет делать ToArray() и уже сериализовать массив.
                    0
                    Первое предложение вывихнуло мой мозг:)
                    О каком кастомном правиле идет речь? и кто будет обеспечивать его выполнение? И кто его будет поддерживать, как пострадает или нет сопровождаемость? Конечно, WCF аццки расширяемый зверь, с большим количество точек расширения, но использовать все эти навороты для выпрямления передачи списка перечислений, ИМО оверкил.

                    Да, все три решения — костыли. Можете привести конкретное решение и наверняка оно тоже будет костылем. Отличаться будет навороченность костыля: мои костыли — это простые металлические пруты, но можно сделать и кресло с электронным управлением. Но это лишь сделает этот костыль навороченным, но вряд ли понятным.

                    Да, и это костыли по определению, мы ведь подпираем то, что должно работать так, как мы бы того хотели, но работает это дело не так.
                      0
                      что-то мне кажется, что если в ортогональном базисе элементов убрать какой-то базисный элемент, то не так-то просто будет найти для него улучшенную (да или такого же уровня) замену, придется перестраивать весь базис.
                      +3
                      Просто для List надо сериализовать по Count, а не Capacity. Баг в WCF.
                        –1
                        Совершенно верно — баг в WCF.
                          +1
                          Это не баг в WCF. WCF ничего не знает про List, он сериализирует его так же как и любой другой класс. Просто находит там массив в поле и сериализирует его целиком.
                          WCF предоставляет возможность определить собственный сериализатор для класса. Реализовывать 100 сериализаторов для каждого типа коллекции в рамках WCF тоже было бы неправильно (как быть с юзер-коллециями? чем они отличаются от стандартных?)
                          С другой стороны это и не баг BCL, потому что реализовывать ISerializable в List было бы еще глупее (почему *базовая* библиотека вдруг должна зависеть от какого-то WCF?)
                          Отсюда вывод: в тех редких случаях, когда юзер сталкивается с такой проблемой ему нужно писать свою обертку/наследника от List с сериализацией и использовать его, либо искать альтернативные решения (в статье их аж 3 штуки).
                          +2
                          Ваша проблема как раз в том, что вы не использовали абстракцию (T[], IEnumerable{of T}), а взяли конкретный тип (List{of T}).

                          В среднем, List{of T} вообще не следует использовать в публичных интерфейсах.
                            +3
                            … а вторая ваша проблема в том, что вы нарушили гайдлайны по разработке перечислений:

                            «Do provide a value of zero on simple enumerations.

                            If possible, name this value None. If None is not appropriate, assign the value zero to the most commonly used value (the default).»

                            (http://msdn.microsoft.com/en-us/library/ms229058.aspx)
                              0
                              Я люблю гайдлайны, но не люблю следовать им слепо. Вот пример, у меня есть перечисление бизнесс-объектов, при этом в принципе нет значения, которое бы соответстовало значению None, т.е. валидное значение должно быть всегда. И при этом эти перечисления завязаны на базу данных и значения я изменить не могу тоже не могу.

                              Т.е. здесь участвую не только я, но и еще 33 индуса, которые за последние 15 лет наконопатили очень много всего. И добавление перечисления равного 0 может нарушить работу двух тонн существующего кода.

                              Что мне делать? Да, гайдлайны нарушены, но изменить я этого не могу.
                                0
                                Расскажите мне, что нарушит добавление значения 0? Упадет код, который его не использует? Не верю.
                                  0
                                  Кто-то потом попытается записать это значение в БД и мы получим ошибку нарушения внешнего ключа, поскольку енум мапится на id в базе.
                                    0
                                    «Кто-то потом попытается записать это значение в БД и мы получим ошибку нарушения внешнего ключа, поскольку енум мапится на id в базе.»
                                    Согласитесь, что это не существующий код, а новый, потому что существующий берет значения только из тех частей enum, которые были на момент его создания. Не?

                                    А вообще, Obsolete("...", true) — и код с явным использованием этого значения даже не скомпилируется. А все места, где оно используется неявно (типа преобразований) и так должны быть обложены проверками.
                                      0
                                      Сейчас есть код:
                                      {code}
                                      int id = (int)myEnumValue;
                                      {code}
                                        +1
                                        Сори, рано отправил.

                                        Вопрос не в том, что должно было бы быть, а в том, что есть сейчас. А сейчас у нас есть два ведра гов#$да, в который вносить изменения без лишнего повода просто страшно. Поскольку эти перечисления проходят из .NET, через сервисы сериализации на никсы в плюсовый мир, там они могут проходить уже не как перечисления, а как числовые значения, при этом место сохранения данных в БД может уже и не знать о том, что корректно, а что нет.

                                        Соответственно такие изменния могут нарушить очень далекие части приложения, которые не найти с помощью Find All References. Вот потому, я и предпочту какой-нибудь другой вариант и постараюсь не добавлять пустое значение перечисления, если на то не будет крайней необходимости.

                                        З.Ы. Теперь, я надеюсь понятно, что Obsolete в моем случае не поможет.
                                          0
                                          Приведенный вами код не может сломаться из-за добавления нового члена в enum.
                                      0
                                      Попробуйте написать структуру, с полем типа вашего перечисления. А потом инициализируйте где-нибудь переменную этого структурного типа при помощи конструктора по умолчанию. Вас ждет сюрприз. Отсутствие элемента в перечислении не гарантия отсутствия левых значений в рантайме. Если критична важность корректности значения, например, при записи в БД, то нужно контролировать значение в коде, который производит запись. Контроль корректности данных только на «клиентской» стороне (не в смысле клиент-сервера, а в смысле архитектуры приложения, ORM) — очень плохая практика.
                                0
                                Кстати, в WCF все не так просто. SOA вообще не предполагает использование полиморфизма и базовых классов, так что передача IEnumerable в WCF — это тоже не выход.

                                З.Ы. T[] — это такой же конкретный тип, как List;)
                                  0
                                  «Кстати, в WCF все не так просто. SOA вообще не предполагает использование полиморфизма и базовых классов, так что передача IEnumerable в WCF — это тоже не выход.»
                                  В WCF все очень просто. Надо использовать наименьший общий знаменатель. List таким не является. Массив — является. IEnumerable — удобная абстракция на уровне кода, но для SOA всегда можно использовать массивы, потому что, де-факто, именно ими мы внутри сериализованной информации и кидаемся.

                                  «T[] — это такой же конкретный тип, как List;)»
                                  Массив — абстракция (коллекция данных). List — уже нет, это конкретный избыточный функционал.

                                  Отсюда и ошибка.
                                    0
                                    Абстракция — это IEnumerable, которая моделирует последовательность. Массив — это такая же специализация этой абстракции, что и двусвязный список.

                                    Еще раз напомню, что мы живем не в идеальном мире, и такой вопрос, как согласованность кода тоже может играть важную роль. Если у нас уже есть десяток контактов, каждый из которых уже использует типы List, то в одном месте использовать массив или любой другой тип будет, как минимум, не последовательно.

                                    З.Ы. Наименьший общий знаменатель у всех коллекций — это IEnumerable. Массив подходит лишь тем, что он является наиболее распростаренной коллекцией, и вы получите максимальную универсальность вашего интерфейса.
                                      0
                                      «Если у нас уже есть десяток контактов, каждый из которых уже использует типы List»
                                      … то эти контракты все равно написаны неправильно, и ваша проблема — из-за этих контрактов. Дальше вы можете решать ее как угодно, но проблема именно в них.

                                      «Наименьший общий знаменатель у всех коллекций — это IEnumerable»
                                      … в .net. Но не во всех системах, с которыми производится взаимодействие.
                                0
                                Вообще, спорным является уже утверждение «это является «личной проблемой» класса List». Это нифига не является его личной проблемой. В документации ко всяким векторам\списками\мапам\словарям и т.д. обязательно указывается как данные хранятся в памяти и какую сложность имеют базовые операции (добавление, удаление, поиск и т.д.). Это всё вовсе даже не «личные» проблемы класса, а часть абстракции, которая должна учитываться тем, кто её использует.
                                  0
                                  Он является его личной проблемой в подавляющем большинстве случаев. Насколько часто лично Вы задумываетесь над тем, как устроен лист? Я, честно говоря, довольно редко. Меня вполне устраивает его поведение по умолчанию, и тот факт, что он использует внутренний массив меня нисколько не напрягает. Но сериализация — это как раз тот случай, когда его внутреннее поведение противоречит политике сериализации перечислений, что и приводит к ошибке времени выполнения.
                                    0
                                    Это, конечно, не в блоге .NET будет сказано, но я постоянно задумываюсь как оно внутри храниться и как быстро ищется, поскольку пишу на С\С++ и весьма низкоуровневые вещи. Когда какая-то часть драйвера вызывается 1000 раз в секунду и там нужен доступ к какой-то коллекции — тут безусловно нужно думать как она организована.
                                  +1
                                  а чем вам не угодил List.TrimExcess?
                                    0
                                    Да всем он мне угодил:) Вы всегда вызываете его при попытке сериализации листа? Я нет. Меня напрягают любые обязанности, поскольку если что-то может быть не сделано, то по всем законам Мура рано или поздно это будет не сделано, что приведет к ошибке времени выполнения на продакшне.
                                      0
                                      Я поторопился. TrimExcess в этом случае вообще не подходит. Вот, что сказано по приведеннной вами же ссылке:

                                      This method can be used to minimize a collection's memory overhead if no new elements will be added to the collection. The cost of reallocating and copying a large List can be considerable, [b]however, so the TrimExcess method does nothing if the list is at more than 90 percent of capacity[/b]. This avoids incurring a large reallocation cost for a relatively small gain.


                                      Т.е. этот вариант не является решением проблемы, поскольку он ничего не гарантирует.
                                        0
                                        т.е. вам какбэ намекают — не используйте динамические списки при сериализации, иначе потому буите ныть, что абстракции текут.
                                      0
                                      > при этом он «вырастит» не на один элемент
                                      либо «вырастет», либо «вырастит хвост»
                                        0
                                        Да, как тут уже писали, и на мой взгляд, к дырявым абстракциям этот баг имеет слабое отношение. Проблема в том, что в классе List не реализована поддержка сериализации через Data Contracts. Механизм сериализации не нарушает абстракцию потому, что ему надо получить функциональность базового слоя, а просто напросто игнорирует все абстракции, напрямую обходя все поля графа объектов. Такова его суть. И при таком подходе требуется поддержка такого вида сериализации от самих сериализуемых классов. Тут дырявых абстракций нет, тут есть несовместимость поведения классов.
                                        И, к слову, в списке вполне корректно реализована поддержка «классической» сериализации, ISerializable.
                                        И еще на мой взгляд в данной ситуации использование класса List является примером избыточного использования классов. Предназначение класса List — возможность простой модификации состава и порядка коллекции неких элементов. Зачем вам List при десериализации? Вы точно собираетесь модифицировать его содержимое после десериализации?
                                          0
                                          По поводу использования типа List см. здесь.

                                          На самом деле любая сериализация, которая сериализирует внутреннее представление объекта нарушает его инкапсуляцию. Ведь закрытые члены потому и сделаны закрытыми, чтобы к ним не имел доступа внешний код. Сериализация обходит это ограничение (да, в некоторых случаях это является ее сутью) и использует внутреннее представление объекта для своей работы. К примеру, xml сериализация не содержит этой проблемы, поскольку XmlSerializer сериализирует только видимую часть объекта (т.е. его абстракцию, а не реализацию).

                                          Еще раз напомню, что я подразумеваю под дырявой абстракцией в данном случае: если нам приходится думать о том, как класс реализован, то он дырявый. Можно ли рассматривать то, что при сериализации списка перечислений пользователю этого кода просачивается внутреннее представление абстракции? Думаю, что да. Ведь в данном случае эта проблема уже не описывается его публичным контрактом, а связано с его внутренней реализацией.
                                            0
                                            «Еще раз напомню, что я подразумеваю под дырявой абстракцией в данном случае: если нам приходится думать о том, как класс реализован, то он дырявый.»
                                            Не-а. Это некий другой класс (сериализатор) нарушил правила по работе с этим классом, влез в его внутренности, и поэтому сломался.

                                            Если бы мы использовали только публичный интерфейс List, абстракция бы не протекла. Но мы используем его внутренности — то есть уже не абстракцию.
                                          +1
                                          При чем тут абстракции? Просто у МС криво реализована сериализация для упомянутого типа, и все.
                                            0
                                            Записали коллекцию с одним элементом, вычитываем почему-то коллекцию из 4-х элементов… При чём здесь вообще enum'ы и дыры в абстракциях?
                                              +1
                                              А я бы сказал иначе. Во многих случаях имеют место не дыры в абстракциях, аобыкновенные баги, которые можно поправить и сделать библиотеку лучше. Пример со списком в дотнете — это просто баг. Когда List в очередной раз удваивает размер контейнера, он должен заполнять свободные ячейки валидными данными (например, самым первым элементом енума), а не каким-то там нулем. То, что сам дотнет позволяет заполнять ячейки нулями, не существующими в енуме — это недоработка в дотнете, которую тоже можно устранить.

                                              Насчет якобы обязательной необходимости знания, что такое char*, когда работаешь со строками (из статьи Спольски) — это не проблема абстракции «строка», это недоработка языка программирования (который не может по lvalue вычислить необходимый тип rvalue), или же — следствие необходимости использования winapi, которое само по себе — абстракция более низкого порядка, чем строка. Это не значит, что абстракция плохая — просто она неполная, и ее можно было бы доработать.
                                                +1
                                                Вот пример с тормозами и tcp — он получше. Но пример с файлом .forward — тоже плохой, т. к. почтовый сервер должен был бы просто вылететь (или подвиснуть) в ожидании, когда nfs починится, а не трактовать лежащий nfs как отсутствие файла. Т.е. тут опять скорее пример «бага», чем дыры в абстракции.

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

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                              Самое читаемое