コンストラクターのwhile(true)ループが実際に悪いのはなぜですか?


47

一般的な質問ではありますが、C ++のような言語はコンストラクターの実行、メモリ管理、未定義の動作などに関して異なるセマンティクスを持っていることを知っているので、私のスコープはむしろC#です。

誰かが私に興味深い質問をしましたが、それは私には簡単には答えられませんでした。

クラスのコンストラクターが終わりのないループ(つまり、ゲームループ)を開始できるようにするのは悪い設計と見なされるのはなぜですか(またはまったくそうではないのですか)。

これによって破られるいくつかの概念があります:

  • 最小限の驚きの原則のように、ユーザーはコンストラクターがこのように動作することを期待していません。
  • ユニットテストは、このクラスを作成したり、ループを終了しないため挿入したりできないため、より困難です。
  • ループの終わり(ゲームの終わり)は、概念的にはコンストラクターが終了する時間であり、これも奇妙です。
  • 技術的には、このようなクラスにはコンストラクター以外のパブリックメンバーがありません。これにより、理解が難しくなります(特に実装が利用できない言語の場合)

そして、技術的な問題があります:

  • コンストラクターは実際には終了しないため、GCで何が起こるのでしょうか?このオブジェクトは既にGen 0ですか?
  • このようなクラスから派生することは、不可能であるか、少なくとも非常に複雑です。これは、基本コンストラクターが決して返さないため

そのようなアプローチでより明らかに悪いまたは不正な何かがありますか?


61
なぜそれが良いのですか?あなたは、単に方法(非常に簡単なリファクタリング)にあなたのメインループを移動する場合、ユーザーは、このような驚くコードを書くことができます:var g = new Game {...}; g.MainLoop();
Brandin

61
あなたの質問はすでにそれを使わない 6つの理由を述べているので、私はそれを支持する1つのものを見たいです。またwhile(true)、プロパティセッターにループを入れるのがなぜ悪い考えなのかと尋ねることもできnew Game().RunNow = trueます。
-Groo

38
極端なアナロジー:ヒトラーがやったことが間違っていると言うのはなぜですか?人種差別、第二次世界大戦の開始、数百万人の殺害、暴力[その他50以上の理由など]を除いて、彼は何も間違っていませんでした。何かが間違っている理由に関する任意の理由のリストを削除しても、文字通り何でも、そのことは良いと結論付けることができます。
バクリウ

4
ガベージコレクション(GC)(技術的な問題)に関しては、特別なことは何もありません。インスタンスは、コンストラクターコードが実際に実行される前に存在します。この場合、インスタンスはまだ到達可能であるため、GCで要求することはできません。GCが設定されると、このインスタンスは存続し、GC設定が発生するたびに、オブジェクトは通常の規則に従って次世代にプロモートされます。GCが行われるかどうかは、(十分な)新しいオブジェクトが作成されるかどうかによって異なります。
ジェッペスティグニールセン

8
さらに一歩進んで、どこで使用されているかに関係なく、while(true)が悪いと言います。これは、ループを停止する明確でクリーンな方法がないことを意味します。あなたの例では、ゲームが終了したとき、またはフォーカスを失ったときにループを停止するべきではありませんか?
ジョンアンダーソン

回答:


250

コンストラクターの目的は何ですか?新しく構築されたオブジェクトを返します。無限ループは何をしますか?それは決して戻りません。コンストラクターがまったく返されない場合、新しく構築されたオブジェクトを返すにはどうすればよいですか?できません。

エルゴ、無限ループは、コンストラクターの基本的な契約を破る:何かを構築します。


11
おそらく彼らは、真ん中に休憩を入れてwhile(true)を置くつもりだったのでしょうか?危険ですが、合法です。
ジョンドヴォルザーク

2
同意する、これは正しい-少なくとも。学術的な観点から。何かを返すすべての関数に同じことが当てはまります。
ドックブラウン

61
上記のクラスのサブクラスを作成する貧しい芝を想像してください
...-ニュートピア

5
@JanDvorak:それは別の質問です。OPは「終了しない」を明示的に指定し、イベントループに言及しました。
ヨルグ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を返します。正しい。異議(開発者が異議を唱えるために与える可能性のあるすべての通常の理由と共に)は次のとおりです。

  • 読みづらいです(でも、いいプログラマーなら簡単に読むことができますし、私たちはいつもこのようにやっています;望むならメソッドにリファクタリングできます!)
  • 遅い(ただし、これはタイミングが重要な場所ではないため、無視できます。また、必要な場合にのみ最適化することをお勧めします!)
  • より多くのリソースを使用します(新しいプロセス、より多くのRAMなど-しかし、私たちのサーバーは十分に高速です)
  • 依存関係を導入しますbash(ただし、Windows、Mac、Androidでは動作しません)
  • そして、上記の他のすべての理由。

5
「GCは、コンストラクターの実行中に例外的なロック状態になる可能性があります」-なぜですか?アロケータは最初に割り当てを行い、次にコンストラクタを通常のメソッドとして起動します。特別なことは何もありませんか?とにかく、アロケーターはコンストラクター内で新しい割り当てを許可する必要があります(そして、これらはOOMの状況を引き起こす可能性があります)。
ジョン・ドヴォルザーク

1
@JanDvorak、コンストラクター内で「どこか」で特別な動作が発生する可能性があることを指摘したかったのですが、あなたが正しいことは、私が選んだ例は非現実的でした。削除されました。
AnoE

私はあなたの説明が本当に好きで、両方の返信を回答としてマークしたいです(少なくとも賛成票を持っています)。類推はいいですし、二次コストについてのあなたの考えと説明の軌跡もそれですが、私はそれらをコードの補助的な要件と考えています(私の議論と同じです)。現在の最新技術はコードをテストすることであるため、テスト容易性などは事実上の要件です。しかし、@JörgWMittagのステートメントfundamental contract of a constructor: to construct somethingは注目されています。
サミュエル

アナロジーはあまり良くありません。私がこれを見ると、なぜあなたがBashを使って数学をしているのかについてのコメントをどこかで見ることを期待するでしょう、そしてあなたの理由によってはそれは完全に良いかもしれません。基本的には、コンストラクターを使用したすべての場所でコメントに謝罪をする必要があるため、同じことはOPでも機能しません。「//注:このオブジェクトを構築すると、戻り値のないイベントループが開始されます」。
ブランディン

4
「残念ながら、あなたは定期的にこのすべてに完全に無知なプログラマーに会います。それは動作します、それだけです。」とても悲しいけれど、本当です。私たちの職業生活における実質的な負担は、そういう人たちだけだと言ったら、私たち全員のために話すことができると思います。
モニカとの軽さレース

8

あなたはあなたの質問にこのアプローチを除外するのに十分な理由を与えますが、ここでの実際の質問は「そのようなアプローチにはもっと明らかに悪いものや不正なものがありますか?」です。

ここで私の最初のことは、これが無意味であることでした。コンストラクターが終了しない場合、プログラムの他の部分は構築されたオブジェクトへの参照を取得できないため、通常のメソッドではなくコンストラクターにオブジェクトを配置する背後にあるロジックは何ですか。それから、唯一の違いは、ループ内で、ループから部分的に構築されたthisエスケープへの参照を許可できるということでした。これはこの状況に厳密に限定されるものではありませんがthis、エスケープを許可した場合、これらの参照は常に完全に構​​築されていないオブジェクトを指すことが保証されます。

この種の状況に関するセマンティクスがC#で適切に定義されているかどうかはわかりませんが、ほとんどの開発者が掘り下げようとするものではないので、問題ではないと主張します。


私は、オブジェクトが作成された後にコンストラクターが実行されると仮定します。したがって、これが完全に適切に定義された振る舞いをする適切な言語リークが発生します。
-mroman

@mroman:thisコンストラクターでリークしてもかまいません-ただし、最も派生したクラスのコンストラクターにいる場合のみです。あなたは二つのクラス、持っている場合ABB : Aのコンストラクタがあれば、A漏れてしまうthis、のメンバーBはまだ未初期化されるだろう(とと仮想メソッド呼び出しやキャストB缶wreakの混乱は、それに基づいて)。
hoffmale

1
確かに、C ++ではおかしくなりそうです。他の言語では、メンバーは実際の値が割り当てられる前にデフォルト値を受け取るので、そのようなコードを実際に書くのは愚かですが、動作は依然として明確に定義されています。これらのメンバーを使用するメソッドを呼び出すと、NullPointerExceptionsまたは間違った計算(数値がデフォルトで0になっているため)またはそのようなことが発生する可能性がありますが、何が起こるかは技術的に決定的です。
mroman

@mromanそれが事実であることを示すCLRの決定的なリファレンスを見つけることができますか?
ジミージェームズ

コンストラクターを実行する前にメンバー値を割り当てると、オブジェクトが有効な状態で存在し、メンバーがデフォルト値またはコンストラクターが実行される前に割り当てられた値を持つ状態になります。メンバーを初期化するためにコンストラクターは必要ありません。それをドキュメントのどこかに見つけることができるかどうか見てみましょう。
mroman

1

+1少なくとも驚いたことですが、これは新しい開発者に明確に伝えるのが難しい概念です。実用的なレベルでは、コンストラクター内で発生した例外をデバッグすることは困難です。オブジェクトの初期化に失敗すると、そのコンストラクターの状態を検査したり、そのコンストラクターの外部からログを記録したりできなくなります。

この種のコードパターンを実行する必要がある場合は、代わりにクラスの静的メソッドを使用してください。

オブジェクトがクラス定義からインスタンス化されるときに、初期化ロジックを提供するコンストラクターが存在します。このオブジェクトインスタンスを構築するのは、残りのアプリケーションロジックから呼び出したいプロパティと機能のセットをカプセル化するコンテナとしてのインスタンスだからです。

「構築中」のオブジェクトを使用しない場合、最初にオブジェクトをインスタンス化するポイントは何ですか?コンストラクターでwhile(true)ループを使用すると、実際にループを完了するつもりはありません...

C#は非常にリッチなオブジェクト指向言語であり、さまざまな構成要素とパラダイムを使用して、ツールを調べたり、ツールを使用したり、使用するタイミングを把握したりできます。

C#では、コンストラクター内で拡張ロジックや終わらないロジックを実行しないでください。


1
彼は作られたポイントを超える大幅な提供の何にも思えるし、前9つの回答で説明していません
ブヨ

1
ねえ、私はそれを試してみなければなりませんでした:)これを行うことに対する規則はありませんが、より簡単な方法があるという事実によってあなたはすべきではないことを強調することが重要だと思いました。「私はちょうどそのようにそれを残すことができますが、それはコンパイル」他のほとんどの回答は、それが悪い理由の専門的に焦点を当て、それは言う新しいDEVに販売するのは難しい
クリス・シャラー

0

本質的に悪いことは何もありません。ただし、重要なのは、この構成を使用することを選択した理由の問題です。これを行うことで何を得ましたか?

まずwhile(true)、コンストラクターでの使用については説明しません。ありません、絶対にコンストラクタでその構文を使用して何の問題も。ただし、「コンストラクターでゲームループを開始する」という概念は、いくつかの問題を引き起こす可能性があります。

これらの状況では、コンストラクターにオブジェクトを単純に構築させ、無限ループを呼び出す関数を持たせることが非常に一般的です。これにより、コードのユーザーはより柔軟になります。表示される可能性のある問題の例として、オブジェクトの使用を制限することがあります。誰かがクラスの「ゲームオブジェクト」であるメンバーオブジェクトを持ちたい場合、オブジェクトを構築する必要があるため、コンストラクターでゲームループ全体を実行する準備ができている必要があります。これにより、この問題を回避するためにコーディングが拷問される可能性があります。一般的なケースでは、ゲームオブジェクトを事前に割り当てて、後で実行することはできません。一部のプログラムでは問題になりませんが、すべてを事前に割り当ててからゲームループを呼び出すことができるようにしたいプログラムのクラスがあります。

プログラミングの経験則の1つは、ジョブに最も単純なツールを使用することです。これは難しくて速いルールではありませんが、非常に便利なルールです。関数呼び出しで十分だったら、なぜクラスオブジェクトを作成するのが面倒でしょうか?より強力で複雑なツールが使用されているのを見ると、開発者にその理由があると思い、どのような奇妙なトリックをしているのかを探り始めます。

これが妥当な場合があります。コンストラクターでゲームループを使用することが本当に直感的である場合があります。それらの場合、あなたはそれで走ります!たとえば、ゲームループは、いくつかのデータを具体化するクラスを構築するはるかに大きなプログラムに埋め込まれている場合があります。そのデータを生成するためにゲームループを実行する必要がある場合、コンストラクターでゲームループを実行することは非常に合理的です。これを特別なケースとして扱ってください。包括的なプログラムにより、次の開発者があなたが何をしたのか、なぜ理解したのかを直感的に理解できるため、これを行うのは有効です。


オブジェクトの構築にゲームループが必要な場合は、少なくとも代わりにファクトリメソッドに入れてください。GameTestResults testResults = runGameTests(tests, configuration);ではなくGameTestResults testResults = new GameTestResults(tests, configuration);
user253751

@immibisはい、それは理にかなっていない場合を除いて本当です。オブジェクトをインスタンス化するリフレクションベースのシステムで作業している場合、ファクトリメソッドを作成するオプションがない場合があります。
コートアンモン

0

私の印象では、いくつかの概念がまちまちになり、あまりよく表現されていない質問になりました。

クラスのコンストラクターは、「決して」終了しない新しいスレッドを開始する場合があります(ただし、ブールメンバーをどこかでfalseに設定できるので、使用while(_KeepRunning)する方が好きですwhile(true))。

オブジェクトの構築と実際の作業の開始を分離するために、スレッドを作成して開始するメソッドを独自の関数に抽出することができます-リソースへのアクセスをよりよく制御できるので、この方法が好きです。

「アクティブオブジェクトパターン」もご覧ください。結局、質問は「アクティブオブジェクト」のスレッドをいつ開始するかを目的としていたと思います。私の好みは、構築を分離し、より良い制御を開始することです。


0

あなたのオブジェクトは、タスクの開始を思い出させます。タスクオブジェクトは、コンストラクタを持っていますが、それをのようなstaticファクトリメソッドの束もありますTask.RunTask.StartTask.Factory.StartNew

これらはあなたがやろうとしていることに非常に似ており、これはおそらく「慣習」です。人々が抱えている主な問題は、このコンストラクタの使用が彼らを驚かせるということです。@JörgWMittagは、基本的な契約を破ると言います。これは、 Jörgが非常に驚いたことを意味すると思います。私は同意し、それに追加するものは何もありません。

ただし、OPが静的ファクトリメソッドを試行することをお勧めします。驚きは消え、ここには基本的な契約はありません。人々は特別なことをする静的なファクトリメソッドに慣れており、それに応じて名前を付けることができます。

きめ細かな制御を可能にするコンストラクターを提供できます(@Brandinが示唆するようvar g = new Game {...}; g.MainLoop();に、ゲームをすぐに開始したくない場合や、最初にゲームをやり直したい場合var runningGame = Game.StartNew();に適しています)。すぐに簡単に。


それは、ヨルクが根本的に驚くことを意味します。つまり、そのような驚きは基本の痛みです。
スティーブジェソップ

0

コンストラクターのwhile(true)ループが実際に悪いのはなぜですか?

それはまったく悪くありません。

クラスのコンストラクターが終わりのないループ(つまり、ゲームループ)を開始できるようにするのは悪い設計と見なされるのはなぜですか(またはまったくそうではないのですか)。

なぜあなたはこれをしたいのですか?真剣に。このクラスには、コンストラクターという1つの関数があります。必要なのが単一の関数だけである場合、それがコンストラクタではなく関数を持っている理由です。

あなたはそれに対する明白な理由のいくつかを自分で述べましたが、あなたは非常に大きなものを省きました:

同じことを達成するための、より優れたシンプルなオプションがあります。

2つの選択肢があり、1つがより良い場合、どれだけ良いかは関係ありません。より良い方を選択します。ちなみに、これがGoToをほとんど使用しない理由です。


-1

コンストラクタの目的は、オブジェクトを構築することです。コンストラクターが完了する前に、そのオブジェクトのメソッドを使用することは原則として安全ではありません。もちろん、コンストラクタ内でオブジェクトのメソッドを呼び出すこともできますが、そのたびに、そのようにすることは、現在の半ば焼き状態のオブジェクトに対して有効であることを確認する必要があります。

間接的にすべてのロジックをコンストラクターに配置することにより、この種の負担を自分に追加します。これは、初期化と十分な使用の違いを設計しなかったことを示唆しています。


1
これは、以前の8つの回答で作成され説明されたポイントに対して実質的な何かを提供していないようです
-gnat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.