27

in-codeとの両方を初期化できるようにしたいカスタムビューがありますnib

initWithFrameとメソッドの両方を書く正しい方法は何initWithCoderですか?これらは両方とも、初期化に使用されるコードのブロックを共有します。

4

3 に答える 3

62

その場合の正しい方法は、との両方-initWithFrame:に共通のコードを含む別のメソッドを作成してから、との-initWithCoder:両方からそのメソッドを呼び出すことです。-initWithFrame:-initWithCoder:

- (void)commonInit
{
    // do any initialization that's common to both -initWithFrame:
    // and -initWithCoder: in this method
}

- (id)initWithFrame:(CGRect)aRect
{
    if ((self = [super initWithFrame:aRect])) {
        [self commonInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder*)coder
{
    if ((self = [super initWithCoder:coder])) {
        [self commonInit];
    }
    return self;
}

Justinの回答で概説されている懸念事項、特にサブクラスがオーバーライドしてはならない -commonInitことに注意してください。ここでは、その説明的な価値のためにその名前を使用しましたが、クラスとより密接に関連し、誤ってオーバーライドされる可能性が低い名前が必要になる可能性があります。それ自体がサブクラス化される可能性が低い専用のUIViewサブクラスを作成する場合は、上記のような一般的な初期化メソッドを使用することはまったく問題ありません。他の人が使用するフレームワークを作成している場合、または問題を理解していないが可能な限り安全なことを実行したい場合は、代わりに静的関数を使用してください。

于 2011-08-29T05:37:48.010 に答える
8

解決策は、最初に表示されるほど単純ではありません。初期化にはいくつかの危険があります-さらに下の危険があります。これらの理由から、私は通常、objcプログラムで次の2つのアプローチのいずれかを採用しています。

些細なケースでは、複製は悪い戦略ではありません。

- (id)initOne
{
    self = [super init];
    if (nil != self) { monIntIvar = SomeDefaultValue; }
    return self;
}

- (id)initTwo
{
    self = [super init];
    if (nil != self) { monIntIvar = SomeDefaultValue; }
    return self;
}

重要なケースについては、一般的な形式をとる静的初期化関数をお勧めします。

// MONView.h

@interface MONView : UIView
{
    MONIvar * ivar;
}

@end

// MONView.m

static inline bool InitMONView(MONIvar** ivar) {
    *ivar = [MONIvar new];
    return nil != *ivar;
}

@implementation MONView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (nil != self) {
        if (!InitMONView(&ivar)) {
            [self release];
            return nil;
        }
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];
    if (nil != self) {
        if (!InitMONView(&ivar)) {
            [self release];
            return nil;
        }
    }
    return self;
}

// …

@end

目的-C++:

objc ++を使用している場合は、c ++ ivarに適切なデフォルトのコンストラクターを実装し、初期化とdeallocスキャフォールディングの大部分を省略できます(コンパイラーフラグを正しく有効にしている場合)。

アップデート:

これがオブジェクトを初期化する安全な方法である理由を説明し、他の回答の一般的な実装が危険である理由の概要を説明します。

初期化中にインスタンスメソッドを呼び出す一般的な初期化子の一般的な問題は、継承グラフを悪用し、通常は複雑さとバグをもたらすことです。

推奨事項:部分的に構築されたオブジェクト(初期化やdeallocなど)でオーバーライドされたインスタンスメソッドを呼び出すことは安全ではなく、回避する必要があります。アクセサーは特に悪いです。他の言語では、これはプログラマーのエラーです(UBなど)。この件に関するobjcドキュメントを確認してください(「初期化子の実装」を参照)。これは必須だと思いますが、インスタンスメソッドとアクセサーは「通常は機能する」ため、部分的に構築された状態の方が優れていると主張する人々はまだ知っています。

要件:継承グラフを尊重します。ベースから初期化します。トップダウンで破壊します。いつも。

推奨事項:すべての初期化の一貫性を維持してください。ベースがinitから何かを返す場合は、すべてが順調であると想定する必要があります。クライアントとサブクラスが実装するための壊れやすい初期化ダンスを導入しないでください(バグとして戻ってくる可能性があります)。有効なインスタンスを保持しているかどうかを知る必要があります。また、サブクラッサーは、指定された初期化子からオブジェクトを返すときに、ベースが適切に初期化されていると(正しく)想定します。基本クラスのivarをプライベートにすることで、この可能性を減らすことができます。initから戻ると、クライアント/サブクラスは、派生元のオブジェクトが使用可能であり、適切に初期化されていると見なします。クラスグラフが大きくなるにつれて、状況は非常に複雑になり、バグが忍び寄り始めます。

推奨事項: initのエラーを確認してください。また、エラー処理と検出の一貫性を保ちます。nilを返すことは、初期化中にエラーが発生したかどうかを判断するための明白な規則です。早期に検出してください。

わかりましたが、共有インスタンスメソッドはどうですか?

別の投稿から借用および変更された例:

@implementation MONDragon

- (void)commonInit
{
    ivar = [MONIvar new];
}

- (id)initWithFrame:(CGRect)aRect
{
        if ((self = [super initWithFrame:aRect])) {
                [self commonInit];
        }
        return self;
}

- (id)initWithCoder:(NSCoder*)coder
{
        if ((self = [super initWithCoder:coder])) {
                [self commonInit];
        }
        return self;
}

// …

(ところで、その例ではエラー処理はありません)

カレブ:上記のコードで私が目にする最大の「危険」は、誰かが問題のクラスのサブクラスを作成し、-commonInitをオーバーライドし、オブジェクトを2回初期化する可能性があることです。

具体的には、サブクラス-[MONDragon commonInit]が2回呼び出され(2回作成されるため、リソースがリークします)ベースの初期化子とエラー処理は実行されません。

カレブ:それが本当のリスクなら…</ p>

どちらの効果も、信頼性の低いプログラムに相当する可能性があります。この問題は、従来の初期化を使用することで簡単に回避できます。

カレブ:…これに対処する最も簡単な方法は、-commonInitを非公開にするか、オーバーライドしないものとして文書化することです。

ランタイムはメッセージング時に可視性を区別しないため、どのサブクラスも同じプライベート初期化メソッドを簡単に宣言できるため、このアプローチは危険です(以下を参照)。

メソッドをオーバーライドしてはならないものとして文書化すると、サブクラッサーの負担が明らかになり、他のアプローチを使用することで簡単に回避できる複雑さと問題が発生します。また、コンパイラがフラグを立てないため、エラーが発生しやすくなります。

インスタンスメソッドの使用を主張する場合、ほとんどの場合、エラーを大幅に減らすことができるなど、予約した規則を使用します-[MONDragon constructMONDragon]-[MONKomodo constructMONKomodo]イニシャライザはクラス実装のTUにのみ表示される可能性が高いため、コンパイラは潜在的な間違いのいくつかにフラグを立てることができます。

補足:次のような一般的なオブジェクトコンストラクタ:

- (void)commonInit
{
    [super commonInit];
    // init this instance here
}

(私も見ましたが)初期化を制限し、コンテキスト(パラメーターなど)を削除し、指定された初期化子との間でクラス間で初期化コードを混合することになります-commonInit

そのすべてを通して、一般的な誤解やばかげた間違い/見落としから上記のすべての問題をデバッグするのに多くの時間が無駄になりました。クラスの一般的な初期化を実装する必要がある場合、静的関数が最も理解し、維持するのが最も簡単であると結論付けました。クラスは、クレントを危険から隔離する必要があります。これは、「インスタンスメソッドを介した一般的な初期化子」で繰り返し失敗する問題です。

これは、指定されたメソッドに基づくOPのオプションではありませんが、一般的な注意として、通常、便利なコンストラクターを使用して、一般的な初期化をより簡単に統合できます。これは、クラスクラスター、特殊化を返す可能性のあるクラス、および複数の内部初期化子から選択することを選択する可能性のある実装を処理する際の複雑さを最小限に抑えるのに特に役立ちます。

于 2011-08-29T05:42:07.080 に答える
3

コードを共有している場合は、3番目の初期化メソッドを呼び出してもらいます。

たとえば、initWithFrame次のようになります。

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        [self doMyInitStuff];
    }

    return self;
}

(iOSではなく)OS Xを使用している場合、フレームはNSRectの代わりになりCGRectます。

エラーチェックを行う必要がある場合は、初期化メソッドに次のようなエラーステータスを返すようにします。

- (id)initWithFrame:(CGRect)frame {
    if ((self = [super initWithFrame:frame])) {
        if (![self doMyInitStuff]) {
            [self release];
            self = nil;
        }
    }

    return self;
}

doMyInitStuffこれは、メソッドがエラー時に戻ることを前提としてNOいます。

また、まだご覧になっていない場合は、初期化に関するドキュメントが少しあります(ただし、この質問には直接対応していません)。

Cocoaのコーディングガイドライン:フレームワーク開発者向けのヒントとテクニック

于 2011-08-29T05:40:57.887 に答える