Как стать автором
Обновить

Комментарии 47

Во-первых, абстракция нужна не для того, чтобы скрыть низкоуровневые детали, а для того, чтобы начать работать в терминах более высокого уровня. Хорошая абстракция — это замкнутая система с определёнными гарантиями. Например, алгебра — это абстракция над множеством предметов реального мира, позволяющая проводить некоторые операции без учёта свойств нижележащих реальных предметов. Если у вас есть 6 яблок, то вы можете поделить их между 2 людьми просто разделив число 6 на 2, без учёта цвета яблок, их вкуса, размера и т.д. Алгебраическая абстракция в данном случе не предоставляет гарантии, что люди останутся довольны тем, какие яблоки им достались. Но если вам нужно максимально удовлетворить потребности потребности обоих людей, то простая алгебра является просто неподходящей абстракицей — это задачи оптимизации, и она требует соответствующих моделей.
Точно так же и с технологиями: если .Net Remoting скрывает от разработчика, где именно будет выполняться процедура (локально или удалённо), а для разработчика эта информация важна, то это значит только то, что данная абстракция не подходит для данного случая. Не сама абстракция плоха, а выбор этой абстракции для данной конкретной задачи — эта абстракция не даёт тех гарантий и инструментов, которые нужны программисту.

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

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

«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)
Я люблю гайдлайны, но не люблю следовать им слепо. Вот пример, у меня есть перечисление бизнесс-объектов, при этом в принципе нет значения, которое бы соответстовало значению None, т.е. валидное значение должно быть всегда. И при этом эти перечисления завязаны на базу данных и значения я изменить не могу тоже не могу.

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

Публикации

Истории