You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中如何让数组仅接收同时符合Animal与Codable的类型?

解决Swift中存在类型数组的Codable问题(不标记协议为Codable)

这个问题我之前也碰到过,核心原因是Swift没办法自动为存在类型数组(比如[Animal])合成Codable实现——毕竟协议本身没遵循Codable,而且Swift需要明确知道数组里每个元素的具体类型,才能正确完成编码和解码。结合你的需求(不把Animal标记为Codable,但数组仅接收同时符合Animal和Codable的类型),这里有两种可行的方案:

方案1:组合协议+手动实现Farm的Codable

首先我们定义一个组合协议,把Animal和Codable的要求合并,这样就能保证只有同时符合两个协议的类型才能加入数组。然后手动实现Farm的Codable逻辑,编码时记录每个元素的类型标识,解码时根据标识实例化具体类型。

代码实现

// 基础Animal协议,不标记Codable
protocol Animal {
    var name: String { get }
    func makeSound()
}

// 组合协议:同时符合Animal和Codable
protocol CodableAnimal: Animal, Codable {}

// 让你的结构体遵循组合协议
struct Dog: CodableAnimal {
    let name: String
    func makeSound() { print("Woof!") }
}

struct Cat: CodableAnimal {
    let name: String
    func makeSound() { print("Meow!") }
}

// Farm结构体,手动实现Codable
struct Farm: Codable {
    var animals: [CodableAnimal]
    
    // 定义类型标识符,用来区分不同的动物类型
    private enum AnimalType: String, Codable {
        case dog, cat
    }
    
    // 自定义编码逻辑
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var animalsArray = container.nestedUnkeyedContainer(forKey: .animals)
        
        for animal in animals {
            var animalContainer = animalsArray.nestedContainer(keyedBy: AnimalCodingKeys.self)
            
            // 写入类型标识
            switch animal {
            case is Dog:
                try animalContainer.encode(AnimalType.dog, forKey: .type)
            case is Cat:
                try animalContainer.encode(AnimalType.cat, forKey: .type)
            default:
                throw EncodingError.invalidValue(
                    animal,
                    EncodingError.Context(
                        codingPath: animalContainer.codingPath,
                        debugDescription: "不支持的动物类型"
                    )
                )
            }
            
            // 写入动物本身的数据
            try animalContainer.encode(animal, forKey: .data)
        }
    }
    
    // 自定义解码逻辑
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        var animalsArray = try container.nestedUnkeyedContainer(forKey: .animals)
        
        var animals = [CodableAnimal]()
        
        while !animalsArray.isAtEnd {
            let animalContainer = try animalsArray.nestedContainer(keyedBy: AnimalCodingKeys.self)
            let type = try animalContainer.decode(AnimalType.self, forKey: .type)
            
            // 根据类型标识解码具体的动物实例
            let animal: CodableAnimal = switch type {
            case .dog: try animalContainer.decode(Dog.self, forKey: .data)
            case .cat: try animalContainer.decode(Cat.self, forKey: .data)
            }
            
            animals.append(animal)
        }
        
        self.animals = animals
    }
    
    // 编码键定义
    private enum CodingKeys: String, CodingKey {
        case animals
    }
    
    private enum AnimalCodingKeys: String, CodingKey {
        case type, data
    }
}

使用示例

// 创建Farm实例
let dog = Dog(name: "Buddy")
let cat = Cat(name: "Whiskers")
let farm = Farm(animals: [dog, cat])

// 编码到JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(farm)
print(String(data: jsonData, encoding: .utf8)!)

// 解码JSON
let decodedFarm = try JSONDecoder().decode(Farm.self, from: jsonData)
decodedFarm.animals.forEach { $0.makeSound() }

方案2:类型擦除包装器

如果后续需要扩展更多动物类型,类型擦除的方式会更灵活——我们创建一个包装器AnyCodableAnimal,它遵循Animal和Codable,内部包装所有符合Animal & Codable的类型,把编码解码的逻辑封装在包装器里。

代码实现

// 基础Animal协议,不标记Codable
protocol Animal {
    var name: String { get }
    func makeSound()
}

// 让你的结构体直接遵循Animal和Codable
struct Dog: Animal, Codable {
    let name: String
    func makeSound() { print("Woof!") }
}

struct Cat: Animal, Codable {
    let name: String
    func makeSound() { print("Meow!") }
}

// 类型擦除包装器,遵循Animal和Codable
struct AnyCodableAnimal: Animal, Codable {
    private let _name: () -> String
    private let _makeSound: () -> Void
    private let _underlying: Codable
    
    // 只接受同时符合Animal和Codable的类型
    init<T: Animal & Codable>(_ animal: T) {
        self._name = { animal.name }
        self._makeSound = { animal.makeSound() }
        self._underlying = animal
    }
    
    // 实现Animal协议的要求
    var name: String { _name() }
    func makeSound() { _makeSound() }
    
    // 自定义编码
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        // 写入类型标识
        switch _underlying {
        case is Dog:
            try container.encode("dog", forKey: .type)
        case is Cat:
            try container.encode("cat", forKey: .type)
        default:
            throw EncodingError.invalidValue(
                _underlying,
                EncodingError.Context(
                    codingPath: container.codingPath,
                    debugDescription: "不支持的动物类型"
                )
            )
        }
        
        // 写入动物数据
        try _underlying.encode(to: encoder)
    }
    
    // 自定义解码
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        
        // 根据类型标识解码具体实例
        switch type {
        case "dog":
            let dog = try Dog(from: decoder)
            self.init(dog)
        case "cat":
            let cat = try Cat(from: decoder)
            self.init(cat)
        default:
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: container.codingPath,
                    debugDescription: "未知的动物类型标识:\(type)"
                )
            )
        }
    }
    
    private enum CodingKeys: String, CodingKey {
        case type
    }
}

// Farm结构体,直接使用包装器数组,可自动合成Codable
struct Farm: Codable {
    var animals: [AnyCodableAnimal]
}

使用示例

// 创建Farm实例(需要用包装器包裹动物)
let dog = Dog(name: "Buddy")
let cat = Cat(name: "Whiskers")
let farm = Farm(animals: [AnyCodableAnimal(dog), AnyCodableAnimal(cat)])

// 编码和解码逻辑和方案1一致
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try encoder.encode(farm)
print(String(data: jsonData, encoding: .utf8)!)

let decodedFarm = try JSONDecoder().decode(Farm.self, from: jsonData)
decodedFarm.animals.forEach { $0.makeSound() }

方案选择建议

  • 如果你的动物类型数量固定、不会频繁扩展,方案1更直接,不需要额外的包装器。
  • 如果后续需要添加更多动物类型,方案2的包装器可以把类型判断逻辑集中管理,扩展起来更方便。

内容的提问来源于stack exchange,提问作者ngngngn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:51:17