時間がかかる可能性がある操作をコンストラクターで実行する必要があるか、オブジェクトを構築して後で初期化する必要があります。
たとえば、ディレクトリ構造を表すオブジェクトを構築する場合、オブジェクトとその子の作成はコンストラクターで行う必要があります。明らかに、ディレクトリにはディレクトリを含めることができ、ディレクトリにはディレクトリなどを含めることができます。
これに対するエレガントな解決策は何ですか?
時間がかかる可能性がある操作をコンストラクターで実行する必要があるか、オブジェクトを構築して後で初期化する必要があります。
たとえば、ディレクトリ構造を表すオブジェクトを構築する場合、オブジェクトとその子の作成はコンストラクターで行う必要があります。明らかに、ディレクトリにはディレクトリを含めることができ、ディレクトリにはディレクトリなどを含めることができます。
これに対するエレガントな解決策は何ですか?
要約する:
少なくとも、コンストラクターは、不変条件が true になるようにオブジェクトを構成する必要があります。
不変条件の選択は、クライアントに影響を与える可能性があります (オブジェクトは常にアクセスできる状態にあることを約束しますか? それとも、特定の状態でのみ可能ですか?)クラスのクライアントのために。
長時間実行されるコンストラクターは本質的に悪いわけではありませんが、状況によっては悪い場合があります。
ユーザーとの対話を伴うシステムの場合、長時間実行されるメソッドは応答性が低下する可能性があるため、避ける必要があります。
コンストラクターの後まで計算を遅らせることは、効果的な最適化になる可能性があります。すべての作業を実行する必要がなくなる場合があります。これはアプリケーションに依存するため、時期尚早に決定するべきではありません。
全体的に、それは依存します。
通常、コンストラクターに計算を行わせたくありません。コードを使用する他の誰かは、基本的なセットアップ以上のことを期待することはありません。
あなたが話しているようなディレクトリツリーの場合、「エレガントな」解決策は、オブジェクトの構築時に完全なツリーを構築しないことです。代わりに、オンデマンドでビルドしてください。あなたのオブジェクトを使用する人は、サブディレクトリの内容をあまり気にしないかもしれません。そのため、コンストラクタに最初のレベルをリストさせることから始めて、誰かが特定のディレクトリに降りたい場合は、要求されたときにツリーのその部分を構築します。それ。
必要な時間は、コンストラクターに何かを入れない理由にはなりません。コンストラクター内のコードを明確に保つために、コード自体をプライベート関数に入れて、コンストラクターから呼び出すことができます。
しかし、あなたがやりたいことがオブジェクトに定義された条件を与える必要がなく、最初の使用時に後でそれを行うことができる場合、これはそれを出して後で行うための合理的な議論になります. ただし、クラスのユーザーに依存させないでください。これらのこと (オンデマンドの初期化) は、クラスのユーザーに対して完全に透過的でなければなりません。そうしないと、オブジェクトの重要な不変条件が簡単に壊れてしまう可能性があります。
それは異なります(典型的なCSの回答)。長時間実行されるプログラムの起動時にオブジェクトを構築する場合、コンストラクターで多くの作業を行うことに問題はありません。これが高速応答が期待される GUI の一部である場合、適切ではない可能性があります。いつものように、最善の答えは、最初に最も簡単な方法で試し、プロファイリングし、そこから最適化することです。
この特定のケースでは、サブディレクトリ オブジェクトの遅延構築を行うことができます。最上位ディレクトリの名前のエントリのみを作成します。アクセスされた場合は、そのディレクトリの内容をロードします。ユーザーがディレクトリ構造を探索するときに、これをもう一度行います。
コンストラクターの最も重要な仕事は、オブジェクトに初期の有効な状態を与えることです。私の意見では、コンストラクターに対する最も重要な期待は、コンストラクターに副作用がないようにすることです。
長時間実行されるコンストラクターが本質的に悪いわけではないことに同意します。しかし、私はあなたがすることはほとんど常に間違っていると主張します. 私のアドバイスは、Hugo、Rich、および Litb からのアドバイスに似ています。
I/O の問題の例: 多くのハード ディスクには、数百ミリ秒または数千ミリ秒の間、読み取りまたは書き込みを処理しない状態になるという問題があります。第 1 世代および第 1 世代のソリッド ステート ドライブでは、これが頻繁に行われます。ユーザーは、あなたのプログラムがちょっとだけハングアップしたことを知る方法を手に入れました。
もちろん、長時間実行されるコンストラクターの悪さは、次の 2 つの要因に依存しています。
ここで、「長い」というのが単純に数 100 クロック サイクルの余分な作業である場合、それほど長くはありません。しかし、コンストラクターは数百マイクロ秒の範囲に入っています。かなり長いことをお勧めします。もちろん、これらのうちの 1 つだけをインスタンス化する場合、またはほとんどインスタンス化しない場合 (たとえば、数秒ごとに 1 つ)、この範囲の継続時間による問題が発生する可能性は低くなります。
頻度は重要な要素です。数個しかビルドしない場合、500 us の ctor は問題になりません。ただし、100 万個の ctor を作成すると、重大なパフォーマンスの問題が発生します。
あなたの例について話しましょう:「クラスディレクトリ」オブジェクト内のディレクトリオブジェクトのツリーを作成します。(注、これはグラフィカル UI を備えたプログラムであると仮定します)。ここで、CTOR の所要時間は、記述したコードには依存しません。被告は、任意に大きなディレクトリ ツリーを列挙するのにかかる時間に依存しています。これは、ローカルのハード ドライブでは十分に悪いことです。リモート(ネットワーク化された)リソースではさらに問題があります。
ここで、ユーザー インターフェイス スレッドでこれを行うことを想像してみてください。UI は、数秒、数十秒、場合によっては数分間、そのトラックで完全に停止します。Windows では、これを UI ハングと呼びます。それらは悪い悪い悪いです(はい、私たちはそれらを持っています...はい、それらを排除するために一生懸命働いています).
UIハングは、人々があなたのソフトウェアを本当に嫌う原因になる可能性があります.
ここで行うべき正しいことは、単にディレクトリ オブジェクトを初期化することです。キャンセル可能なループでディレクトリ ツリーを構築し、UI を応答状態に保ちます (キャンセル ボタンは常に機能する必要があります)。
コードのメンテナンス、テスト、およびデバッグのために、コンストラクターにロジックを入れないようにしています。ロジックをコンストラクターから実行したい場合は、ロジックを init() などのメソッドに入れ、コンストラクターから init() を呼び出すと便利です。単体テストの開発を計画している場合は、さまざまなケースをテストするのが難しい可能性があるため、コンストラクターにロジックを配置しないようにする必要があります。以前のコメントで既にこれに対処していると思いますが...アプリケーションがインタラクティブな場合は、パフォーマンスが著しく低下する単一の呼び出しを避ける必要があります。アプリケーションが非対話型 (例: 夜間のバッチ ジョブ) の場合、1 つのパフォーマンス ヒットはそれほど大きな問題ではありません。
歴史的には、コンストラクター メソッドが完成したらすぐにオブジェクトを使用できるように、コンストラクターをコーディングしてきました。含まれるコードの量は、オブジェクトの要件によって異なります。
たとえば、次の Company クラスを詳細ビューに表示する必要があるとします。
public class Company
{
public int Company_ID { get; set; }
public string CompanyName { get; set; }
public Address MailingAddress { get; set; }
public Phones CompanyPhones { get; set; }
public Contact ContactPerson { get; set; }
}
会社に関するすべての情報を詳細ビューに表示したいので、コンストラクターには、すべてのプロパティを設定するために必要なすべてのコードが含まれます。これが複合型であることを考えると、Company コンストラクターは、Address、Phones、および Contact コンストラクターの実行もトリガーします。
ここで、CompanyName と主要な電話番号のみが必要なディレクトリ リスト ビューを生成する場合、その情報のみを取得し、残りの情報を空のままにするクラスに 2 番目のコンストラクターを作成するか、単に作成するだけです。その情報のみを保持する別のオブジェクト。それは、情報がどのように、どこから取得されるかにかかっています。
クラスのコンストラクターの数に関係なく、私の個人的な目標は、オブジェクトに課せられるタスクに備えてオブジェクトを準備するために必要な処理を行うことです。
RAIIはC++リソース管理のバックボーンであるため、コンストラクターで必要なリソースを取得し、デストラクタで解放します。
これは、クラスの不変条件を確立するときです。時間がかかると時間がかかります。「Xが存在する場合はYを実行する」構成が少ないほど、クラスの残りの部分の設計は簡単になります。後で、プロファイリングでこれが問題であることがわかった場合は、遅延初期化(最初にリソースが必要になったときにリソースを取得する)などの最適化を検討してください。
コンストラクターでどれだけの作業を行う必要があるかについては、物事がどれほど遅いか、クラスをどのように使用するか、そして一般的にそれについて個人的にどのように感じているかを考慮に入れる必要があると思います.
ディレクトリ構造オブジェクトについて: 私は最近、HTPC 用に samba (Windows 共有) ブラウザを実装しました。たとえば、最初のツリーはマシンのリストだけで構成され、次にディレクトリをブラウズするたびに、システムはそのマシンからツリーを自動的に初期化し、1 レベル深いディレクトリ リストを取得します。
理想的には、ディレクトリを幅優先でスキャンし、現在閲覧しているディレクトリを優先するワーカースレッドを作成することさえできると思いますが、一般的に、それは単純なものにはあまりにも多くの作業です;)
ctor が例外をスローする可能性のあることを何もしないことを確認してください。
コンストラクターの外部で何かを実行できる場合は、内部で実行しないでください。後で、自分のクラスが他の点では適切に動作していることがわかったときに、内部でそれを実行する危険を冒す可能性があります。
必要なだけ、それ以上ではありません。
コンストラクターはオブジェクトを使用可能な状態にする必要があるため、少なくともクラス変数を初期化する必要があります。初期化が意味するものは、広い解釈を持つことができます。これは不自然な例です。N を提供する責任を持つクラスがあるとします。呼び出し元のアプリケーションに。
これを実装する 1 つの方法は、必要な値を計算して返すループを持つメンバー関数を使用して、コンストラクターに何も行わせないことです。
それを実装する別の方法は、配列であるクラス変数を持つことです。コンストラクターは、値がまだ計算されていないことを示すために、すべての値を -1 に設定します。メンバー関数は遅延評価を行います。配列要素を調べます。-1 の場合は、それを計算して格納し、値を返します。それ以外の場合は、配列から値を返すだけです。
それを実装する別の方法は、最後の方法とまったく同じです。コンストラクターのみが値を事前に計算し、配列にデータを入力するため、メソッドは配列から値を取り出して返すことができます。
これを実装するもう 1 つの方法は、値をテキスト ファイルに保持し、ファイルへのオフセットの基準として N を使用して値を取得することです。この場合、コンストラクタはファイルを開き、デストラクタはファイルを閉じますが、メソッドは何らかの fseek/fread を実行して値を返します。
それを実装する別の方法は、値を事前に計算し、それらをクラスが参照できる静的配列として格納することです。コンストラクターは機能せず、メソッドは配列に到達して値を取得し、それを返します。複数のインスタンスがその配列を共有します。
つまり、注意すべきことは、一般に、コンストラクターを一度呼び出してから、他のメソッドを頻繁に使用できるようにすることです。コンストラクターでより多くの作業を行うことが、メソッドが行う作業が少なくなり、より高速に実行されることを意味する場合、それは良いトレードオフです。ループのように多くの構築/破棄を行っている場合、コンストラクターのコストを高くするのはおそらく良い考えではありません。
すばらしい質問です。「ディレクトリ」オブジェクトが他の「ディレクトリ」オブジェクトへの参照を持っている例も良い例です。
この特定のケースでは、コードを移動して、コンストラクターから下位オブジェクトを構築し (または、ここの別の投稿が推奨するように、最初のレベル [即時の子] を実行します)、別の「初期化」または「構築」メカニズムを使用します)。
そうでなければ、パフォーマンスだけでなく、メモリフットプリントという別の潜在的な問題があります。再帰が終了するまで]。
それは本当にコンテキスト、つまりクラスが解決しなければならない問題に依存します。たとえば、現在の子を常に内部に表示できるようにする必要がありますか? 答えが「はい」の場合、子はコンストラクターにロードされません。一方、クラスがディレクトリ構造のスナップショットを表す場合は、コンストラクターにロードできます。
私は薄いコンストラクターに投票し、その場合、オブジェクトに追加の「初期化されていない」状態の動作を追加します。
理由:そうしないと、すべてのユーザーに重いコンストラクターも持たせるか、クラスを動的に割り当てる必要があります。どちらの場合も、面倒と見なされる可能性があります。
そのようなオブジェクトが静的になると、コンストラクターが main() の前に実行され、デバッガーがトレースするのがより困難になるため、そのようなオブジェクトからエラーをキャッチするのは難しい場合があります。
そこに必要だと思うものを用意してみてください。それが遅いか速いかは考えないでください。事前最適化は時間の無駄なので、必要に応じてコーディングし、プロファイリングして最適化してください。
オブジェクトの配列は、常にデフォルト (引数なし) のコンストラクターを使用します。それを回避する方法はありません。
「特別な」コンストラクターがあります。コピー コンストラクターと operator=() です。
たくさんのコンストラクターを持つことができます!または、後で多くのコンストラクターを使用することになります。時折、ラ・ラ・ランドで請求書を発行する人は、この 4 バイトを節約するために、double ではなく float を使用した新しいコンストラクターを必要とします。(RAM 請求書を購入してください!)
通常のメソッドのようにコンストラクターを呼び出して、その初期化ロジックを再度呼び出すことはできません。
コンストラクターのロジックを仮想化して、サブクラスで変更することはできません。(ただし、手動ではなくコンストラクターから initialize() メソッドを呼び出している場合、仮想メソッドは機能しません。)
.
コンストラクターに重要なロジックが存在する場合、これらすべてが多くの悲しみを生み出します。(または、少なくともコードの複製。)
したがって、私は、設計上の選択として、(オプションで、パラメーターと状況に応じて) initialize() メソッドを呼び出す最小限のコンストラクターを使用することを好みます。
状況によっては、initialize() が非公開になる場合があります。または、パブリックで、複数の呼び出し (再初期化など) をサポートする場合があります。
.
最終的に、ここでの選択は状況によって異なります。柔軟に対応し、トレードオフを考慮する必要があります。画一的なものはありません。
スレッドを使用して専用ハードウェアと通信し、1 時間に 1/2 で記述しなければならない単一の孤立したインスタンスを持つクラスを実装するために使用するアプローチは、必ずしもクラスの実装に使用するものではありません。何ヶ月にもわたって書かれた可変精度浮動小数点数の数学を表します。