27 October 2011

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

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

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


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

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



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

Когда-то давно, в «небольшой» мелкомягкой компании решили, а почему бы нам не «абстрагироваться» от местоположения объекта и сделать сам факт того, является ли объект локальным или удаленным, лишь «деталью реализации». Так появились технологии 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.
Tags:wcf
Hubs: .NET
+37
4.9k 63
Comments 47
Разработчик .NET
from 60,000 to 120,000 ₽GMCSТулаRemote job
Разработчик .NET
from 60,000 ₽GMCSКазаньRemote job
С#/.NET Developer
to 100,000 ₽Teleperformance Russia GroupВолгоград
Ведущий программист .net
from 70,000 to 120,000 ₽Мечел-СервисЧелябинскRemote job
Разработчик .Net Core
from 90,000 ₽ГК InnoSTageRemote job
Top of the last 24 hours