?

Log in

No account? Create an account
   Journal    Friends    Archive    Profile    Memories
 

Инициализация в конструкторе - morfizm


Jun. 21st, 2010 09:31 pm Инициализация в конструкторе

Обнаружился баг такого плана (C#):

Было a = new C(value); class C { C(type value) { field = value; ... } ... }
Изменили на a = new C() { field = value }.
А внутри конструктора (где-то в середине кода) было такое: if (field == null) { field2 = false; } Соответственно, всё сломалось, т.к. member initialization апплаится только после того, как конструктор отработал.

Мораль: если объект конструируется по-разному (выполняется разный код), в зависимости от начальных значений параметров, то следует создавать объект невалидным, и вынести логику инициализации в метод Init() (или ApplyOptions() как в appender'ах log4net'а). Кроме, пожалуй, тривиальных и очевидных случаев. Тогда таких багов не было бы как класса.

Вообще, парадигма умных конструкторов с параметрами, довольно ущербна и обычно хорошо работает только в случае, когда этих параметров всего один или несколько, они очевидны и естественны (о них можно догадаться по названию класса), и кода в конструкторе очень мало.

28 comments - Leave a commentPrevious Entry Share Next Entry

Comments:

From:dennyrolling
Date:June 22nd, 2010 03:51 pm (UTC)
(Link)
Проклятье инитов (aka "if(!m_inited) throw;") тоже дело такое, обязательно же где-нибудь забудешь написать.

Похоже я не до конца проснулся еще: в чем глубокий философский смысл new C(){field=value;} если и так уже есть подходящий конструктор? Или я как-то неправильно твой пример понимаю?
From:morfizm
Date:June 22nd, 2010 05:17 pm (UTC)
(Link)
Проблема здесь глубже. Дело в том, что у этого класса куча "опций" и некоторые из них всё равно нужно ставить вне конструктора (например, потому что надо вызывать метод). Т.е., я думаю, вынести все взаимосвязи в Init в данном случае было бы правильным рефакторингом. Второй вариант рефакторинга - создать дополнительный класс, в котором будут "исходные" опции (без преобразований, основанных на их взаимной комбинации), а новый класс будет в конструкоре брать struct с исходными опциями и делать преобразования.

Насчёт "в чём смысл new C() { field = value; }" - главным образом, в том, что явно проговаривается слово "field" - не в комментах, а как часть синтаксиса. В данном случае это очень ценно, потому что опций много и они не естественно вытекают из названия класса.
From:dennyrolling
Date:June 22nd, 2010 06:14 pm (UTC)
(Link)
видимо ты как-то сильно упростил проблему, потому что для меня до сих пор непонятно :) чай вроде уже выпил с утра.

Если у тебя класс это набор опций то естественным является решение в котором конструктор ставит опции "по умолчанию" (или импортирует их из параметров, как видимо у вас и было) и потом они устанавливаются либо через set либо через setoption(option, value).

Если хочется заставить людей чтобы они действительно вызывали опции то можно сделать значение "по умолчанию" невалидным и обрабатывать соответственно.

а по поводу "проговариваться": где ты, визуальный васик?
MsgBox(Title:="Title bar text", Prompt:="Test message")
From:morfizm
Date:June 22nd, 2010 06:33 pm (UTC)
(Link)
С опциями у нас такая шутка: валидны лишь некоторые из их комбинаций, и плюс есть опции, которые пересчитываются в зависимости от других опций (которые действуют как override). Поидее, это сразу подсказывает сделать метод вроде "Init" или "ApplyOptions", перед которым скормить классу всю исходную информацию. Но задумка оригинального дизайна была избежать этого пересчёта, поместив опции в параметр конструктора. Что громоздко, неудобно, в точке вызова не понятно, какие именно опции устанавливаются, т.к. параметры не именованные и т.п. Потому это было починено. Но починено неправильно.
From:yau_hen
Date:June 22nd, 2010 06:48 pm (UTC)
(Link)
А вы еще не перешли на .Net 4 ?
Там C# уже поддерживает именованные параметры,
так что можно написать a = new C(field1 : value1, field2 : value2);
From:morfizm
Date:June 22nd, 2010 07:23 pm (UTC)
(Link)
Офигеть! Спасибо большое!
Для продукта ещё не перешли, но для test framework'а - да (а описанный баг был именно в нём).
From:sasha_gil
Date:June 22nd, 2010 11:30 pm (UTC)
(Link)
А ещё вроде параметры со значениями по умолчанию, как в C++...
From:dennyrolling
Date:June 23rd, 2010 12:35 am (UTC)
(Link)
...испортили совсем язык, куда катится мир %) они скоро препроцессор начнут расширять.
From:dennyrolling
Date:June 22nd, 2010 10:47 pm (UTC)
(Link)
о, я знал что визуальный васик живет и побеждает.
From:morfizm
Date:June 22nd, 2010 10:52 pm (UTC)
(Link)
Ассоциация "Вижуал Бейсик - отстой" возникла у людей неспроста. Упоминая те редкие вещи из Вижуал Бейсика, которые в нём очень хороши, ты делаешь некорректный ход ;)
From:dennyrolling
Date:June 22nd, 2010 11:10 pm (UTC)
(Link)
не понимаю в чем состоит некорректность и, что самое главное: кто сказал что редкие? там вообще сплошное счастье: и with есть, и разница между функциями и процедурами, и циклы нормальные, а не как в Си-образных языках.

я уж не говорю про человеческую обработку ошибок через on error goto
From:morfizm
Date:June 22nd, 2010 11:52 pm (UTC)
(Link)
Ты забыл перечислить, сколько всего в Вижуал Бейсике нет :)
From:dennyrolling
Date:June 23rd, 2010 12:33 am (UTC)
(Link)
кто здесь?! чего там нет? я просто уверен что он полный по Тьюрингу, значит любую задачу на нем решить можно. как и на темплейтах в С++.

ну да, бывает что какого-то синтаксического сахара и нет. но как я уже говорил много чего нет в языках которые многим нравятся и которые всем ставят в пример.
From:morfizm
Date:July 4th, 2010 04:00 am (UTC)
(Link)
В Visual Basic'е (last time I checked) нет нормальных compile-time type safety checks. В результате получается язык типа bat/cmd. А за case insensitivity надо убивать. Создателей SQL тоже.
From:dennyrolling
Date:July 4th, 2010 03:37 pm (UTC)
(Link)
I just say one word to you, just one word: "object".

в ВБ уже лет десять как есть option strict, канает за "нормальную type safety check"?
From:morfizm
Date:July 4th, 2010 08:27 pm (UTC)
(Link)
Не знал и внимательно не изучал. Подозреваю, что не канает. Всё равно ведь есть Object, к которому это не относится, не так ли?
From:dennyrolling
Date:July 5th, 2010 01:32 am (UTC)
(Link)
про обджект я имел в виду как раз "типизированые" языки, до-диез или жабу там.
From:morfizm
Date:July 5th, 2010 01:50 am (UTC)
(Link)
Понятно. Странно, почему ты подчеркнул в них объектность. Паскаль, Ада и, в некотором смысле, даже C и C++, дают достаточно много compile-time типизации, безо всяких объектов.

"до-диез" - класс! Возьму на заметку :)
Буду теперь говорить "я разучиваю пьесу в тональности си-шарп минор", с двойным смыслом :) (У меня действительно есть одна гитарная пьеса в этой тональности).


Edited at 2010-07-05 01:54 am (UTC)
From:morfizm
Date:June 22nd, 2010 11:52 pm (UTC)
(Link)
И забыл про case insensitivity упомянуть. (It's KILLING)
From:dennyrolling
Date:June 23rd, 2010 12:30 am (UTC)
(Link)
ну к этому я давно привык в виндбг ;)

если человек действительно нуждается в том чтобы иметь две переменных отличающиеся только капитализацией он всего что из этого следует заслуживает %) (пример с perSon против Person не приводить :)
From:morfizm
Date:July 4th, 2010 04:02 am (UTC)
(Link)
Ты, видимо, размышляешь над case sensitivity как над фичей, и тебе нужны аргументы, чтобы её оправдать. Я же считаю case sensitivity естественной вещью, а case insensitivity - ненужной фичей. Зло здесь не в том, чтобы различать переменные, отличающиеся в case, а в том, что в случае стилевого разнобоя сложнее читать код.
From:dennyrolling
Date:July 4th, 2010 03:41 pm (UTC)
(Link)
я уже столько кода вынужден был написать который делает либо каст строчки к lower case или сравнивает две строчки без учета кейса, что уже серьезно задумываюсь о том что case sensitivity это зло, порожденное бедностью в 70х годах.

ну куда угодно посмотри - нет разницы где какой кейс ни на вебе, ни в файловой системе, ни в нормальном человеческом языке.
From:morfizm
Date:July 4th, 2010 08:25 pm (UTC)
(Link)
Я оптимист, и считаю, что все эти перечисленные тобой сценарии - это плохие баги в дизайне :)
From:sasha_gil
Date:June 22nd, 2010 11:29 pm (UTC)
(Link)
Дима, я хотел бы этот вопрос обсудить немного (здесь, вероятно) в удобном тебе темпе. У меня в голове следующие пункты:

0. Есть мнение, что сервисный (утилитный) компонент, предоставляющий клиенту возможность создавать сколь-либо нетривиальный объект (не value-type), всегда в перспективе выиграет, инкапсулировав собственно создание в фабрику-функцию (статический метод интерфейсного класса), а конструкторы конкретных классов спрятав. Минус этого подхода для C# - потеря синтаксического сахара инициализаторов. ОК, но я не зря оговорился о "нетривальности" и исключении для value types. Давай во избежании путаницы (с настоящим смыслом value types) назовём типы, для которых мы это правило не применяем, а сохраняем сахар с инициализаторами, "лёгкими" типами. У них конструкторы должны быть достаточно тривиальными в плане того, что нужная клиенту настройка нового значения выполняется при желании в инициализаторе (сахар работает по делу).

1. Итак, конструкторы "тяжёлых" типов не должны вызываться клиентом вообще, клиенту следует вызывать фабрики. Создание объектов таким способом прячет фазы инициализации нового объекта на стороне сервиса в один вызов - и, таким образом, вроде бы соответсвует идиоме RAII, так?

2. Остаётся вопрос: при том, что мы поощряем RAII в клиентском коде, с какой стати мы будем использовать другие guidelines (раздельное конструирование и Init) в коде на стороне сервиса? Мне кажется, логично было бы попытаться этого избежать. Насколько я тебя понял, роблема в поливариантности и многопараметричности конструкторов "тяжёлых" типов. Мне кажется, можно избежать разделения на один конструктор и "тяжёлый" Init c несколькими аргументами посредством частичного переноса логики Init-а в вспомогательный лёгкий тип, значение которого передаётся параметром в конструктор типа RAII. В этом случае конструктор тяжёлого типа - один, а поливариантность и многопараметричность спрятана в один-два, максимум три аргумента вспомогательных лёгких типов, представляющих разные группы настроек процесса создания объекта тяжёлого типа. Код, вызывающий конструктор, будет иметь возможность достаточно компактно выразить то, что раньше выражалось вызовами разных конструкторов, вызовом одного конструктора - при использовании синтаксиса инициализаторов для лёгких значений-параметров конструктора.

3. Наконец, лёгкие настроечные типы для аргументов спрятанного конструктора могут пригодиться и в публичном внешнем интерфейсе - как типы параметров функции-фабрики. Сервисный компонент имеет дополнительную гибкость в этой прослойке (за счёт возможности иметь различные параметры у фабрики и у скрытого конструктора), но в наиболее простых случаях эти параметры могут просто идти в вызов скрытого конструктора один в один.

Что ты думаешь на этот счёт? Хорошо было бы разобрать конкретный пример (я попробую придумать свой на досуге).
From:_m_e_
Date:June 23rd, 2010 07:41 am (UTC)
(Link)
Может проблема в том, что у класса есть публичные поля, что естественно не рекомендуется, а правильно делать свойства?

Мне кажется, что если в конструкторе было
if (field == null) { field2 = false; }

то field должен быть свойством, и соответственно
Field {
get { return field; }
set { field = value; if (value == null) field2 = false; }
}

Хотя это конечно не решает всех проблем, и если инициализация класса такая сложная, то следует делать фактори аля скажем XmlWriter.Create
http://msdn.microsoft.com/en-us/library/system.xml.xmlwriter.create.aspx
Всякие Init методы мне нравятся гораздо меньше.
From:dennyrolling
Date:June 23rd, 2010 05:35 pm (UTC)
(Link)
Я тоже предлагал свойства - отказывается :) говорит что слишком сложные отношения между валидными и невалидными.
From:sko4
Date:July 4th, 2010 03:53 pm (UTC)

не по теме

(Link)
позволю себе у Вас, Дмитрий спросить: как листать страницы в Вашем Журнале, ибо эта "статья" в "моем обозрении" первая с конца, или остальные скрыты? - мне только убедиться, что ознакомилась со всеми предлагаемыми "топами" в блоге
From:morfizm
Date:July 4th, 2010 08:24 pm (UTC)

Re: не по теме

(Link)
У меня очень неорганизованный блог. Я лишь недавно начал ставить тэги, и ставлю их только иногда. Где-то 80% записей френдс-онли.