web-dev-qa-db-ja.com

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

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

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

クラスのコンストラクターに無限ループ(つまり、ゲームループ)を開始させることが悪いデザインと見なされるのはなぜですか(またはまったくそうですか?)

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

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

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

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

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

47
Samuel

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

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

251
Jörg W Mittag

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

はい、もちろん。それは不必要で、予想外で、役に立たない、無責任です。これは、クラス設計の現代の概念(凝集、結合)に違反しています。これはメソッドコントラクトに違反します(コンストラクターには定義済みのジョブがあり、単なるランダムメソッドではありません)。それは確かに十分に保守可能ではありません。将来のプログラマーは、何が起こっているのかを理解し、なぜそのように行われたのか理由を推測しようとすることに多くの時間を費やすでしょう。

これは、コードが機能しないという意味での「バグ」ではありません。しかし、コードを維持するのが難しくなる(つまり、テストを追加するのが難しい、再利用が難しい、デバッグが難しい、拡張が難しいなど)と、長期的には(二次コードを最初に記述するコストに比べて)大きな二次コストが発生する可能性があります。 。)。

ソフトウェア開発メソッドの多くの/最も最近の改善は、ソフトウェアの作成/テスト/デバッグ/保守の実際のプロセスをより簡単にするために特別に行われました。これはすべて、「機能する」ためにコードがランダムに配置されるようなものによって回避されます。

残念ながら、あなたは定期的に、これらすべてを完全に知らないプログラマーに会います。うまくいきました。

類推で終了するには(別のプログラミング言語、ここでの問題は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)で実行されることはありません)
  • 上記のその他すべての理由。
45
AnoE

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

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

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

8
JimmyJames

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

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

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

「構築中」のオブジェクトを使用するつもりがない場合、そもそもオブジェクトをインスタンス化する意味は何ですか。コンストラクタにwhile(true)ループがあることは、効果的に完了することを意図していないことを意味します...

C#は非常に豊富なオブジェクト指向言語であり、さまざまな構成要素とパラダイムを備えており、ツールを調べたり、ツールを理解したり、いつ使用するかを把握したりすることができます。

C#では、コンストラクター内で拡張ロジックまたは決して終了しないロジックを実行しないでください。

1
Chris Schaller

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

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

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

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

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

0
Cort Ammon

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

それは全然悪くありません。

クラスのコンストラクターに無限ループ(つまり、ゲームループ)を開始させるのが悪いデザインと見なされるのはなぜですか(またはまったくそうですか?)

なぜこれをしたいのですか?真剣に。そのクラスには、コンストラクターという単一の関数があります。必要なのが単一の関数である場合は、コンストラクタではなく関数を使用するのはそのためです。

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

同じことを達成するためのより良い、より簡単なオプションがあります。

2つの選択肢があり、1つが優れている場合、どれほど優れているかは問題ではありません。ちなみに、だからこそ しない GoToを使用することはほとんどありません。

0
Peter

私の印象は、いくつかの概念が混同され、あまりよく表現されていない質問をもたらしたということです。

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

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

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

0
Bernhard Hiller

あなたのオブジェクトは Tasks の開始を思い出させます。タスクオブジェクトにはコンストラクタがありますが、_Task.Run_、_Task.Start_、_Task.Factory.StartNew_などの静的なファクトリメソッドも多数あります。

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

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

(@Brandinが示唆しているように、var g = new Game {...}; g.MainLoop();のような、ゲームをすぐに開始したくない、最初に渡したいユーザーに対応できるような、きめ細かい制御を可能にするコンストラクターを提供できます。そして、 var runningGame = Game.StartNew();のようなものを書くと、すぐに開始できます。

0
Nathan Cooper