Почему цикл while (true) в конструкторе действительно плох?

47

Хотя это и общий вопрос, моя сфера - скорее C #, так как я знаю, что языки, подобные C ++, имеют различную семантику в отношении выполнения конструктора, управления памятью, неопределенного поведения и т. Д.

Кто-то задал мне интересный вопрос, на который мне было нелегко ответить.

Почему (или это вообще?) Считается плохим проектом, позволяющим конструктору класса запускать бесконечный цикл (т. Е. Игровой цикл)?

Есть некоторые концепции, которые нарушаются этим:

  • Как и принцип наименьшего удивления, пользователь не ожидает, что конструктор будет вести себя так.
  • Модульные тесты сложнее, так как вы не можете создать этот класс или внедрить его, так как он никогда не выходит из цикла.
  • Конец цикла (конец игры) - это концептуально время окончания конструктора, что тоже странно.
  • Технически такой класс не имеет открытых членов, кроме конструктора, что усложняет понимание (особенно для языков, где реализация недоступна)

И тут возникают технические проблемы:

  • Конструктор фактически никогда не заканчивается, так что же здесь происходит с GC? Этот объект уже в Gen 0?
  • Вывод из такого класса невозможен или, по крайней мере, очень сложен из-за того, что базовый конструктор никогда не возвращается

Есть ли что-то более очевидно плохое или коварное с таким подходом?

Самуил
источник
61
Почему это хорошо? Если вы просто перенесете свой основной цикл в метод (очень простой рефакторинг), то пользователь может написать неудивительный код, подобный следующему:var g = new Game {...}; g.MainLoop();
Brandin
61
В вашем вопросе уже указано 6 причин, чтобы не использовать его, и я хотел бы видеть одну в его пользу. Вы могли бы также спросить , почему это плохая идея поставить while(true)петлю в собственность сеттер: new Game().RunNow = true?
Groo
38
Чрезвычайная аналогия: почему мы говорим, что то, что сделал Гитлер, было неправильно? За исключением расовой дискриминации, начала Второй мировой войны, убийства миллионов людей, насилия (и т. Д. По более чем 50 причинам), он не сделал ничего плохого. Конечно, если вы удалите произвольный список причин того, почему что-то не так, вы можете также сделать вывод, что это хорошо, буквально для всего.
Бакуриу
4
Что касается сбора мусора (GC) (ваша техническая проблема), в этом не было бы ничего особенного. Экземпляр существует до фактического запуска кода конструктора. В этом случае экземпляр все еще доступен, поэтому он не может быть запрошен GC. Если GC включается, этот экземпляр будет существовать, и при каждом появлении GC установка будет повышаться до следующего поколения в соответствии с обычным правилом. Будет ли GC иметь место, зависит от того, (достаточно ли) создаются новые объекты или нет.
Джепп Стиг Нильсен
8
Я бы сделал еще один шаг и сказал бы, что while (true) плохо, независимо от того, где оно используется. Это подразумевает, что нет никакого ясного и чистого способа остановить цикл. В вашем примере, не должен ли цикл останавливаться при выходе из игры или теряет фокус?
Джон Андерсон

Ответы:

250

Какова цель конструктора? Возвращает вновь созданный объект. Что делает бесконечный цикл? Это никогда не возвращается. Как конструктор может вернуть вновь созданный объект, если он вообще не возвращается? Не может

Следовательно, бесконечный цикл нарушает основной контракт конструктора: что-то строить.

Йорг Миттаг
источник
11
Может быть, они хотели поставить (правда) с перерывом в середине? Рискованно, но законно.
Джон Дворжак
2
Я согласен, это правильно - по крайней мере. с академической точки зрения. То же самое верно для каждой функции, которая возвращает что-то.
Док Браун
61
Представьте себе беднягу, которая создает подкласс указанного класса ...
Newtopian
5
@JanDvorak: Ну, это другой вопрос. ОП явно указывал «никогда не заканчивающийся» и упоминал цикл событий.
Йорг Миттаг,
4
@ JörgWMittag хорошо, тогда, да, не помещайте это в конструктор.
Джон Дворжак
45

Есть ли что-то более очевидно плохое или коварное с таким подходом?

Да, конечно. Это ненужное, неожиданное, бесполезное, нелегальное. Это нарушает современные концепции классового дизайна (сплоченность, сцепление). Он нарушает контракт метода (конструктор имеет задание, а не просто какой-то случайный метод). Это, конечно, плохо поддерживается, будущие программисты будут тратить много времени, пытаясь понять, что происходит, и пытаясь угадать причины, почему это было сделано таким образом.

Ничто из этого не является «ошибкой» в том смысле, что ваш код не работает. Но в долгосрочной перспективе это, вероятно, повлечет за собой огромные вторичные затраты (по сравнению с первоначальной стоимостью написания кода), затрудняя сопровождение кода (т. Е. Сложность добавления тестов, сложность повторного использования, сложность отладки, сложность расширения и т. Д.). .).

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

К сожалению, вы регулярно встречаетесь с программистами, которые совершенно не знают всего этого. Это работает, вот и все.

Чтобы закончить с аналогией (другой язык программирования, здесь под рукой стоит задача расчета 2+2):

$sum_a = `bash -c "echo $((2+2)) 2>/dev/null"`;   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 2+2;

Что не так с первым подходом? Возвращает 4 через достаточно короткий промежуток времени; верно. Возражения (вместе со всеми обычными причинами, по которым разработчик может их опровергнуть):

  • Труднее читать (но, эй, хороший программист может прочитать это так же легко, и мы всегда делали это так; если вы хотите, я могу преобразовать его в метод!)
  • Медленнее (но это не в критическом месте, поэтому мы можем игнорировать это, и, кроме того, лучше оптимизировать только тогда, когда это необходимо!)
  • Использует гораздо больше ресурсов (новый процесс, больше оперативной памяти и т. Д. - но, по крайней мере, наш сервер более чем достаточно быстр)
  • Вводит зависимости от bash(но он никогда не будет работать на Windows, Mac или Android в любом случае)
  • И все остальные причины, упомянутые выше.
Anoe
источник
5
«GC вполне может находиться в каком-то исключительном состоянии блокировки во время работы конструктора» - почему? Распределитель сначала выделяет, а затем запускает конструктор как обычный метод. В этом нет ничего особенного, не так ли? И распределитель должен разрешать новые выделения (и они могут вызвать ситуации OOM) в конструкторе в любом случае.
Джон Дворжак
1
@JanDvorak, просто хотел подчеркнуть, что в конструкторе может быть какое-то особенное поведение «где-то», но вы правы, пример, который я выбрал, был нереальным. Удалены.
AnoE
Мне очень нравятся ваши объяснения, и я хотел бы отметить оба ответа как ответы (по крайней мере, с возражением). Хорошая аналогия, и ваш след мысли и объяснения вторичных затрат тоже, но я рассматриваю их как вспомогательные требования к коду (так же, как мои аргументы). Современное состояние - тестирование кода, поэтому тестируемость и т. Д. Является фактическим требованием. Но заявление @ JörgWMittag точно fundamental contract of a constructor: to construct somethingна месте.
Самуил
Аналогия не очень хорошая. Если я увижу это, я ожидаю увидеть комментарий где-то, почему вы используете Bash для математики, и в зависимости от вашей причины это может быть совершенно нормально. То же самое не сработало бы для OP, потому что вы должны были бы в основном извиняться в комментариях везде, где вы использовали конструктор «// Примечание: создание этого объекта запускает цикл обработки событий, который никогда не возвращается».
Брэндин
4
«К сожалению, вы регулярно встречаетесь с программистами, которые совершенно не знают всего этого. Это работает, вот и все». Так грустно, но так верно. Я думаю, что могу говорить за всех нас, когда говорю, что единственное существенное бремя в нашей профессиональной жизни, ну, в общем, эти люди.
Гонки Легкости с Моникой
8

В своем вопросе вы приводите достаточное количество причин, чтобы исключить такой подход, но на самом деле здесь возникает вопрос: «Есть ли что-то более очевидно плохое или изощренное при таком подходе?»

Сначала я подумал, что это бессмысленно. Если ваш конструктор никогда не завершает свою работу, никакая другая часть программы не может получить ссылку на созданный объект, так что логика заключается в том, чтобы поместить его в конструктор вместо обычного метода. Тогда мне пришло в голову, что единственное отличие будет в том, что в вашем цикле вы можете разрешить ссылки на частично сконструированный thisвыход из цикла. Хотя это строго не ограничивается этой ситуацией, гарантируется, что если вы разрешите thisэкранирование, эти ссылки всегда будут указывать на объект, который не был полностью создан.

Я не знаю, хорошо ли определена семантика вокруг такого рода ситуаций в C #, но я бы сказал, что это не имеет значения, потому что большинство разработчиков не захотят в них вникать.

JimmyJames
источник
Я бы предположил, что конструктор запускается после того, как объект был создан, таким образом, в утечке нормального языка это должно быть совершенно четко определенное поведение.
mroman
@mroman: утечка thisв конструкторе возможна, но только если вы находитесь в конструкторе самого производного класса. Если у вас есть два класса, Aи Bс B : A, если конструктор Aутечек this, члены Bбудут по-прежнему неинициализированы (и вызовы виртуальных методов или приведение к ним Bмогут нанести ущерб, основанный на этом).
Хоффмэйл,
1
Я думаю, в C ++ это может быть сумасшествием, да. В других языках члены получают значение по умолчанию, прежде чем им присваиваются их фактические значения, поэтому, хотя на самом деле глупо писать такой код, поведение все еще хорошо определено. Если вы вызываете методы, использующие эти члены, вы можете столкнуться с NullPointerExceptions или неправильными вычислениями (потому что числа по умолчанию равны 0) или подобными вещами, но это технически детерминировано, что происходит.
mroman
@mroman Можете ли вы найти точную ссылку на CLR, которая показала бы, что это так?
JimmyJames,
Итак, вы можете назначить значения членов до того, как конструктор будет запущен, чтобы объект существовал в допустимом состоянии с членами, которым назначены значения по умолчанию или значения, назначенные им еще до того, как конструктор был запущен. Вам не нужен конструктор для инициализации членов. Дайте мне посмотреть, смогу ли я найти это где-нибудь в документах.
mroman
1

+1 Для наименьшего удивления, но это сложная концепция, чтобы сформулировать для новых разработчиков. На прагматическом уровне трудно отлаживать исключения, возникающие внутри конструкторов, если объект не инициализируется, он не будет существовать для проверки состояния или регистрации извне этого конструктора.

Если вы чувствуете необходимость сделать такой шаблон кода, используйте вместо этого статические методы в классе.

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

Если вы не намерены использовать объект, который вы «строите», то какой смысл создавать объект в первую очередь? Наличие цикла while (true) в конструкторе фактически означает, что вы никогда не собираетесь его завершать ...

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

В C # не выполняйте расширенную или бесконечную логику внутри конструкторов, потому что ... есть лучшие альтернативы

Крис Шаллер
источник
1
кажется, что он не предлагает ничего существенного по поводу высказанных и объясненных в предыдущих 9 ответах
комментариев
1
Эй, я должен был попробовать :) Я подумал, что важно подчеркнуть, что, хотя нет никаких правил против этого, вы не должны делать это из-за того, что есть более простые способы. Большинство других ответов сосредоточены на техническом отношении того, почему это плохо, но это трудно продать новому разработчику, который говорит: «Но он компилируется, поэтому я могу просто оставить это так»
Крис Шаллер,
0

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

Прежде всего, я не буду говорить об использовании while(true)в конструкторе. Существует абсолютно ничего плохого с использованием этого синтаксиса конструктора. Однако концепция «запуска игрового цикла в конструкторе» может вызвать некоторые проблемы.

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

Одним из практических правил в программировании является использование простейшего инструмента для работы. Это не жесткое и быстрое правило, но оно очень полезно. Если вызова функции было бы достаточно, зачем беспокоиться о создании объекта класса? Если я увижу более мощный и сложный инструмент, я предполагаю, что у разработчика была причина для этого, и я начну исследовать, какие странные уловки вы могли бы делать.

Есть случаи, когда это может быть разумным. Могут быть случаи, когда вам действительно интуитивно понятно иметь игровой цикл в конструкторе. В тех случаях, вы бежите с этим! Например, ваш игровой цикл может быть встроен в гораздо большую программу, которая создает классы для воплощения некоторых данных. Если вам нужно запустить игровой цикл, чтобы сгенерировать эти данные, может быть очень разумно запустить игровой цикл в конструкторе. Просто отнеситесь к этому как к особому случаю: было бы правильно сделать это, потому что всеобъемлющая программа делает интуитивно понятным для следующего разработчика понять, что вы сделали и почему.

Корт Аммон
источник
Если для создания вашего объекта требуется игровой цикл, по крайней мере, поместите его в фабричный метод. GameTestResults testResults = runGameTests(tests, configuration);а не GameTestResults testResults = new GameTestResults(tests, configuration);.
user253751 11.09.16
@immibis Да, это правда, за исключением случаев, когда это неразумно. Если вы работаете с системой на основе отражений, которая создает для вас объект, у вас может не быть возможности создать фабричный метод.
Cort Ammon
0

У меня сложилось впечатление, что некоторые понятия перепутались и привели к не очень хорошо сформулированному вопросу.

Конструктор класса может начать новый поток, который «никогда» не закончится (хотя я предпочитаю использовать while(_KeepRunning)over, while(true)потому что я могу где-то установить для логического члена значение false).

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

Вы также можете взглянуть на «Шаблон активного объекта». В конце концов, я предполагаю, что вопрос был нацелен на то, когда начинать поток «Активного объекта», и я предпочитаю разделение конструкции и начало для лучшего контроля.

Бернхард Хиллер
источник
0

Ваш объект напоминает мне о запуске задач . Объект задачи имеет конструктор, но это есть и куча статических фабричных методов , таких как: Task.Run, Task.Startи Task.Factory.StartNew.

Это очень похоже на то, что вы пытаетесь сделать, и, скорее всего, это «соглашение». Основная проблема, с которой люди сталкиваются, заключается в том, что такое использование конструктора удивляет их. @ JörgWMittag говорит, что это нарушает фундаментальный контракт , который, я полагаю, означает, что Йорг очень удивлен. Я согласен, и мне больше нечего добавить к этому.

Тем не менее, я хочу предположить, что OP пытается использовать статические фабричные методы. Сюрприз исчезает, и здесь нет принципиального контракта . Люди привыкли к статическим фабричным методам, которые делают особые вещи, и их можно называть соответственно.

Вы можете предоставить конструкторы, которые обеспечивают мелкозернистый контроль (как предлагает @Brandin, что-то вроде того var g = new Game {...}; g.MainLoop();, чтобы приспособить пользователей, которые не хотят сразу запускать игру, и, возможно, захотят сначала ее прогнать. И вы можете написать что-то вроде var runningGame = Game.StartNew();запуска это сразу легко.

Натан Купер
источник
Это означает, что Йорг фундаментально удивлен, то есть такой сюрприз, как боль в фундаменте.
Стив Джессоп
0

Почему цикл while (true) в конструкторе действительно плох?

Это совсем не плохо.

Почему (или это вообще?) Считается плохим проектом, позволяющим конструктору класса запускать бесконечный цикл (т. Е. Игровой цикл)?

Зачем тебе это делать? Шутки в сторону. Этот класс имеет одну функцию: конструктор. Если все, что вам нужно, это одна функция, поэтому у нас есть функции, а не конструкторы.

Вы упомянули некоторые очевидные причины против этого сами, но вы упустили самую главную:

Есть лучшие и более простые варианты для достижения того же.

Если есть 2 варианта, и один лучше, то не важно, насколько он лучше, вы выбрали лучший. Кстати, именно поэтому мы не не почти никогда не использовать GoTo.

Питер
источник
-1

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

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

Хаген фон Айцен
источник
1
это, кажется, не предлагает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 8 ответах
комнат