Чому певний час (справжній) цикл у конструкторі насправді поганий?


47

Хоча загальним питанням, мій обсяг є скоріше C #, оскільки я знаю, що такі мови, як C ++, мають різну семантику щодо виконання конструктора, керування пам'яттю, невизначеної поведінки тощо

Хтось задав мені цікаве запитання, на яке мені було нелегко відповісти.

Чому (або це взагалі?) Вважають поганим дизайном, щоб дозволити конструктору класу запустити нескінченний цикл (тобто цикл гри)?

Є кілька понять, які порушені цим:

  • як принцип найменшого здивування, користувач не сподівається, що конструктор поводиться так.
  • Тестові одиниці складніше, оскільки ви не можете створити цей клас або ввести його, оскільки він ніколи не виходить з циклу.
  • Тоді кінець циклу (кінець гри) концептуально - це час закінчення конструктора, що також є дивним.
  • Технічно такий клас не має публічних членів, крім конструктора, що ускладнює його розуміння (особливо це стосується мов, де немає можливості реалізації)

А тут є технічні питання:

  • Конструктор насправді ніколи не закінчується, так що ж відбувається з GC тут? Цей об’єкт вже в Gen 0?
  • Виведення з такого класу неможливе або, принаймні, дуже складне через те, що базовий конструктор ніколи не повертається

Чи є щось більш очевидно погане чи хитре при такому підході?


61
Чому це добре? Якщо ви просто перемістите основний цикл на метод (дуже простий рефакторинг), тоді користувач може написати несподіваний код на зразок цього:var g = new Game {...}; g.MainLoop();
Брандін

61
У вашому питанні вже вказано 6 причин не використовувати його, і я хотів би бачити одну на свою користь. Ви також можете запитати, чому це неправильно ставити while(true)цикл у програмі налаштування властивостей new Game().RunNow = true:?
Груо

38
Крайня аналогія: чому ми говоримо, що те, що робив Гітлер, було неправильним? За винятком расової дискримінації, починаючи Другу світову війну, вбиваючи мільйони людей, насильство [тощо тощо з 50+ інших причин], він не зробив нічого поганого. Звичайно, якщо ви видалите довільний перелік причин того, чому щось не так, ви також можете зробити висновок, що ця річ є доброю для буквально нічого.
Бакуріу

4
Що стосується збору сміття (GC) (Ваша технічна проблема), то нічого особливого в цьому немає. Екземпляр існує до фактичного запуску коду конструктора. У цьому випадку екземпляр все ще доступний, тому GC не може вимагати його. Якщо GC встановлюється, цей примірник виживе, і при кожному виникненні налаштування GC об'єкт буде просунутий до наступного покоління відповідно до звичайного правила. Відбувається чи ні GC, залежить від того, створюються (достатньо) нові об'єкти чи ні.
Джеппе Стіг Нільсен

8
Я б пішов на крок далі і сказав, що час (правда) поганий, незалежно від того, де він використовується. Це означає, що немає чіткого і чистого способу зупинити цикл. У вашому прикладі, чи не повинен цикл зупинятися, коли гра закінчується, або вона втрачає фокус?
Джон Андерсон

Відповіді:


250

Яке призначення конструктора? Він повертає щойно побудований об’єкт. Що робить нескінченна петля? Він ніколи не повертається. Як конструктор може повернути щойно побудований об'єкт, якщо він взагалі не повертається? Це не може.

Ерго, нескінченний цикл розриває фундаментальний договір конструктора: щось побудувати.


11
Можливо, вони мали намір поставити час (правда) з перервою в середині? Ризиковано, але законно.
Джон Дворак

2
Я згоден, це правильно - принаймні. з академічної точки зору. Те саме стосується кожної функції, яка щось повертає.
Док Браун

61
Уявіть бідну дерну, яка створює підклас згаданого класу ...
ньютопієць

5
@JanDvorak: Ну, це інше питання. ОП чітко вказала "не закінчується" і згадувала цикл подій.
Йорг W Міттаг

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 через досить короткий проміжок часу; це правильно. Заперечення (разом із усіма звичайними причинами, які розробник може дати, щоб спростувати їх):

  • Важко читати (але ей, хороший програміст може прочитати це так само легко, і ми завжди робили це так; якщо ви хочете, я можу переробляти його на метод!)
  • Повільніше (але це не в критичному місці, тому ми можемо проігнорувати це, і до того ж, краще оптимізувати лише за необхідності!)
  • Використовує набагато більше ресурсів (новий процес, більше оперативної пам’яті тощо - але dito, наш сервер більш ніж швидкий)
  • Представляє залежності від bash(але ніколи не працюватиме в Windows, Mac чи Android)
  • І всі інші причини, згадані вище.

5
"GC цілком може бути у винятковому стані блокування під час роботи конструктора" - чому? Алокатор виділяє спочатку, потім запускає конструктор як звичайний метод. У цьому немає нічого особливого, чи не так? І розподільник повинен дозволяти нові виділення (а це може викликати ситуації OOM) всередині конструктора.
Джон Дворак

1
@JanDvorak, Просто хотів зазначити, що може бути якась особлива поведінка "десь", перебуваючи в конструкторі, але ви вірні, приклад, який я вибрав, був нереальним. Вилучено.
AnoE

Мені дуже подобається ваше пояснення, і я хотів би позначити обидві відповіді як відповіді (мати хоча б відгук). Аналогія є приємною, і ваш слід думки та пояснення вторинних витрат теж є, але я розглядаю їх як допоміжні вимоги до коду (так само, як і мої аргументи). Сучасний стан техніки полягає у тестуванні коду, тому доказовість тощо є фактичною вимогою. Але заява @ JörgWMittag не fundamental contract of a constructor: to construct somethingвказано.
Самуель

Аналогія не така хороша. Якщо я бачу це, я би сподівався десь побачити коментар щодо того, чому ви використовуєте Bash для математики, і в залежності від вашої причини це може бути абсолютно добре. Це ж не буде працювати для ОП, тому що вам, в основному, доведеться ставити вибачення в коментарі скрізь, де ви використовували конструктор "// Примітка: побудова цього об'єкта запускає цикл подій, який ніколи не повертається".
Брандін

4
"На жаль, ви регулярно зустрінете програмістів, які абсолютно не знають про все це. Це працює, ось і все". Так сумно, але так правда. Я думаю, що я можу говорити за всіх нас, коли кажу, що єдиний істотний тягар у нашому професійному житті - це, ці люди.
Гонки легкості з Монікою

8

Ви даєте достатньо причин у своєму питанні, щоб виключати цей підхід, але актуальне питання тут: "Чи є щось більш очевидно погане чи хитромудрий при такому підході?"

Моє перше, хоча тут було, що це безглуздо. Якщо ваш конструктор ніколи не закінчується, жодна інша частина програми не може отримати посилання на побудований об'єкт, тож яка логіка в тому, щоб помістити його в конструктор замість звичайного методу. Тоді мені спало на думку, що єдиною різницею було б те, що у вашому циклі ви можете дозволити посилання на частково побудований thisвтечу з циклу. Хоча це не суворо обмежено цією ситуацією, але гарантується, що якщо ви дозволите thisвтекти, ці посилання завжди будуть вказувати на об'єкт, який не повністю побудований.

Я не знаю, чи чітко визначена семантика навколо подібної ситуації в C #, але я б стверджував, що це не має значення, оскільки це не те, що більшість розробників хотіли б спробувати поглибити.


Я припускаю, що конструктор запускається після того, як об’єкт створений таким чином, здоровою мовою, що протікає, це має бути досконало чітко визначеною поведінкою.
mroman

@mroman: це може бути нормально протікати thisв конструкторі - але тільки якщо ви в конструкторі найбільш похідного класу. Якщо у вас є два класи, Aі Bз B : A, якщо конструктор Aбуде просочуватися this, члени Bбудуть по- , як і раніше бути ініційовані (і виклики віртуальних методів або закидів до Bможе завдати шкоди на його основі).
hoffmale

1
Я думаю, що в C ++ це може бути божевільним, так. В інших мовах учасники отримують значення за замовчуванням перед тим, як їм присвоїти фактичні значення, тому, хоча дурно писати такий код, поведінка все ще добре визначена. Якщо ви викликаєте методи, які використовують цих членів, ви можете зіткнутися з NullPointerExceptions або помилковими обчисленнями (оскільки число за замовчуванням до 0) або подібні речі, але це технічно детерміновано, що відбувається.
mroman

@mroman Чи можете ви знайти остаточне посилання на CLR, яке показало б, що так?
JimmyJames

Ну ви можете призначити значення членів до запуску конструктора, щоб об'єкт існував у дійсному стані, а членам присвоєно значення за замовчуванням або значення, присвоєні їм ще до того, як конструктор навіть був запущений. Вам не потрібен конструктор для ініціалізації членів. Дозвольте мені побачити, чи можу я це знайти десь у документах.
mroman

1

+1 Принаймні здивування, але це складна концепція артикуляції до нових дияволів. На прагматичному рівні важко налагодити винятки, створені в конструкторах, якщо об'єкт не може ініціалізуватись, він не буде існувати для того, щоб ви перевіряли стан або журнал ззовні цього конструктора.

Якщо ви відчуваєте необхідність робити подібний зразок коду, будь ласка, використовуйте замість нього статичні методи.

Конструктор існує для надання логіки ініціалізації, коли об'єкт інстанціюється з визначення класу. Ви створюєте цей екземпляр об'єкта, оскільки він є контейнером, який інкапсулює набір властивостей та функціональних можливостей, які ви хочете викликати з іншої логіки програми.

Якщо ви не маєте наміру використовувати об'єкт, який ви "будуєте", то в чому полягає сенс інстанціювати об'єкт в першу чергу? Маючи деякий час (справжній) цикл у конструкторі, це означає, що ви ніколи не збираєтесь його завершити ...

C # - це дуже багата об'єктно-орієнтована мова з безліччю різних конструкцій та парадигм, щоб ви могли вивчити, знати свої інструменти та коли їх використовувати, підсумок:

У C # не виконуйте розширену або нескінченну логіку в конструкторах, тому що ... є кращі альтернативи


1
Схоже, він не пропонує нічого суттєвого щодо питань, викладених та пояснених у попередніх 9 відповідях
gnat

1
Гей, я мусив це зробити :) Я подумав, що важливо підкреслити, що, хоча немає правила проти цього, ви не повинні через те, що існують більш прості способи. Більшість інших відповідей зосереджені на технічності того, чому це погано, але це важко продати новому розробнику, який каже, "але він збирає, тому я можу просто залишити це так"
Кріс Шалер

0

У цьому немає нічого поганого. Однак, що буде важливо, це питання, чому ви вирішили використовувати цю конструкцію. Що ви отримали, роблячи це?

По-перше, я не збираюся говорити про використання while(true)в конструкторі. З використанням цього синтаксису в конструкторі немає абсолютно нічого поганого. Однак концепція "запуску ігрового циклу в конструкторі" є такою, яка може викликати деякі проблеми.

У таких ситуаціях дуже часто буває, щоб конструктор просто сконструював об'єкт і мав функцію, яка викликає нескінченний цикл. Це дає користувачеві вашого коду більшу гнучкість. Як приклад різного роду питань, які можуть з’являтися, ви обмежуєте використання свого об'єкта. Якщо хтось хоче мати у своєму класі об’єкт-член, який є "ігровим об'єктом", він повинен бути готовий запустити весь цикл гри у своєму конструкторі, оскільки він повинен сконструювати об'єкт. Це може призвести до катованого кодування для вирішення цього питання. У загальному випадку ви не можете попередньо виділити свій ігровий об’єкт, а потім запустити його пізніше. Для деяких програм це не проблема, але є класи програм, на яких ви хочете мати можливість розподілити все наперед, а потім викликати ігровий цикл.

Одне з головних правил програмування - використовувати найпростіший інструмент для роботи. Це не важке і швидке правило, але воно дуже корисне. Якщо виклику функції було б достатньо, навіщо займатись побудовою об’єкта класу? Якщо я побачу використаний більш потужний складний інструмент, я припускаю, що у розробника була причина для цього, і я почну вивчати, які дивні хитрощі ви можете робити.

Є випадки, коли це може бути розумним. Можливо, бувають випадки, коли у конструкторі вам справді інтуїтивно зрозуміло, що у вас є ігровий цикл. У таких випадках ти з ним біжиш! Наприклад, ваш цикл гри може бути вбудований у значно більшу програму, яка будує класи для втілення деяких даних. Якщо вам потрібно запустити ігровий цикл для створення цих даних, може бути дуже розумним запустити цикл гри в конструкторі. Просто трактуйте це як особливий випадок: було б справедливо це зробити, оскільки загальна програма дозволяє інтуїтивно зрозуміти наступному розробнику, що ви зробили і чому.


Якщо для побудови вашого об'єкта потрібен ігровий цикл, принаймні, замість цього поставте його заводським методом. GameTestResults testResults = runGameTests(tests, configuration);а не GameTestResults testResults = new GameTestResults(tests, configuration);.
користувач253751

@immibis Так, це правда, за винятком випадків, коли це нерозумно. Якщо ви працюєте з системою, що базується на рефлексії, яка створює ваш об'єкт для вас, можливо, у вас немає можливості зробити заводський метод.
Корт Аммон

0

Моє враження, що деякі поняття переплуталися і вилилися в не так чітко сформульоване питання.

Конструктор класу може запустити новий потік, який "ніколи" не закінчиться (хоча я вважаю за краще використовувати while(_KeepRunning)над while(true)тим, що я можу встановити булевий член десь false).

Ви можете витягти цей метод створення та запуску потоку у власну функцію, щоб відокремити побудову об'єкта та фактичний початок його роботи - я вважаю за краще цей спосіб, оскільки я маю кращий контроль над доступом до ресурсів.

Ви також можете ознайомитися з "Активним шаблоном об'єкта". Зрештою, я здогадуюсь, питання було спрямоване на те, коли запустити нитку "Активного об'єкта", і моїм уподобанням є роз'єднання конструкції та початок для кращого контролю.


0

Ваш об’єкт нагадує мені почати завдання . Об'єкт завдання має конструктор, але це є і купа статичних фабричних методів , таких як: Task.Run, Task.Startі Task.Factory.StartNew.

Вони дуже схожі на те, що ви намагаєтесь зробити, і, ймовірно, це «конвенція». Головне питання, яке люди, мабуть, полягає в тому, що таке використання конструктора дивує їх. @ JörgWMittag каже, що він розриває фундаментальний контракт , який, мабуть, означає, що Йорг дуже здивований. Я згоден і більше нічого додати до цього не можу.

Однак я хочу припустити, що ОП намагається статичні заводські методи. Сюрприз зникає, і тут немає жодного принципового контракту . Люди звикли статичні фабричні методи, які роблять спеціальні речі, і їх можна назвати відповідно.

Ви можете надати конструкторів, які дозволяють тонкозернистий контроль (як пропонує @Brandin, щось на кшталт того var g = new Game {...}; g.MainLoop();, що вміщує користувачів, які не хочуть розпочати гру негайно, і, можливо, хочуть передати її спочатку. І ви можете написати щось на кшталт, var runningGame = Game.StartNew();щоб зробити старт це відразу легко.


Це означає, що Йорг принципово здивований, що означає такий сюрприз, як біль у фундаменті.
Стів Джессоп

0

Чому певний час (справжній) цикл у конструкторі насправді поганий?

Це зовсім не погано.

Чому (або це взагалі?) Вважають поганим дизайном, щоб дозволити конструктору класу запустити нескінченний цикл (тобто цикл гри)?

Чому б вам хотілося це робити? Серйозно. Цей клас має єдину функцію: конструктор. Якщо все, що вам потрібно, - це одна функція, тому ми маємо функції, а не конструктори.

Ви згадали кілька очевидних причин проти цього, але ви вийшли з найбільшої:

Є кращі та простіші варіанти досягнення того самого.

Якщо є 2 варіанти, і один кращий, не важливо, наскільки це краще, ви вибрали кращий. До речі, саме тому ми НЕ НЕ майже ніколи не використовувати GoTo.


-1

Мета конструктора - добре сконструювати ваш об'єкт. Перед тим, як конструктор завершиться, в принципі небезпечно використовувати будь-які методи цього об’єкта. Звичайно, ми можемо викликати методи об'єкта в конструкторі, але тоді, коли ми це робимо, ми повинні переконатися, що це дійсно для об'єкта в його поточному напівзапеченому стані.

Побічно вкладаючи всю логіку в конструктор, ви додаєте більше такого навантаження на себе. Це говорить про те, що ви не спроектували різницю між ініціалізацією та використанням достатньо добре.


1
це, здається, не пропонує нічого суттєвого щодо питань, викладених та пояснених у попередніх 8 відповідях
gnat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.