Swiftで失敗可能な初期化子を実装するためのベストプラクティス


100

次のコードを使用して、単純なモデルクラスを定義しようとしています。これは、(json-)辞書をパラメーターとして使用する、失敗可能な初期化子です。初期化子はnil、ユーザー名が元のjsonで定義されていない場合に返されます。

1.コードがコンパイルされないのはなぜですか?エラーメッセージは言う:

イニシャライザからnilを返す前に、クラスインスタンスのすべての格納されたプロパティを初期化する必要があります。

それは意味がありません。戻る予定があるのに、なぜこれらのプロパティを初期化する必要があるのnilですか?

2.私のアプローチは適切ですか、それとも私の目標を達成するための他のアイデアや一般的なパターンはありますか?

class User: NSObject {

    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        if let value: String = dictionary["user_name"] as? String {
            userName = value
        }
        else {
           return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()
    }
}

私は同様の問題を抱えていましたが、私は各辞書の値が期待されるはずであると結論付けたので、値を強制的にアンラップしました。プロパティが存在しない場合、バグをキャッチすることができます。さらに、canSetCalculablePropertiesブール値パラメーターを追加して、初期化者がその場で作成できるプロパティと作成できないプロパティを計算できるようにしました。たとえば、dateCreatedキーが見つからず、canSetCalculablePropertiesパラメーターがtrueであるため、その場でプロパティを設定できる 場合は、現在の日付に設定します。
Adam Carter

回答:


71

更新:からスウィフト2.2変更ログ(2016年3月21日リリース):

失敗可能またはスローとして宣言された指定クラス初期化子は、オブジェクトが完全に初期化される前に、それぞれnilまたはエラーをスローするようになりました。


Swift 2.1以前の場合:

Appleのドキュメント(およびコンパイラエラー)によると、クラスは返される前に、格納されているすべてのプロパティを初期化する必要があります nil、失敗した初期化子からます。

ただし、クラスの場合、失敗可能な初期化子は、そのクラスによって導入されたすべての格納されたプロパティが初期値に設定され、初期化子の委任が行われた後にのみ初期化の失敗をトリガーできます。

注意:実際には、クラスだけでなく、構造体および列挙体でも正常に機能します。

イニシャライザが失敗する前に初期化できない格納されたプロパティを処理するための推奨される方法は、暗黙的にアンラップされたオプションとしてそれらを宣言することです。

ドキュメントの例:

class Product {
    let name: String!
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

上記の例では、Productクラスのnameプロパティは、暗黙的にアンラップされたオプションの文字列型(String!)として定義されています。これはオプションのタイプであるため、初期化中に特定の値が割り当てられる前に、nameプロパティのデフォルト値はnilになります。このデフォルト値のnilは、Productクラスによって導入されたすべてのプロパティに有効な初期値があることを意味します。その結果、初期化子内のnameプロパティに特定の値を割り当てる前に、空の文字列が渡された場合、Productの失敗可能な初期化子は初期化子の開始時に初期化エラーをトリガーできます。

ただし、基本クラスのプロパティを初期化することについてはまだ心配する必要があるため、単にuserNameとして定義してString!もコンパイルエラーは修正されませんNSObject。幸い、userNameとして定義されているためString!、実際に呼び出すsuper.init()前にreturn nilNSObject基本クラスを初期化し、コンパイルエラーを修正できます。

class User: NSObject {

    let userName: String!
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        super.init()

        if let value = dictionary["user_name"] as? String {
            self.userName = value
        }
        else {
            return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            self.isSuperUser = value
        }

        self.someDetails = dictionary["some_details"] as? Array
    }
}

1
右だけでなく、どうもありがとうございました、だけでなく、うまく説明
甲斐Huppmann

9
swift1.2では、ドキュメントの例でエラーが発生します「クラスインスタンスのすべての保存されたプロパティは、初期化子からnilを返す前に初期化する必要があります」
jeffrey

2
@jeffrey正解です。ドキュメント(Productクラス)の例では、特定の値を割り当てる前に初期化の失敗をトリガーできません。ドキュメントは最新のSwiftバージョンと同期していません。var代わりに今のところにすることをお勧めしletます。出典:Chris Lattner
Arjan、2015

1
ドキュメントには、このコードが少し異なります。最初にプロパティを設定し、それが存在するかどうかを確認します。「クラスの失敗可能な初期化子」、「Swiftプログラミング言語」を参照してください。`` `クラスProduct {let name:String!init?(name:String){self.name = name if name.isEmpty {return nil}} `` `
Misha Karpenko

私もこれをAppleのドキュメントで読みましたが、なぜこれが必要になるのかわかりません。失敗はとにかくnilを返すことを意味しますが、プロパティが初期化されているかどうかはどうですか?
アルパー、2016

132

それは意味がありません。nilを返す予定があるときに、これらのプロパティを初期化する必要があるのはなぜですか?

クリス・ラットナーによると、これはバグです。これが彼の言うことです:

これは、リリースノートに記載されているSwift 1.1コンパイラの実装制限です。コンパイラーは現在、すべてのケースで部分的に初期化されたクラスを破棄することができないため、必要な状況の形成を許可していません。これは機能ではなく、今後のリリースで修正されるバグと考えています。

ソース

編集:

したがって、swiftはオープンソースになり、この変更ログによると、swift 2.2のスナップショットで修正されています

失敗可能またはスローとして宣言された指定クラス初期化子は、オブジェクトが完全に初期化される前に、それぞれnilまたはエラーをスローするようになりました。


2
不要になったプロパティを初期化するという考えはあまり合理的ではないようだという私の指摘を述べてくれてありがとう。そして、ソースを共有するための+1は、Chris Lattnerが私と同じように感じていることを証明しています;)。
Kai Huppmann、2014年

22
参考:「確かに、これはまだ改善したい点ですが、Swift 1.2の削減にはなりませんでした。」-クリス・
ラットナー

14
参考:Swift 2.0ベータ2では、これはまだ問題であり、スローするイニシャライザの問題でもあります。
アラナサウルス2015年

7

私はマイクSの答えがAppleの推奨であることを受け入れますが、それがベストプラクティスだとは思いません。強力な型システムの要点は、実行時エラーをコンパイル時間に移動することです。この「解決策」は、その目的に反しています。IMHO、先に進んでユーザー名をに初期化し""、super.init()の後でそれを確認することをお勧めします。空のuserNamesが許可されている場合は、フラグを設定します。

class User: NSObject {
    let userName: String = ""
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: [String: AnyObject]) {
        if let user_name = dictionary["user_name"] as? String {
            userName = user_name
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()

        if userName.isEmpty {
            return nil
        }
    }
}

ありがとうございます。しかし、強力な型システムのアイデアがマイクの答えによってどのように損なわれるかはわかりません。全体として同じソリューションを提示しますが、初期値がnilではなく ""に設定されている点が異なります。さらに、ユーザー名として ""を使用するようにコードを
削除し

2
確認したところ、userNameが定数であることだけが理由です。変数の場合、userNameが後でnilに設定される可能性があるため、受け入れられた回答は私の回答よりも悪くなります。
ダニエルT.

私はこの答えが好きです。@KaiHuppmann、空のユーザー名を許可する場合は、単純なBool needsReturnNilを使用することもできます。値が辞書に存在しない場合は、needsReturnNilをtrueに設定し、userNameを何にでも設定します。super.init()の後、needsReturnNilを確認し、必要に応じてnilを返します。
Richard Venable

6

制限を回避する別の方法は、クラス関数を使用して初期化を行うことです。その関数を拡張機能に移動することもできます。

class User: NSObject {

    let username: String
    let isSuperUser: Bool
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {

         self.userName = userName
         self.isSuperUser = isSuperUser
         self.someDetails = someDetails

         super.init()
    }
}

extension User {

    class func fromDictionary(dictionary: NSDictionary) -> User? {

        if let username: String = dictionary["user_name"] as? String {

            let isSuperUser = (dictionary["super_user"] as? Bool) ?? false
            let someDetails = dictionary["some_details"] as? [String]

            return User(username: username, isSuperUser: isSuperUser, someDetails: someDetails)
        }

        return nil
    }
}

それを使用すると:

if let user = User.fromDictionary(someDict) {

     // Party hard
}

1
私はこれが好き; 私はコンストラクタが望むものに対して透明であることを好み、辞書で渡すことは非常に不透明です。
Ben Leggiero

3

Swift 2.2がリリースされ、イニシャライザが失敗する前にオブジェクトを完全に初期化する必要はなくなりましたが、https://bugs.swift.org/browse/SR-704が修正されるまで馬を保持する必要があります。


1

これ Swift 1.2で実行できることがわかりました

いくつかの条件があります。

  • 必須のプロパティは、暗黙的にアンラップされたオプションとして宣言する必要があります
  • 必要なプロパティに値を1回だけ割り当てます。この値はnilの場合があります。
  • 次に、クラスが別のクラスから継承している場合は、super.init()を呼び出します。
  • 後は、すべての必要なプロパティが期待通りの価値があるかどうかを確認、値が割り当てられています。そうでない場合は、nilを返します。

例:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}

0

値型(つまり、構造体または列挙型)の失敗可能な初期化子は、その初期化子実装内の任意の時点で初期化失敗をトリガーできます

ただし、クラスの場合、失敗可能な初期化子は、そのクラスによって導入されたすべての格納されたプロパティが初期値に設定され、初期化子の委任が行われた後にのみ初期化の失敗をトリガーできます。

抜粋:Apple Inc.“ Swiftプログラミング言語。” iBooks。https://itun.es/sg/jEUH0.l


0

あなたは便利なinitを使うことができます

class User: NSObject {
    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {
        self.userName = userName
        self.isSuperUser = isSuperUser
        self.someDetails = someDetails
    }     

    convenience init? (dict: NSDictionary) {            
       guard let userName = dictionary["user_name"] as? String else { return nil }
       guard let isSuperUser = dictionary["super_user"] as? Bool else { return nil }
       guard let someDetails = dictionary["some_details"] as? [String] else { return nil }

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