意外地发现nil意外 - struct init?不工作

时间:2017-12-08 10:07:24

标签: swift struct optional

我正在开发一个演示应用程序,用于返回带有google places API的餐馆列表。

  • 我有一个Restaurant类 - 1个属性是自定义类型RestaurantDetails。这就是挑战所在:

Restaurant.swift

class Restaurant {

    var id:String
    var placeId:String
    var name:String
    var location: Location //Location + address + coordinate + distance
    var phone:String?
    let details : RestaurantDetails


    init(id:String, placeId:String, name:String, location: Location, details: RestaurantDetails) {
        self.id = id
        self.placeId = placeId
        self.name = name
        self.location = location
        self.details = details
    }

    convenience init(dict:[String:Any]) {
        let id = dict["id"] as! String
        let placeId = dict["place_id"] as! String
        let name = dict["name"] as! String
        let address = dict["formatted_address"] as? String
        let location = Location(address: address!, json: dict["geometry"] as! [String : Any])

        if let price = dict["price_level"] as? Double {
            print("price => \(Price(valueDouble: price))")
        }

        if let rating = dict["rating"] as? Double {
            print("rating => \(Rating(valueDouble: rating))")
        }

        self.init(id: id, placeId: placeId, name: name, location: location!, details: RestaurantDetails(json: dict)!)
    }

}

unexpectedly found nil while unwrapping an optional 每次初始化details属性都会失败 即使使用断点也没有帮助调试错误,因为自定义init?似乎没有开火。我没有看到任何语法错误 如果值存在,我通常使用guard let控制流来解析JSON格式。不成功。 断点不会引发RestaurantDetails.swift文件

RestaurantDetails.swift

enum Price {
    case cheap, expensive, `default`

    init(valueDouble: Double) {
        switch valueDouble {
        case 0..<2: self = .cheap
        case 2..<4 : self = .expensive
        default: self = .default
        }
    }

    var dollarSymbol : String {
        switch self {
        case .cheap: return "$"
        case .expensive: return "$$"
        default: return ""
        }
    }
}

enum Rating {
    case low, fair, good, excellent, `default`

    init(valueDouble: Double) {
        switch valueDouble {
        case 0..<2: self = .low
        case 3..<4: self = .fair
        case 4..<5: self = .good
        case 6 : self = .excellent
        default: self = .default
        }
    }

    var starSymbol : String {
        switch self {
        case .low: return "⭐"
        case .fair: return "⭐⭐"
        case .good: return "⭐⭐⭐"
        case .excellent: return "⭐⭐⭐⭐"
        default: return ""
        }
    }
}

struct RestaurantDetails {
    let price:Price
    let rating:Rating // Rating (enum) (filter)
    let openNow: Bool //(filter)
    let types : [String]?
    let photos : [NSDictionary]?
}


extension RestaurantDetails {

    init?(json: [String: Any]) {

        guard let price = json["price_level"] as? Double,
            let rating = json["rating"] as? Double,
            let openingHours = json["opening_hours"] as? NSDictionary,
            let types = json["types"] as? [String],
            let photos = json["photos"] as? [NSDictionary] else {
            return nil
        }

        print("json details : \(json)")

        self.price = Price(valueDouble: price)
        self.rating = Rating(valueDouble: rating)
        self.openNow = openingHours["open_now"] as! Bool != nil ?? false
        self.types = types
        self.photos = photos
    }
}

我还有一个完美运行的Location.swift文件

struct Location {
    var address :String
    var coordinate:(lat:Double, lng:Double)
}

extension Location {
    init?(address: String, json:[String:Any]) {
        guard let latitude = json["location"] as? NSDictionary, let longitude = json["location"] as? NSDictionary else { return nil }

        self.init(address: address, coordinate: (lat: latitude["lat"] as! Double, lng: longitude["lng"] as! Double))
    }
}

1 个答案:

答案 0 :(得分:0)

As vadian described in his comment,导致崩溃的问题是location有一个可用的初始化程序,你强行解包它,并且由于初始化实际上是失败的,你现在已经强制解包了一个nil值。

更广泛的问题是:当某些或所有位置数据不可用时,您(开发人员)想要发生什么,以及最佳方法是什么?

处理不完整或缺失的位置数据的选项

根据您对问题的描述(“使用google places api返回餐馆列表的演示应用程序”),目前尚不清楚没有可用位置数据的餐馆是否应该输入您的模型。目前还不清楚是否有一些位置数据可以不用。所以我看到三个选项:

  1. 丢弃所有位置数据不完整的餐馆
  2. 放弃一些餐馆,其中包含不完整的位置数据,同时保留其他人
  3. 保留所有餐厅,无论位置数据如何
  4. 要实现这些选项,您可能需要使用一些常用工具来处理选项:

    Nil-coalescing operator

    a = b ?? c

    a设为b,除非b为零,在这种情况下,a等于c

    guardif

    中的可选绑定

    guard let a = b else { return }

    a设为等于b,除非b为零,在这种情况下从当前上下文返回。你可以在返回之前在else分支中工作。

    if let a = b { /* Do work using a */ } else { /* Do other work */ }

    如果b不是nil,请输入if分支,并将a设为b;如果b为nil,请输入else分支。

    怎么做

    丢弃所有位置数据不完整的餐馆

    在这种情况下,您可以为Restaurant制作一个可用的初始值设定项,由location保护为非零:

    /// Initialization fails if `location` is `nil`
    convenience init?(id:String, placeId:String, name:String, location: Location?, details: RestaurantDetails) {
        guard let location = location else { return nil }
    
        self.init(id: id, placeId: placeId, name: name, location: location, details: details)
    }
    

    丢弃一些餐馆,其中包含不完整的位置数据,同时保留其他人

    您可以从某些类型的缺失位置数据中恢复。例如,您可能确实需要街道地址字符串,而纬度和经度是不必要的。您可以重写Location结构以反映:

    struct Location {
        var address: String
        var coordinate: (lat: Double, lng: Double)?
    }
    
    extension Location {
        init?(address: String, json:[String:Any]) {
            let coordinate: (Double, Double)?
            if let latitude = json["location"] as? NSDictionary, let longitude = json["location"] as? NSDictionary {
                coordinate = (latitude, longitude)
            } else {
                coordinate = nil
            }
    
            self.init(address: address, coordinate: coordinate)
        }
    }
    

    替代方案,lat / long可能是重要的部分,但有时可能会使用其他API从街道地址恢复它们:

    struct Location {
        var address: String
        var coordinate: (lat: Double, lng: Double)
    }
    
    extension Location {
        init?(address: String, json:[String:Any]) {
            let coordinate: (Double, Double)?
            if let latitude = json["location"] as? NSDictionary, let longitude = json["location"] as? NSDictionary {
                coordinate = (latitude, longitude)
            } else {
    
                /* An API call that takes an address string and tries to return lat/long but might return nil */
    
                coordinate = returnedCoordinateFromAPI
            }
    
            guard let coordinate = coordinate else { return nil }
            self.init(address: address, coordinate: coordinate)
        }
    }
    

    保留所有餐厅,不论位置数据

    这是最简单的情况。如果location不是必需的,请将其设为可选:

    class Restaurant {
    
        var id: String
        var placeId: String
        var name: String
        var location: Location? //Location + address + coordinate + distance
        var phone: String?
        let details: RestaurantDetails
    
    
        init(id:String, placeId:String, name:String, location: Location?, details: RestaurantDetails) {
    
            self.id = id
            self.placeId = placeId
            self.name = name
            self.location = location
            self.details = details
        }
    
        convenience init?(dict:[String:Any]) {
            guard let id = dict["id"] as? String,
                let placeId = dict["place_id"] as? String,
                let name = dict["name"] as? String else { return nil }
    
            // Use the address from the dictionary unless it is nil, in which case substitute an empty string
            let address = dict["formatted_address"] as? String ?? ""
    
            let location = Location(address: address, json: dict["geometry"] as? [String : Any])
    
            if let price = dict["price_level"] as? Double {
                print("price => \(Price(valueDouble: price))")
            }
    
            if let rating = dict["rating"] as? Double {
                print("rating => \(Rating(valueDouble: rating))")
            }
    
            guard let details = RestaurantDetails(json: dict) else { return nil }
    
            self.init(id: id, placeId: placeId, name: name, location: location, details: details)
        }
    
    }