在 Swift 中实现可失败的初始化程序的最佳实践

使用以下代码,我试图定义一个简单的模型类及其可失败的初始化器,该初始化器以(json -) dictionary 作为参数。如果原始 json 中没有定义用户名,初始化器应该返回 nil

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()
}
}
21526 次浏览

Update: From the Swift 2.2 Change Log (released March 21, 2016):

Designated class initializers declared as failable or throwing may now return nil or throw an error, respectively, before the object has been fully initialized.


For Swift 2.1 and earlier:

According to Apple's documentation (and your compiler error), a class must initialize all its stored properties before returning nil from a failable initializer:

For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place.

Note: It actually works fine for structures and enumerations, just not classes.

The suggested way to handle stored properties that can't be initialized before the initializer fails is to declare them as implicitly unwrapped optionals.

Example from the docs:

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

In the example above, the name property of the Product class is defined as having an implicitly unwrapped optional string type (String!). Because it is of an optional type, this means that the name property has a default value of nil before it is assigned a specific value during initialization. This default value of nil in turn means that all of the properties introduced by the Product class have a valid initial value. As a result, the failable initializer for Product can trigger an initialization failure at the start of the initializer if it is passed an empty string, before assigning a specific value to the name property within the initializer.

In your case, however, simply defining userName as a String! does not fix the compile error because you still need to worry about initializing the properties on your base class, NSObject. Luckily, with userName defined as a String!, you can actually call super.init() before you return nil which will init your NSObject base class and fix the compile error.

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

I accept that Mike S's answer is Apple's recommendation, but I don't think it's best practice. The whole point of a strong type system is to move runtime errors to compile time. This "solution" defeats that purpose. IMHO, better would be to go ahead and initialize the username to "" and then check it after the super.init(). If blank userNames are allowed, then set a flag.

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

That doesn't make sense. Why should I initialize those properties when I plan to return nil?

According to Chris Lattner this is a bug. Here is what he says:

This is an implementation limitation in the swift 1.1 compiler, documented in the release notes. The compiler is currently unable to destroy partially initialized classes in all cases, so it disallows formation of a situation where it would have to. We consider this a bug to be fixed in future releases, not a feature.

Source

EDIT:

So swift is now open source and according to this changelog it is fixed now in snapshots of swift 2.2

Designated class initializers declared as failable or throwing may now return nil or throw an error, respectively, before the object has been fully initialized.

A failable initializer for a value type (that is, a structure or enumeration) can trigger an initialization failure at any point within its initializer implementation

For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place.

Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/sg/jEUH0.l

Another way to circumvent the limitation is to work with a class-functions to do the initialisation. You might even want to move that function to an extension:

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

Using it would become:

if let user = User.fromDictionary(someDict) {


// Party hard
}

I found out this can be done in Swift 1.2

There are some conditions:

  • Required properties should be declared as implicitly unwrapped optionals
  • Assign a value to your required properties exactly once. This value may be nil.
  • Then call super.init() if your class is inheriting from another class.
  • After all your required properties have been assigned a value, check if their value is as expected. If not, return nil.

Example:

class ClassName: NSObject {


let property: String!


init?(propertyValue: String?) {


self.property = propertyValue


super.init()


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

You can use convenience 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)
}
}

Although Swift 2.2 has been released and you no longer have to fully initialize the object before failing the initializer, you need to hold your horses until https://bugs.swift.org/browse/SR-704 is fixed.