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
相关产品推荐
相关产品推荐

