コンストラクターは通常、メソッドを呼び出さないでください


12

メソッドを呼び出すコンストラクターがアンチパターンになり得る理由を同僚に説明しました。

例(さびたC ++で)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

あなたの追加の貢献を通じて、この事実をより良く動機付けたいと思います。例、本の参照、ブログのページ、または原則の名前があれば、大歓迎です。

編集:私は一般的に話しているが、私たちはPythonでコーディングしています。


これは一般的なルールですか、それとも特定の言語に固有ですか?
ChrisF

どの言語?C ++では、アンチパターン以上のものです:parashift.com/c++
faq

@ Lenny222、OPは「クラスメソッド」について語っています。これは、少なくとも私にとっては、非インスタンスメソッドを意味します。したがって、仮想化することはできません。
ペテルトレック

3
@Alb Javaでは、まったく問題ありません。ただしthis、コンストラクターから呼び出すメソッドに明示的に渡すことはできません。
biziclop

3
@Stefano Borini:Pythonでコーディングしている場合、さびたC ++ではなくPythonで例を表示してみませんか?また、これがなぜ悪いのかを説明してください。私たちは常にそれを行います。
S.Lott

回答:


26

言語を指定していません。

C ++では、コンストラクターは仮想関数を呼び出すときに注意する必要があります。これは、コンストラクターが呼び出す実際の関数がクラス実装であるためです。実装のない純粋な仮想メソッドである場合、これはアクセス違反になります。

コンストラクターは、非仮想関数を呼び出す場合があります。

あなたの言語がJavaであり、関数が一般にデフォルトで仮想である場合、特別に注意する必要があることは理にかなっています。

C#は状況をあなたが期待する方法で処理しているようです:コンストラクターで仮想メソッドを呼び出すことができ、最も最終的なバージョンを呼び出します。したがって、C#ではアンチパターンではありません。

コンストラクターからメソッドを呼び出す一般的な理由は、共通の「init」メソッドを呼び出す複数のコンストラクターがあるためです。

デストラクタには仮想メソッドでも同じ問題があるため、デストラクタの外側にある仮想「クリーンアップ」メソッドを使用して、ベースクラスデストラクタによって呼び出されることを期待できないことに注意してください。

JavaとC#にはデストラクタがなく、ファイナライザがあります。Javaの動作がわかりません。

これに関して、C#はクリーンアップを正しく処理するようです。

(JavaとC#にはガベージコレクションがありますが、これはメモリの割り当てのみを管理することに注意してください。デストラクタが行う必要がある他のクリーンアップはメモリを解放しないことです)。


13
ここにはいくつかの小さなエラーがあります。C#のメソッドはデフォルトでは仮想ではありません。コンストラクターで仮想メソッドを呼び出す場合、C#はC ++とは異なるセマンティクスを持ちます。現在構築されている型の部分の仮想メソッドではなく、最も派生した型の仮想メソッドが呼び出されます。C#はそのファイナライズメソッドを「デストラクタ」と呼びますが、ファイナライザのセマンティクスを持っているのは当然です。C#デストラクタで呼び出される仮想メソッドは、コンストラクタで実行されるのと同じように機能します。最も派生したメソッドが呼び出されます。
エリックリッパー

@Péter:インスタンスメソッドを対象としました。混乱させて申し訳ありません。
ステファノボリーニ

1
@Eric Lippert。C#の専門知識をありがとう。それに応じて回答を編集しました。私はその言語については知りません、私はC ++をよく知っていますが、Javaはあまりよく知っていません。
CashCow

5
どういたしまして。C#の基本クラスコンストラクターで仮想メソッドを呼び出すことは、まだかなり悪い考えであることに注意してください。
エリックリッパー

コンストラクターからJavaの(仮想)メソッドを呼び出すと、常に最も派生したオーバーライドが呼び出されます。しかし、あなたが「期待する方法」と呼ぶものは、私が紛らわしいと呼ぶものです。Javaは最も派生したオーバーライドを呼び出しますが、そのメソッドは処理されたフィールド化された初期化子のみを参照し、独自のクラス実行のコンストラクターは参照しません。不変式がまだ確立されていないクラスでメソッドを呼び出すのは危険です。ですから、ここではC ++がより良い選択をしたと思います。
5gon12eder

18

OK、クラスメソッドインスタンスメソッドに関する混乱が解消されたので、答えを出すことができます:-)

一般的な問題は、コンストラクターからインスタンスメソッドを呼び出すことではありません。仮想メソッドの呼び出し(直接的または間接的)に伴います。そして主な理由は、コンストラクター内ではオブジェクトがまだ完全に構築されていないことです。特に、サブクラスの部分は、基本クラスコンストラクターの実行中にはまったく構築されません。そのため、その内部状態は言語に依存した方法で一貫しておらず、これは異なる言語で異なる微妙なバグを引き起こす可能性があります。

C ++とC#は、すでに他の人によって議論されています。Javaでは、最も派生した型の仮想メソッドが呼び出されますが、その型はまだ初期化されていません。そのため、そのメソッドが派生型のフィールドを使用している場合、それらのフィールドはその時点でまだ適切に初期化されていない可能性があります。この問題については、Effecive Java 2nd Edition、Item 17:Design and document for inheritanceで禁止されています。

これは、オブジェクト参照を時期尚早公開するという一般的な問題の特殊なケースであることに注意してください。インスタンスメソッドには暗黙的なthisパラメータがありますがthis、メソッドに明示的に渡すと同様の問題が発生する可能性があります。特に、オブジェクト参照が早まって別のスレッドに公開されている場合、そのスレッドは、最初のスレッドのコンストラクターが終了する前に既にそのメソッドを呼び出すことができます。


3
(+1)「コンストラクター内では、オブジェクトはまだ完全には構築されていません。」「クラスメソッドとインスタンス」と同じです。一部のプログラミング言語は、プログラマーがコンストラクターに値を割り当てるかのように、コンストラクターに入るときに構築されると考えています。
umlcat

7

ここでのメソッド呼び出しは、それ自体がアンチパターンであるとは考えていません。よりコード臭いです。クラスがresetメソッドを提供し、オブジェクトを元の状態に戻す場合reset()、コンストラクターの呼び出しはDRYです。(私はリセット方法についての声明を出していません)。

権威への訴えを満足させるのに役立つ記事は次のとおりです。http//misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

それは実際にはメソッドの呼び出しではなく、あまりにも多くのコンストラクターについてです。私見、コンストラクターでのメソッドの呼び出しは、コンストラクターが重すぎることを示している可能性があります。

これは、コードのテストがいかに簡単であるかに関係しています。理由は次のとおりです。

  1. 単体テストには多くの作成と破棄が含まれます。そのため、構築は高速になります。

  2. これらのメソッドの動作によっては、コンストラクターで設定された(テストできない可能性のある)前提条件(ネットワークから情報を取得するなど)に依存せずに、コードの個別のユニットをテストすることが難しくなる場合があります。


3

哲学的に、コンストラクタの目的は、メモリの生のチャンクをインスタンスに変えることです。コンストラクターの実行中は、オブジェクトはまだ存在しないため、メソッドを呼び出すことはお勧めできません。結局のところ、彼らが内部で何をしているのか分からないかもしれず、彼らはオブジェクトが呼び出されたときに少なくともオブジェクトが存在することを正当に考えるかもしれません(duh!)。

技術的には、C ++で、特にPythonでは、何も問題はないかもしれませんが、注意するのはあなた次第です。

実際には、クラスメンバーを初期化するメソッドのみに呼び出しを制限する必要があります。


2

これは汎用的な問題ではありません。C ++では、特に継承と仮想メソッドを使用する場合に問題が発生します。オブジェクトの構築が逆方向に行われ、vtableポインターが継承階層の各コンストラクターレイヤーでリセットされるため、仮想メソッドを呼び出す場合は、最終的には、作成しようとしているクラスに実際に対応するクラスを取得することになり、仮想メソッドを使用する目的全体を無効にします。

健全なOOPサポートを備えた言語では、最初からvtableポインターを正しく設定するため、この問題は存在しません。


2

メソッドの呼び出しには2つの問題があります。

  • 仮想メソッドを呼び出します。これは、予期しないこと(C ++)を行うか、まだ初期化されていないオブジェクトの一部を使用することができます
  • オブジェクトは必ずしも完全ではないため、パブリックメソッドを呼び出す(クラスの不変条件を強制する必要があります)

前の2つのケースに当てはまらない限り、ヘルパー関数の呼び出しに問題はありません。


1

私はこれを買いません。オブジェクト指向システムでは、メソッドを呼び出すことしかできません。実際、それは多かれ少なかれ「オブジェクト指向」の定義です。コンストラクターがメソッドを呼び出せない場合、何ができるのでしょうか?


オブジェクトを初期化します。
ステファノボリーニ

@Stefano Borini:どうやって?オブジェクト指向システムでは、できることはメソッドの呼び出しだけです。または、反対の角度から見ると、メソッドを呼び出すことで何でもできます。そして、「何でも」には明らかにオブジェクトの初期化が含まれます。オブジェクトを初期化するためにメソッドを呼び出す必要があるが、コンストラクターがメソッドを呼び出せない場合、コンストラクターはどのようにオブジェクトを初期化できますか?
ヨルグWミットタグ

できるのはメソッドを呼び出すことだけであることは絶対に真実ではありません。オブジェクトの内部を直接呼び出すことなく、状態を初期化することができます...コンストラクターのポイントは、オブジェクトを一貫した状態にすることです。あなたが他のメソッドを呼び出す場合、彼らは方法がない限り、これらは、部分的な状態でオブジェクトを扱うトラブルを有していてもよく、具体的(一般的にヘルパー・メソッドなど)コンストラクタから呼ばれるように作られた
ステファノBorini

@Stefano Borini:「オブジェクトの内部を直接呼び出すことなく、状態を初期化することができます。」悲しいことに、それがメソッドを伴うとき、あなたは何をしますか?コードをコピーして貼り付けますか?
S.Lott

1
@ S.Lott:いいえ、私はそれを呼び出しますが、オブジェクトメソッドの代わりにモジュール関数を保持し、コンストラクタでオブジェクト状態に入れることができる戻りデータを提供するようにします。オブジェクトメソッドが本当に必要な場合は、プライベートにし、適切な名前を付けるなど、初期化用であることを明確にします。ただし、コンストラクタからオブジェクトステータスを設定するためにパブリックメソッドを呼び出すことはありません。
ステファノボリーニ

0

OOP理論では重要ではありませんが、実際には、各OOPプログラミング言語は異なるコンストラクターを処理します。静的メソッドはあまり使用しません。

C ++&Delphiで、一部のプロパティ(「フィールドメンバー」)に初期値を指定する必要があり、コードが非常に拡張されている場合、コンストラクターの拡張としていくつかのセカンダリメソッドを追加します。

そして、より複雑なことを行う他のメソッドを呼び出さないでください。

プロパティの "getters"および "setters"メソッドについては、通常、プライベート/保護された変数を使用してその状態を保存し、さらに "getters"および "setters"メソッドを使用します。

コンストラクターでは、プロパティステートフィールドに「デフォルト」値を割り当て、「アクセサー」を呼び出すことはありません

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.