【问题标题】:Swift - Decode/encode an array of generics with different typesSwift - 解码/编码具有不同类型的泛型数组
【发布时间】:2020-04-09 09:50:36
【问题描述】:

如何解码/编码不同泛型类型的数组?

我有一个数据结构,它具有符合Connection 协议的属性,因此我使用泛型:

// Data structure which saves two objects, which conform to the Connection protocol
struct Configuration<F: Connection, T: Connection>: Codable {
    var from: F
    var to: T
    private var id: String = UUID.init().uuidString

    enum CodingKeys: String, CodingKey {
        case from, to, id
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.from = try container.decode(F.self, forKey: .from)
        self.to = try container.decode(T.self, forKey: .to)
        self.id = try container.decode(String.self, forKey: .id)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(from, forKey: .from)
        try container.encode(to, forKey: .to)
        try container.encode(id, forKey: .id)
    }
}

protocol Connection: Codable {
    var path: String { get set }
}


// Two implementations of the Connection protocol
struct SFTPConnection: Connection, Codable {
    var path: String
    var user: String
    var sshKey: String
}

struct FTPConnection: Connection, Codable {
    var path: String
    var user: String
    var password: String
}

当我知道FT 是什么类型的连接时,这很好用。但是我有一些情况,我想加载配置,不知道 FT 是哪种类型。

public static func load<F: Connection, T: Connection>(for key: String) throws -> Configuration<F, T>? {
    // Load from UserDefaults
    guard let configurationData = defaults.object(forKey: key) as? Data else {
        return nil
    }

    // Decode
    guard let configuration = try? PropertyListDecoder().decode(Configuration<F, T>.self, from: configurationData) else {
        return nil
    }

    return configuration
}

// OR

func loadAll<F:Connection, T: Connection>() -> [String: Configuration<F, T>]? {
    return UserDefaults.standard.dictionaryRepresentation() as? [String: Configuration<F, T>]
}

在上述情况下,FT 可以是符合 Connection 协议的任何未知类型。所以上面的函数是行不通的,因为我在调用函数时需要为FT指定一个特定的类型,我不知道。

在第二个函数中,F 本身实际上可以是不同的类型。这就是困难的地方。我想我需要以某种方式将FT 的类型存储在用户默认值中,然后在decodeencode 函数中使用它们(从而丢弃泛型)。但我不知道如何优雅地做到这一点。

我将不胜感激有关如何解决此问题的任何想法!

【问题讨论】:

  • 短篇小说:你必须知道类型。你有多少种不同的类型?您可以将类型字符串与数据一起保存(例如作为字典),并在 switch 语句中在检索数据时将字符串映射到具体类型。
  • @vadian 我已经添加了一个答案,这是我能想到的最好的答案。但这不是很漂亮,因为有很多重复的代码。你知道如何缩短它吗?
  • 如何使用Connection?按照您编写的方式,调用者必须知道配置的确切类型才能创建泛型。这没有多大意义。例如,什么调用代码可以访问.password?该协议不包括该协议,因此 Configuration 无法使用它。我不明白为什么配置是通用的。感觉 Connection 需要更多方法。
  • 对于这个特定问题更关键的是,连接实现的列表是否可能很小且是静态的(如这 2 个),还是可能存在您无法控制的任意数量的实现?两者都是可解的,但后者更复杂。
  • @RobNapier 是的,没错,调用者需要知道Connection 的类型,而这正是问题所在。使用泛型的方法来自另一个问题(请参阅stackoverflow.com/q/59353454/3272409),我最初的问题是,我无法将协议与Codable 一起使用。现在我看到使用泛型是行不通的。最后,调用者应该收到一个Configuration 对象,并且必须知道Connection 的类型。所以现在我知道,我必须将类型存储在Configuration 的某个变量中。有了这些信息,调用者就可以访问.password

标签: swift generics encoding decode codable


【解决方案1】:

以下解决方案解决了我在使用泛型时遇到的所有问题,但不知道 Connection 的具体类型。解决方案的关键是

  1. 在实现本身中保存 Connection 实现的类型,并
  2. 使用superEncodersuperDecoderfromto 属性进行编码/解码。

这是解决方案:

import Foundation

protocol Connection: Codable {
    var type: ConnectionType { get }
    var path: String { get set }
}


struct LocalConnection: Connection {
    let type: ConnectionType = ConnectionType.local

    var path: String
}


struct SFTPConnection : Connection {
    let type: ConnectionType = ConnectionType.sftp

    var path: String
    var user: String
    var sshKey: String

    init(path: String, user: String, sshKey: String) {
        self.path = path
        self.user = user
        self.sshKey = sshKey
    }
}


struct FTPConnection: Connection {
    let type: ConnectionType = ConnectionType.ftp

    var path: String
    var user: String
    var password: String
}


struct TFTPConnection: Connection {
    let type: ConnectionType = ConnectionType.tftp

    var path: String
}




enum ConnectionType : Int, Codable {
    case local
    case sftp
    case ftp
    case tftp

    func getType() -> Connection.Type {
        switch self {
        case .local: return LocalConnection.self
        case .sftp: return SFTPConnection.self
        case .ftp: return FTPConnection.self
        case .tftp: return TFTPConnection.self
        }
    }
}




struct Configuration {
    var from : Connection
    var to : Connection
    private var id = UUID.init().uuidString

    var fromType : ConnectionType { return from.type }
    var toType : ConnectionType { return to.type }

    init(from: Connection, to: Connection) {
        self.from = from
        self.to = to
    }
}


extension Configuration : Codable {

    enum CodingKeys: String, CodingKey {
        case id, from, to, fromType, toType
    }

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

        self.id = try container.decode(String.self, forKey: .id)

        var type : ConnectionType

        type = try container.decode(ConnectionType.self, forKey: .fromType)
        let fromDecoder = try container.superDecoder(forKey: .from)
        self.from = try type.getType().init(from: fromDecoder)

        type = try container.decode(ConnectionType.self, forKey: .toType)
        let toDecoder = try container.superDecoder(forKey: .to)
        self.to = try type.getType().init(from: toDecoder)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(self.id, forKey: .id)

        try container.encode(self.fromType, forKey: .fromType)
        let fromContainer = container.superEncoder(forKey: .from)
        try from.encode(to: fromContainer)

        try container.encode(self.toType, forKey: .toType)
        let toContainer = container.superEncoder(forKey: .to)
        try to.encode(to: toContainer)
    }
}

【讨论】:

    猜你喜欢
    • 2020-11-05
    • 2018-10-05
    • 2018-12-25
    • 2019-02-28
    • 1970-01-01
    • 1970-01-01
    • 2018-11-13
    • 1970-01-01
    • 2023-02-09
    相关资源
    最近更新 更多