在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,我可以即时设置属性 ,那么我只需将其设置为当前日期即可。
亚当·卡特

Answers:


71

更新:Swift 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类引入的所有属性都具有有效的初始值。如此一来,Product的失败初始化程序如果在为初始化程序内的name属性分配特定值之前,将其传递给空字符串,则会在初始化程序启动时触发初始化失败。

但是,在您的情况下,简单地定义userName为a String!并不能解决编译错误,因为您仍然需要担心初始化基类上的属性NSObject。幸运的是,使用userName定义为String!,您实际上可以super.init()先调用,return nil这将初始化您的NSObject基类并修复编译错误。

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
非常感谢,不仅对,而且也
对它进行了

9
在swift1.2中,文档示例产生错误“必须从类初始化器返回nil之前初始化类实例的所有存储属性”
jeffrey 2015年

2
@jeffrey是的,文档中的示例(Product类)在分配特定值之前不会触发初始化失败,即使文档说可以。该文档与最新的Swift版本不同步。这是建议,使之成为var目前替代let资料来源:克里斯·拉特纳(Chris Lattner)
Arjan 2015年

1
文档中的这段代码有些不同:您首先设置属性,然后检查它是否存在。请参见“类的失败的初始化程序”,“ Swift编程语言”。```class Product {让名字:字符串!init?(name:String){self.name =名称,如果name.isEmpty {return nil}}}```
Misha Karpenko 2015年

我在Apple文档中也阅读过此书,但我看不到为什么需要这样做。失败意味着无论如何都返回nil,那么属性是否已初始化又有什么关系呢?
Alper

132

那没有道理。当我计划返回nil时,为什么要初始化那些属性?

根据Chris Lattner的说法,这是一个错误。他说的是:

这是发行说明中记录的swift 1.1编译器中的实现限制。编译器当前无法在所有情况下销毁部分初始化的类,因此它不允许形成必须这样做的情况。我们认为此错误是在将来的版本中修复的,而不是功能。

资源

编辑:

因此,swift现在是开源的,根据此变更日志,现在已在swift 2.2的快照中对其进行了修复。

现在,在对象已完全初始化之前,声明为可失败或抛出的指定类初始化器可能分别返回nil或抛出错误。


2
感谢我的观点,初始化不再需要的属性的想法似乎不是很合理。+1表示共享资源,这证明克里斯·拉特纳(Chris Lattner)感觉就像我一样;)。
Kai Huppmann

22
仅供参考:“的确如此。这仍然是我们希望改进的地方,但并没有为Swift 1.2带来好处。” -克里斯·拉特纳(Chris Lattner)2015
dreamlab

14
仅供参考:在Swift 2.0 beta 2中,这仍然是一个问题,并且引发初始化程序也是一个问题。
aranasaurus

7

我接受Mike S的回答是Apple的推荐,但我认为这不是最佳实践。强类型系统的重点是将运行时错误转移到编译时。这种“解决方案”无法达到目的。恕我直言,最好是继续将用户名初始化为"",然后在super.init()之后检查它。如果允许使用空白的用户名,则设置一个标志。

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
        }
    }
}

谢谢,但是我看不到Mike的回答如何破坏强类型系统的思想。总而言之,您提供了相同的解决方案,不同之处在于初始值设置为“”而不是nil。此外,您的代码无需使用“”作为用户名(这在学术上可能看起来不错,但至少与未在json / dictionary中设置不同)
Kai Huppmann 2014年

2
经过审查,我发现您是对的,但这仅是因为userName是一个常量。如果它是一个变量,那么可接受的答案将比我的差,因为以后可以将userName设置为nil。
Daniel T.

我喜欢这个答案。@KaiHuppmann,如果要允许空用户名,则也可以只使用一个简单的Bool needsReturnNil。如果字典中不存在该值,则将NeedsReturnNil设置为true并将userName设置为任何值。在super.init()之后,检查needsReturnNil并在必要时返回nil。
理查德·维纳布尔

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 2015年


1

我发现这可以在Swift 1.2中完成

有一些条件:

  • 必需的属性应声明为隐式展开的可选
  • 只需一次为您的必需属性分配一个值。该值可以为零。
  • 如果您的类是从另一个类继承的,则调用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

您可以使用便捷初始化

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)
    } 
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.