swiftでprotocolをCodableにした時に出るエラーの対処方法
protocol
をCodable
にしてそれを継承したstruct
を他のstruct
のプロパティにしたらCodable
でエラーが出た。
問題のコード
まずはこんなprotocol
を作成。
protocol ShapeProtocol: Codable {
var position: PVector {get}
func hasIntersection(ray: Ray) -> Intersection?
}
PVector
はCodable
なstruct
。
コード
struct PVector: Codable {
let x: Double
let y: Double
let z: Double
}
このShapeProtocol
に対応したSphere
のコードが以下。
struct Sphere: ShapeProtocol {
let radius: Double
// ShapeProtocol
var position: PVector
init(position: PVector, radius: Double) {
self.radius = radius
self.position = position
}
// ShapeProtocol
func hasIntersection(ray: Ray) -> Intersection? {
return nil
}
}
この状態でSphere
のインスタンスを作成してエンコード&デコードしてみる。
let sphere = Sphere(position: PVector(0,0,0), radius: 2.0)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encodedSphereData = try encoder.encode(sphere)
guard let sphereJson = String(data: encodedSphereData, encoding: .utf8) else {
XCTAssert(true)
return
}
print("encoded: \(sphereJson)")
let decoder = JSONDecoder()
guard let sphereData = sphereJson.data(using: .utf8) else {
XCTAssert(true)
return
}
let decodedSphere = try decoder.decode(Sphere.self, from: sphereData)
print("decoded: \(decodedSphere)")
結果は下記。
encoded: {
"position" : {
"x" : 0,
"y" : 0,
"z" : 0
},
"radius" : 2
}
decoded: Sphere(radius: 2.0, position: CodableSample.PVector(x: 0.0, y: 0.0, z: 0.0))
ちゃんとできてる。
次に[ShapeProtocol]
をプロパティに持つScene
を作成。
struct Scene: Codable {
let position: PVector
let shapes: [ShapeProtocol]
}
するとlet shapes: [ShapeProtocol]
で下記のエラーが出る。
Type 'Scene' does not conform to protocol 'Decodable'
Type 'Scene' does not conform to protocol 'Encodable'
念のためにこの行をコメントアウトしてlet position: PVector
だけにするとエラーは出ない。
念のためにScene
にCodable
なコードを足してみる。
struct Scene: Codable {
let position: PVector
let shapes: [ShapeProtocol]
// Codable
enum CodingKeys: String, CodingKey {
case position, shapes
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.position = try values.decode(PVector.self, forKey: .position)
self.shapes = try values.decode([ShapeProtocol].self, forKey: .shapes)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(position, forKey: .position)
try container.encode(shapes, forKey: .shapes)
}
}
するとshapes
をデコード・エンコードする箇所で以下のエラーが出る。
Protocol 'ShapeProtocol' as a type cannot conform to 'Decodable'
Protocol 'ShapeProtocol' as a type cannot conform to 'Encodable'
ShapeProtocol
はCodable
なのになぜ?
Protocol doesn't confirm to itself
ググってみると出てきたのがstackoverflowのこのページ。色々なページから辿ると結局このページにやってくる。protocol
は継承するべきものの定義であって、protocol
そのものが継承しているprotocol
に対応しているわけではないということか。ShapeProtocol
は自身を継承したstruct
のSphere
にCodable
を強いるけど、ShapeProtocol
そのものはCodable
に対応しているわけではない。だから[ShapeProtocol]
な型のshapes
がCodable
でないのでScene
がCodable
に対応(confirm)できていないとエラーが出る。
で、どうすれば良いのか?
Codableなラッパーで包む
このページに2つの解決方法が載っていた。enum
を使うか。
今回は特に考えなしに最初の解決策だったラッパーで包んでみた。
Scene
を以下のように変更。
struct Scene: Codable {
let position: PVector
let shapes: [ShapeProtocol]
enum CodingKeys: String, CodingKey {
case shapes, position
}
init(position: PVector, shapes: [ShapeProtocol]) {
self.position = position
self.shapes = shapes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.position = try container.decode(PVector.self, forKey: .position)
let wrappers = try container.decode([ShapeWrapper].self, forKey: .shapes)
self.shapes = wrappers.map { $0.shape }
}
func encode(to encoder: Encoder) throws {
let wrappers = shapes.map { ShapeWrapper($0) }
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(position, forKey: .position)
try container.encode(wrappers, forKey: .shapes)
}
}
fileprivate struct ShapeWrapper: Codable {
let shape: ShapeProtocol
enum CodingKeys: String, CodingKey {
case base, payload
}
enum Base: String, Codable {
case sphere
}
init(_ shape: ShapeProtocol) {
self.shape = shape
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let base = try container.decode(Base.self, forKey: .base)
switch base {
case .sphere:
self.shape = try container.decode(Sphere.self, forKey: .payload)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch shape {
case let payload as Sphere:
try container.encode(Base.sphere, forKey: .base)
try container.encode(payload, forKey: .payload)
default:
break
}
}
}
基本的に載っていた方法そのままだがjson文字列を見る時に数字でなく名前で見られるようにBase
をInt
からString
に変更してある。
やっていることはエラーになっていた箇所でShapeProtocol
でなくCodable
なstruct
のShapeWrapper
をデコード・エンコードする様になっている。これでエラーは出なくなった。
デコード・エンコードの結果
scene encoded: {
"shapes" : [
{
"base" : "sphere",
"payload" : {
"position" : {
"x" : 0,
"y" : 0,
"z" : 0
},
"radius" : 2
}
},
{
"base" : "sphere",
"payload" : {
"position" : {
"x" : 10,
"y" : 20,
"z" : 30
},
"radius" : 5
}
}
],
"position" : {
"x" : 1,
"y" : 2,
"z" : 3
}
}
scene decoded: Scene(position: CodableSample.PVector(x: 1.0, y: 2.0, z: 3.0), shapes: [CodableSample.Sphere(radius: 2.0, position: CodableSample.PVector(x: 0.0, y: 0.0, z: 0.0)), CodableSample.Sphere(radius: 5.0, position: CodableSample.PVector(x: 10.0, y: 20.0, z: 30.0))])
めでたし、めでたし。
動くコード
最終的なコードはこちら。
最後に
今回の解決方法だとShapeProtocol
を継承するstruct
が増える度にShapeWrapper
にも書き加える必要が出てくる。これはenum
を使った方法でも同じ。面倒なことになるだけで本末転倒な気がする…。
Discussion