Swift:guard "let self = ..."的快捷方式?

4
我需要把json服务器响应解析成swift对象。我使用以下代码:
struct MyGPSCoords {

    var latitude:Double
    var longitude:Double
    var accuracy:Int
    var datetime:NSDate

    init?(infobrutFromJson_:[String:String]?)
    {
        guard let infobrut = infobrutFromJson_ else {
            // first time the user sign up to the app, php server returns "null" in Json 
            return nil
        }

        guard
        let lat:Double = Double(infobrut["latitude"] ?? "nil"),
        let lng = Double(infobrut["longitude"] ?? "nil"),
        let acc = Int(infobrut["accuracy"] ?? "nil"),
        let dtm = NSDate(timeIntervalSince1970: Double(infobrut["time"] ?? "nil"))
        else {
            print("warning : unable to parse data from server. Returning nil");
            return nil ; // position not NIL but format not expected => = nil
        }
        self.latitude = lat
        self.longitude = lng
        self.accuracy = acc
        self.datetime = dtm
    }


}

我希望能让“守卫”语句尽可能简短。首先,我添加了??“nil”,所以如果其中一个键不存在,Double("nil")会得到nil,并且守卫语句可以处理。对于NSDate,我编写了一个扩展,使用方便的init?返回nil,如果其输入为nil,那么我也可以采用同样的方法。

我的问题是,我是否可以通过在guard语句中直接为self.latitude赋值来使其更短?我试过这样做:

guard self.latitude = Double(infobrut["latitude"] ?? "nil"), ... 

它说无法将 Double? 转换为 Double。有没有办法让这个保护更短,并避免我分配 lat、lng、acc 和 dtm 缓冲变量?


@ Damon 是的,你可以这样做: `var a: String = "1"; var b = Int(a);print(a, type(of: b));-->// 1 Optional<Int>` - Jordan
你的属性应该使用 let,而不是 var - Paulw11
3
您可以使用Codable吗? - Paulw11
1
Codable很棒,但如果你的JSON使用字符串表示数字值,那么你也必须在模型中将它们声明为字符串。如果你有API的控制权,我建议先修复这个问题。 - EmilioPelaez
如果JSON确实是[String: String],那么可解码性将无济于事。实际的JSON长什么样子,它是如何转换为infobrutFromJson_字典的? - Sven
显示剩余2条评论
4个回答

5
首先,您当然应该尝试修复JSON,因为这个JSON格式不正确。在JSON中,字符串不是数字。假设您无法纠正此损坏的JSON,则您需要的工具是flatMap,它将T??转换为T?(这是guard-let所期望的)。
guard
    let lat = infobrut["latitude"].flatMap(Double.init),
    let lng = infobrut["longitude"].flatMap(Double.init),
    let acc = infobrut["accuracy"].flatMap(Int.init),
    let dtm = infobrut["time"].flatMap(TimeInterval.init).flatMap(Date.init(timeIntervalSince1970:))
    else {
        print("warning : unable to parse data from server. Returning nil")
        return nil // position not NIL but format not expected => = nil
}

我看到很多评论说 Codable 在这里行不通,但它绝对可以,并且这确实是你应该使用的。这里有一种方法(虽然错误信息有点混乱,但很简单):

struct MyGPSCoords: Decodable {

    var latitude:Double
    var longitude:Double
    var accuracy:Int
    var datetime:Date

    enum CodingKeys: String, CodingKey {
        case latitude, longitude, accuracy, datetime
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        guard
            let lat = Double(try container.decode(String.self, forKey: .latitude)),
            let lng = Double(try container.decode(String.self, forKey: .longitude)),
            let acc = Int(try container.decode(String.self, forKey: .accuracy)),
            let dtm = TimeInterval(try container.decode(String.self,
                                                        forKey: .datetime)).flatMap(Date.init(timeIntervalSince1970:))
        else {
            throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Could not decode"))
        }

        self.latitude = lat
        self.longitude = lng
        self.accuracy = acc
        self.datetime = dtm
    }

}

或者你可以使用一个内部的有用函数,通过 throws 的力量,摆脱所有临时变量和可选项,让代码更加精简。
struct MyGPSCoords: Decodable {

    var latitude:Double
    var longitude:Double
    var accuracy:Int
    var datetime:Date

    enum CodingKeys: String, CodingKey {
        case latitude, longitude, accuracy, datetime
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        func decodeBrokenJSON<T>(_ type: T.Type,
                                 forKey key: CodingKeys) throws -> T
            where T: Decodable & LosslessStringConvertible {
                return try T.init(container.decode(String.self, forKey: key)) ?? {
                    throw DecodingError.dataCorruptedError(forKey: key,
                                                           in: container,
                                                           debugDescription: "Could not decode \(key)")
                    }()
        }

        self.latitude = try decodeBrokenJSON(Double.self, forKey: .latitude)
        self.longitude = try decodeBrokenJSON(Double.self, forKey: .longitude)
        self.accuracy = try decodeBrokenJSON(Int.self, forKey: .accuracy)
        self.datetime = Date(timeIntervalSince1970: try decodeBrokenJSON(TimeInterval.self, forKey: .datetime))
    }

}

在我看来,这是 throws 能够大放异彩并且应该经常使用的好例子。


可以在 return nil 后面省略分号;,也可以省略你的 print - CodeBender
你不需要显式声明 CodingKeys 枚举,因为键名与属性名称相同。 - NRitH
@NRitH 我不认为那是正确的。你传递给 keyedBy: 的参数是什么?可以试一试吗? - Rob Napier
我的意思是,如果你的结构体实现了 Codable ,那么你不需要它。在我的答案中,我概述了这个过程。 - NRitH

3
其他解决方案似乎过于复杂。只需将其简化即可。
struct MyGPSCoords: Codable {

    var latitude: Double?
    var longitude: Double?
    var accuracy: Int?
    var datetime: Date?

    var isValid {
        return [latitude, longitude, accuracy, datetime].filter { $0 == nil }.isEmpty
    }
}

// jsonData is whatever payload you get back from the URL request.
let coords = JSONDecoder().decode(jsonData, type: MyGPSCoords.self)

if !coords.isValid {
    print("warning : unable to parse data from server.")
}

由于您所有的属性都是Optional,如果其中一个或多个属性缺失,则解析不会失败。相比于原始代码中的guard let...子句,isValid检查要简单得多。

编辑:如Rob Napier所建议的那样,如果所有JSON值都被编码为String,那么可以另一种方式来构造您的MyGPSCoords

struct MyGPSCoords: Codable {

    // These are the Codable properties
    fileprivate var latitudeString: String?
    fileprivate var longitudeString: String?
    fileprivate var accuracyString: String?
    fileprivate var datetimeString: String?

    // Default constant to use as a default check for validity
    let invalid = Double.leastNonzeroMagnitude

    // And these are the derived properties that you want users to use
    var latitude: Double {
        return Double(latitudeString ?? "\(invalid)") ?? invalid
    }

    var longitude: Double {
        return Double(longitudeString ?? "\(invalid)") ?? invalid
    }

    var accuracy: Int {
        return Int(accuracyString ?? "\(invalid)") ?? Int(invalid)
    }

    var date: Date {
        return <whatever-formatter-output-you-need>
    }

    var isValid {
        return [latitudeString, longitudeString, accuracyString, datetimeString].filter { $0 == nil }.isEmpty
               && latitude != invalid && longitude != invalid
               && accuracy != Int(invalid) /* && however you compare dates */
    }
}

这不会将JSON字符串解码为数字值。问题强烈暗示JSON的格式为{"latitude": "123.456"},这是错误的,但是这是一种常见的JSON编码方式。这也会将Optionals传播到MyGPSCoords的每个用户中,这是非常糟糕的设计。结构应该被验证一次,然后就不应该有optionals(就像原始代码中一样)。 - Rob Napier
我喜欢你实现isValid函数的方式。 - Muhammad Shauket

3
你想做的事情是不可能的。编译器已经告诉你了,尽管错误信息有些误导性。你可以使用guard let创建一个新变量,或者使用带有布尔表达式的guard。在你的情况下没有let,所以编译器会尝试解析布尔表达式。相反,它看到了赋值并产生了类型不匹配的错误消息。如果类型匹配(例如guard self.latitude = 12.0),错误消息将更清晰:error: use of '=' in a boolean context, did you mean '=='?

0

我知道这个问题很老,但我必须承认我并没有很好地理解Swift中内置的Decodable/Decoder系统(特别是“Container”这个概念,我无法确定它到底代表什么)。

不管怎样,我自己编写了一个解码器,可以以与Android解码JSONObject相同的方式处理此情况。我创建了一个类似于以下代码的Dictionary扩展:

protocol Decodable {
    init(from raw:[String:Any]) throws
}


extension Dictionary where Key == String
{

    enum DecodableError : Error {
        case unknownKey(key:String)
        case keyWrongType(key:String, expectedType:String, actualValue:String)
        case nullValueAtKey(key:String)
    }
    
    func getSafe<T>(_ key:String, forType t:T.Type) throws -> T
    {
        if(self[key] != nil)
        {
            if(self[key] is NSNull) // corresponds to the JSON null value (by experience)
            {
                throw DecodableError.nullValueAtKey(key:key)
            }
            else if(self[key] is T) // for raw type
            {
                return self[key] as! T
            }
            
            // try to parse self[key] to provided type if it's decodable
            else if(self[key] is [String:Any] && t is Decodable.Type)
            {
                return try (t as! Decodable.Type).init(from: self[key] as! [String:Any]) as! T
            }
            
            throw DecodableError.keyWrongType(key: key,
                    expectedType: String(describing: T.self),
                    actualValue: String(describing:self[key]!))
            
        }
            
        throw DecodableError.unknownKey(key:key)
        
    }
    
    func getSafeOpt<T>(_ key:String, forType t:T.Type) throws -> T?
    {
        if(self[key] != nil)
        {
            if(self[key] is NSNull)
            {
                return nil
            }
            return try getSafe(key, forType: t)
        
        }
            
        throw DecodableError.unknownKey(key:key)
    }
    
}

我这样使用它:
struct Position : Decodable {
    
    
    let latitude:Double
    let longitude:Double
    let accuracy:Int?
    let member:Member

    init(from raw:[String:Any]) throws
    {
    
        // getSafe throw exception whenever node are json<null> or if node doesn't exist 
        latitude = try raw.getSafe("lat", forType: Double.self)
        longitude = try raw.getSafe("lng", forType: Double.self)

        // getSafeOpt returns nil if the JSON value of node acc is null, 
        // but it still throw an exception if there is no "acc" node 
        accuracy = try raw.getSafeOpt("acc", forType: Int.self)

        // you can use it to decode other objects that implement my Decodable protocol too : 
        member = try raw.getSafeOpt("member", forType: Member.self)
    }
 } 
 
do {
   try app.position = Position(from: rawDic)
}
catch {
    print("Unable to parse Position : \(error) ")
    return
}

目前还没有处理JSON数组的功能,我稍后会添加,或者如果您希望添加JSON数组处理机制,请随意更新我的答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接