コンストラクターが例外をスローするのはいつが適切ですか? (または、Objective Cの場合:初期化者がnilを返すのはいつですか?)
オブジェクトが完全でない場合、コンストラクターは失敗するはずです(したがって、オブジェクトの作成を拒否します)。つまり、コンストラクターは、メソッドを有意に呼び出すことができる機能的かつ作業オブジェクトを提供するために、呼び出し元との契約を持っている必要がありますか?それは合理的ですか?
コンストラクターの仕事は、オブジェクトを使用可能な状態にすることです。これには基本的に2つの考え方があります。
1つのグループは2段階構成を好みます。コンストラクタは単にオブジェクトをスリーパー状態にし、そこでは作業を拒否します。実際の初期化を行う追加の関数があります。
このアプローチの背後にある理由を理解したことはありません。私は、1段階の構築をサポートするグループにしっかりと所属しています。オブジェクトは、構築後に完全に初期化され、使用可能です。
1段階のコンストラクターは、オブジェクトの完全な初期化に失敗した場合にスローする必要があります。オブジェクトを初期化できない場合、オブジェクトの存在を許可してはならないため、コンストラクターはスローする必要があります。
Eric Lippertは言う 4種類の例外があります。
コンストラクターが致命的な例外をスローすることはありませんが、実行するコードが致命的な例外を引き起こす可能性があります。 「メモリ不足」のようなものは、あなたが制御できるものではありませんが、コンストラクターで発生した場合、それは起こります。
あなたのコードのいずれにおいても、骨の折れた例外は決して発生してはならないので、すぐに例外が発生します。
厄介な例外(例はInt32.Parse()
)は、例外的でない状況がないため、コンストラクターによってスローされるべきではありません。
最後に、外因性の例外は回避する必要がありますが、外部環境(ネットワークやファイルシステムなど)に依存するコンストラクターで何かをしている場合は、例外をスローするのが適切です。
参照リンク: https://blogs.msdn.Microsoft.com/ericlippert/2008/09/10/vexing-exceptions/
一般的にオブジェクトの初期化を構築から離すことによって得られるものは何もありません。 RAIIは正しいです。コンストラクターの呼び出しが成功すると、ライブオブジェクトが完全に初期化されるか失敗するはずです。また、[〜#〜] all [〜#〜]はコードの任意の時点で失敗します。パスは常に例外をスローする必要があります。別のinit()メソッドを使用しても、あるレベルでの複雑さが増す以外は何も得られません。 ctorコントラクトは、機能する有効なオブジェクトを返すか、それ自体をクリーンアップしてスローする必要があります。
別のinitメソッドを実装する場合、stillを呼び出す必要があることを考慮してください。それでも例外をスローする可能性があり、それらを処理する必要があり、コンストラクターの直後に事実上常に呼び出す必要がありますが、2の代わりに4つの可能なオブジェクト状態(IE、構築、初期化、未初期化、失敗対単に有効かつ存在しない)。
いずれにせよ、私は25年でOO別のinitメソッドが「問題を解決する」と思われる開発ケースは設計上の欠陥です。オブジェクトが必要ない場合今なら、あなたは今それを構築するべきではなく、あなたが今それを必要とするなら、あなたはそれを初期化する必要があります。KISSは、行動、状態、およびインターフェイスのAPIは、オブジェクトが何を行うかではなく、オブジェクトが何を行うかを反映する必要があります。クライアントコードは、オブジェクトが初期化を必要とする内部状態を持っていることを認識してはなりません。
部分的に作成されたクラスが引き起こす可能性のあるすべての問題のために、私は決して言いません。
構築中に何かを検証する必要がある場合は、コンストラクタをプライベートにし、パブリックな静的ファクトリメソッドを定義します。何かが無効な場合、メソッドはスローできます。しかし、すべてがチェックアウトされると、コンストラクターが呼び出され、コンストラクターはスローしないことが保証されます。
コンストラクターは、オブジェクトの構築を完了できない場合に例外をスローする必要があります。
たとえば、コンストラクターが1024 KBのRAMを割り当てることになっていて、そうしなかった場合は、例外をスローする必要があります。これにより、コンストラクターの呼び出し元は、オブジェクトを使用する準備ができておらず、エラーがあることを認識しますどこかに修正する必要があります。
呼び出し側が知る方法が実際にはないため、半分初期化され、半分死んでいるオブジェクトは、単に問題と問題を引き起こします。問題が発生した場合、trueまたはfalseを返すisOK()関数の呼び出しを実行するためにプログラミングに依存するよりも、コンストラクターにエラーをスローさせたいと思います。
特にコンストラクタ内でリソースを割り当てている場合は、常にかなり危険です。言語によっては、デストラクターは呼び出されないため、手動でクリーンアップする必要があります。それはあなたの言語でオブジェクトの寿命がいつ始まるかによって異なります。
本当にやったのは、どこかでセキュリティの問題が発生したときだけです。つまり、オブジェクトを作成できないのではなく、作成するべきではないということです。
コンストラクターが適切にクリーンアップする限り、コンストラクターが例外をスローすることは合理的です。 [〜#〜] raii [〜#〜] パラダイム(リソース獲得は初期化)に従う場合、それはisコンストラクターが意味のある作業を行うことは非常に一般的です。適切に作成されたコンストラクタは、完全に初期化できない場合、それ自体をクリーンアップします。
私の知る限り、1段構造と2段構造の両方の長所を具体化するかなり明白なソリューションを誰も提示していません。
注:この回答はC#を想定していますが、原則はほとんどの言語に適用できます。
まず、両方の利点:
ワンステージ構成は、オブジェクトが無効な状態で存在するのを防ぎ、あらゆる種類の誤った状態管理とそれに付随するすべてのバグを防ぐことで私たちに利益をもたらします。ただし、コンストラクターに例外をスローさせたくないため、一部の人は奇妙に感じます。また、初期化引数が無効な場合にそれが必要な場合もあります。
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
public Person(string name, DateTime dateOfBirth)
{
if (string.IsNullOrWhitespace(name))
{
throw new ArgumentException(nameof(name));
}
if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
}
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
}
2段階構成は、検証をコンストラクターの外部で実行できるようにすることでメリットがあり、そのため、コンストラクター内で例外をスローする必要がなくなります。ただし、「無効な」インスタンスが残ります。つまり、インスタンスの状態を追跡および管理する必要があるか、ヒープ割り当ての直後に破棄します。それは質問を請います:なぜ私たちがヒープ割り当てを実行し、したがってメモリコレクションを、私たちが最終的に使用することさえしないオブジェクトで実行するのですか?
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
public Person(string name, DateTime dateOfBirth)
{
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
public void Validate()
{
if (string.IsNullOrWhitespace(Name))
{
throw new ArgumentException(nameof(Name));
}
if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
}
}
}
それでは、コンストラクタから例外を除外し、すぐに破棄されるオブジェクトに対してヒープ割り当てを実行できないようにするにはどうすればよいでしょうか?それは非常に基本的です:コンストラクターをプライベートにし、インスタンス化を実行するように指定された静的メソッドを介してインスタンスを作成します。したがって、ヒープ割り当てはafter検証。
public class Person
{
public string Name { get; }
public DateTime DateOfBirth { get; }
private Person(string name, DateTime dateOfBirth)
{
this.Name = name;
this.DateOfBirth = dateOfBirth;
}
public static Person Create(
string name,
DateTime dateOfBirth)
{
if (string.IsNullOrWhitespace(Name))
{
throw new ArgumentException(nameof(name));
}
if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
{
throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
}
return new Person(name, dateOfBirth);
}
}
前述の検証とヒープ割り当て防止の利点に加えて、以前の方法論には、非同期サポートというもう1つの素晴らしい利点があります。これは、APIを使用する前にベアラートークンを取得する必要がある場合など、多段階認証を扱う場合に役立ちます。この方法では、無効な「サインアウト」APIクライアントで終わることはなく、代わりに、リクエストの実行中に認証エラーを受け取った場合にAPIクライアントを再作成できます。
public class RestApiClient
{
public RestApiClient(HttpClient httpClient)
{
this.httpClient = new httpClient;
}
public async Task<RestApiClient> Create(string username, string password)
{
if (username == null)
{
throw new ArgumentNullException(nameof(username));
}
if (password == null)
{
throw new ArgumentNullException(nameof(password));
}
var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
var basicAuthValue = Convert.ToBase64String(basicAuthBytes);
var authenticationHttpClient = new HttpClient
{
BaseUri = new Uri("https://auth.example.io"),
DefaultRequestHeaders = {
Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue)
}
};
using (authenticationHttpClient)
{
var response = await httpClient.GetAsync("login");
var content = response.Content.ReadAsStringAsync();
var authToken = content;
var restApiHttpClient = new HttpClient
{
BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri
DefaultRequestHeaders = {
Authentication = new AuthenticationHeaderValue("Bearer", authToken)
}
};
return new RestApiClient(restApiHttpClient);
}
}
}
私の経験では、この方法の欠点はほとんどありません。
一般に、この方法論を使用すると、クラスをDTOとして使用できなくなります。これは、パブリックなデフォルトコンストラクタなしでオブジェクトにデシリアライズするのは、せいぜい難しいからです。ただし、オブジェクトをDTOとして使用している場合は、オブジェクト自体を実際に検証するのではなく、技術的には値が「無効」ではないため、オブジェクトの値を使用しようとして無効にする必要があります。 DTOへ。
また、IOCコンテナがオブジェクトを作成できるようにする必要がある場合は、ファクトリメソッドまたはクラスを作成することになります。そうしないと、コンテナはオブジェクトのインスタンス化方法を認識できません。ただし、多くの場合、ファクトリメソッドはCreate
メソッド自体のいずれかになります。
イニシャライザで例外をスローした場合、[[[MyObj alloc] init] autorelease]
パターンを使用しているコードがあるとリークが発生することに注意してください。例外は自動解放をスキップするためです。
この質問をご覧ください:
UIコントロール(ASPX、WinForms、WPFなど)を作成している場合、デザイナー(Visual Studio)はコントロールの作成時に例外を処理できないため、コンストラクターで例外をスローしないようにする必要があります。制御ライフサイクル(制御イベント)を把握し、可能な限り遅延初期化を使用します。
コンストラクターでオブジェクトを初期化できない場合、例外をスローします。1つの例は不正な引数です。
一般的な経験則として、例外は常にできるだけ早くスローする必要があります。これは、問題の原因が何かが間違っていることを知らせるメソッドに近い場合にデバッグを容易にするためです。
有効なオブジェクトを作成できない場合は、コンストラクターから例外を必ずスローする必要があります。これにより、クラスで適切な不変式を提供できます。
実際には、非常に注意する必要があります。 C++では、デストラクターは呼び出されないため、リソースを割り当てた後にスローする場合は、適切に処理するように細心の注意を払う必要があります。
このページ には、C++の状況に関する詳細な説明があります。
Objective-Cのベストプラクティスに対処することはできませんが、C++では、コンストラクターが例外をスローしても問題ありません。特に、isOK()メソッドを呼び出さずに、構築時に発生した例外条件を確実に報告する方法は他にないためです。
関数tryブロック機能は、コンストラクターのメンバーごとの初期化の失敗をサポートするために特別に設計されました(ただし、通常の関数にも使用できます)。スローされる例外情報を変更または強化する唯一の方法です。しかし、元の設計目的(コンストラクターでの使用)のため、空のcatch()句で例外を飲み込むことはできません。
回答が完全に言語に依存しないかどうかはわかりません。一部の言語は、例外とメモリ管理を異なる方法で処理します。
開発者は例外の処理が不十分な言語に焼き付いていたため、例外は決して使用せず、イニシャライザーのエラーコードのみを必要とするコーディング標準の下で作業しました。ガベージコレクションのない言語では、ヒープの処理方法とスタック方法が非常に異なります。これは、RAII以外のオブジェクトでは問題になる場合があります。ただし、コンストラクターの後にイニシャライザーを呼び出す必要があるかどうかをデフォルトで知るために、チームが一貫性を保つことを決定することが重要です。すべてのメソッド(コンストラクターを含む)も、スローできる例外について十分に文書化する必要があります。そのため、呼び出し元はそれらを処理する方法を知っています。
オブジェクトを初期化するのを忘れやすいので、私は一般に単一段階の構築を好みますが、それには多くの例外があります。
new
とdelete
を使用する必要がある設計上の理由がありますはい、コンストラクターが内部パーツの1つを構築できなかった場合、 明示的に特定の言語で)---(明示的な例外 をスローする責任がある場合があります。 。
これは唯一のオプションではありません:コンストラクタを終了してオブジェクトを構築できますが、インコヒーレントな状態を通知できるようにするために、メソッド 'isCoherent()'がfalseを返します(場合によっては、例外による実行ワークフローの残忍な中断を避けるため)
警告:EricSchaeferのコメントで述べたように、単体テストに多少の複雑さをもたらす可能性があります(スローは、トリガーする条件のために、関数の 循環的複雑度 を増加させる可能性があります)
呼び出し元が原因で失敗した場合(呼び出し元が提供するnull引数のように、呼び出されたコンストラクタがnull以外の引数を必要とする場合)、コンストラクタはチェックされていないランタイム例外をスローします。
構築中に例外をスローすることは、コードをより複雑にする素晴らしい方法です。単純に見えるものが突然難しくなります。たとえば、スタックがあるとします。スタックをポップしてトップ値を返す方法は?さて、スタック内のオブジェクトがコンストラクターをスローできる場合(呼び出し側に戻るために一時的なものを構築する)、データを失わないことを保証することはできません(スタックポインターを減らす、値のコピーコンストラクターを使用して戻り値を構築する)スタック、スローし、アイテムを失ったばかりのスタックがあります)!これが、std :: stack :: popが値を返さない理由であり、std :: stack :: topを呼び出す必要があります。
この問題はよく説明されています here 、項目10をチェックして、例外セーフコードを記述します。
OOの通常の規約は、オブジェクトメソッドが実際に機能するということです。
そのため、ゾンビオブジェクトを決して返さないようにするには、コンストラクタ/ initを使用します。
ゾンビは機能せず、内部コンポーネントが欠落している可能性があります。発生を待機しているヌルポインター例外のみ。
私は何年も前にObjective Cで初めてゾンビを作りました。
すべての経験則のように、「例外」があります。
特定のインターフェイスが、例外をスローすることが許可されているメソッド「initialize」が存在することを示すコントラクトを持つ可能性は完全にあります。このインターフェイスを実装しているオブジェクトは、初期化が呼び出されるまで、プロパティセッター以外の呼び出しに正しく応答しない可能性があります。ブートプロセス中にOOオペレーティングシステムのデバイスドライバーにこれを使用しましたが、実行可能でした。
一般に、ゾンビオブジェクトは必要ありません。 Smalltalkのような言語でbecomeを使うと物事は少しファジーになりますが、becomeを使いすぎるとスタイルが悪くなります。 オブジェクトを別のオブジェクトにその場で変更できるようになるため、エンベロープラッパー(Advanced C++)または戦略パターン(GOF)は必要ありません。
OPの質問には「言語に依存しない」タグがあります...この質問は、すべての言語/状況で同じ方法で安全に答えることはできません。
次のC#の例のクラス階層はクラスBのコンストラクターをスローし、メインのusing
の終了時にクラスAのIDisposeable.Dispose
への即時呼び出しをスキップし、クラスAのリソースの明示的な破棄をスキップします。
たとえば、クラスAが構築時にSocket
を作成し、ネットワークリソースに接続している場合、using
ブロック(比較的隠された異常)の後も同様です。
class A : IDisposable
{
public A()
{
Console.WriteLine("Initialize A's resources.");
}
public void Dispose()
{
Console.WriteLine("Dispose A's resources.");
}
}
class B : A, IDisposable
{
public B()
{
Console.WriteLine("Initialize B's resources.");
throw new Exception("B construction failure: B can cleanup anything before throwing so this is not a worry.");
}
public new void Dispose()
{
Console.WriteLine("Dispose B's resources.");
base.Dispose();
}
}
class C : B, IDisposable
{
public C()
{
Console.WriteLine("Initialize C's resources. Not called because B throws during construction. C's resources not a worry.");
}
public new void Dispose()
{
Console.WriteLine("Dispose C's resources.");
base.Dispose();
}
}
class Program
{
static void Main(string[] args)
{
try
{
using (C c = new C())
{
}
}
catch
{
}
// Resource's allocated by c's "A" not explicitly disposed.
}
}
私はObjective Cを学んでいるだけなので、実際には経験から話すことはできませんが、これについてはAppleのドキュメントで読みました。
それはあなたが尋ねた質問をどのように扱うかを教えてくれるだけでなく、それを説明するのにも良い仕事をします。
例外について私が見た最善のアドバイスは、代替が事後条件の不履行または不変式の維持の失敗である場合にのみ、例外をスローすることです。
このアドバイスは、不明確な主観的決定(良いアイデア)を、既に行っているべき設計決定(不変条件と事後条件)に基づく技術的で正確な質問に置き換えます。
コンストラクタは、そのアドバイスの特定のケースですが、特別なケースではありません。質問は、クラスにどのような不変式が必要なのでしょうか?構築後に呼び出される別の初期化メソッドの支持者は、クラスに2つ以上のオペレーティングモードがあり、構築後のnreadyモードと少なくとも1つの- 準備完了モード、初期化後に入力。これは追加の複雑さですが、クラスに複数の動作モードがある場合は許容されます。それ以外の場合、クラスに動作モードがない場合、その複雑さの価値を理解するのは困難です。
セットアップを別の初期化メソッドにプッシュしても、例外がスローされることを回避できないことに注意してください。コンストラクターがスローした可能性のある例外は、初期化メソッドによってスローされるようになりました。クラスのすべての便利なメソッドは、初期化されていないオブジェクトに対して呼び出された場合、例外をスローする必要があります。
また、コンストラクターによって例外がスローされる可能性を回避するのは面倒であり、多くの場合、多くの標準ライブラリではimpossibleであることに注意してください。これは、これらのライブラリの設計者が、コンストラクターから例外をスローすることをお勧めしているためです。特に、共有不可または有限のリソースを取得しようとする操作(メモリの割り当てなど)は失敗する可能性があり、その失敗は通常OO言語とライブラリで例外をスローすることで示されます。
Javaの観点から厳密に言えば、不正な値でコンストラクターを初期化するときはいつでも例外をスローする必要があります。そのように、悪い状態で構築されません。
私にとって、それはいくぶん哲学的なデザインの決定です。
存在する限り有効なインスタンスが存在するのは、俳優の時から非常にいいことです。多くの自明ではないケースでは、メモリ/リソースの割り当てができない場合、これはctorから例外をスローする必要があります。
他のいくつかのアプローチは、独自の問題を伴うinit()メソッドです。その1つは、init()が実際に呼び出されるようにすることです。
バリアントは、アクセサ/ミューテータが初めて呼び出されたときに自動的にinit()を呼び出すためにレイジーアプローチを使用しますが、そのためには、潜在的な呼び出し元がオブジェクトの有効性を心配する必要があります。 (「存在するため、有効な哲学」ではありません)。
この問題に対処するためのさまざまな提案された設計パターンを見てきました。 ctorを使用して初期オブジェクトを作成できるが、アクセサー/ミューテーターを含む初期化されたオブジェクトに手を入れるためにinit()を呼び出す必要があるなど。
それぞれのアプローチには長所と短所があります。これらのすべてを正常に使用しました。作成された瞬間からすぐに使用できるオブジェクトを作成しない場合は、大量のアサートまたは例外を使用して、init()の前にユーザーが対話しないようにすることをお勧めします。
補遺
C++プログラマーの観点から書きました。また、例外がスローされたときに解放されるリソースを処理するためにRAIIイディオムを適切に使用していると思います。
すべてのオブジェクトの作成にファクトリまたはファクトリメソッドを使用すると、コンストラクターから例外をスローすることなく、無効なオブジェクトを回避できます。作成メソッドは、要求されたオブジェクトを作成できる場合はそれを返し、作成できない場合はnullを返します。 nullを返してもオブジェクトの作成で何がうまくいかなかったかはわからないため、クラスのユーザーの構築エラーを処理する際の柔軟性が少し失われます。ただし、オブジェクトを要求するたびに複数の例外ハンドラーの複雑さを追加することや、処理すべきではない例外をキャッチするリスクも回避できます。