Swift4のCodableが便利そうということで飛びついた結果、継承+アレイの組み合わせが混じっているととんでもなく面倒なことが起こるということが発覚したわけですが。
http://qiita.com/Yokemura/items/3a704db0bcadec381549
そもそも、継承が存在する時点で親子ともに手作業でencode/decodeを書かなきゃいけないようで
https://stackoverflow.com/questions/44553934/using-decodable-in-swift-4-with-inheritance
正直継承関係のあるオブジェクトをCodableの世界に持ち込むのはやめたほうがいいなと思いました。ひとつやふたつだったらいいですけど、それをアレイにぶち込んでなんたら・・・とかちょっと無理ですね。実際ちょっと書こうとしましたが複雑すぎてあきらめました。それならもうNSCodingの世界に戻った方がましです。
ひとまずサブクラスを使ってポリモーフィズムわっしょい、ってのは厄介そうってことで、ごくプリミティブにこんな風にするのもまあ悪くはないと思ったのですが、これはまあ格好悪いしインスタンスのタイプを見て分岐しているところとかが全部書き直しになるので避けたいなあと。
enum EventType {
case Note
case PitchBend
}
struct SequenceEvent : Codable {
var type : EventType
// Common
var time : Double
// For Note
var noteNo : Int
var velocity : Float
var duration : Float
// For PitchBend
var depth : Float
}
そこでふと思ったのが、ポリモーフィズムはさておき継承が厄介ってところもあったので、プロトコルを使ってこんな感じにしたらどうかな、と。そもそも共通プロパティはtimeくらいしかないというのもあるし。
protocol SequenceEvent : Codable {
var time : Double {get set};
}
struct SequenceEventNote : SequenceEvent {
var time : Double
var noteNo : Int
var velocity : Float
var duration : Float
// クラス継承の関係ではないので、親をデコード/エンコードしてから自分を・・・みたいな処理は不要
}
struct SequenceEventPitchBend : SequenceEvent {
var time : Double
var depth : Float
// 同上
}
とりあえずこれなら var events = Array<SequenceEvent>
という形で、同じ規約に沿った異なるオブジェクトをひとつのアレイに入れることはできるわけです。
で、これを保持する側のencode/decodeはどうなるか。
こうなりました。
class Song : Codable {
var events : [SequenceEvent] = [];
var name : String = "Song"
init(name: String) {
self.name = name
}
enum CodingKeys: String, CodingKey {
case name
case events
}
required init(from decoder: Decoder) throws {
let container = try? decoder.container(keyedBy: CodingKeys.self) // 普通にContainerを取得
if let con = container {
self.name = try con.decode(String.self, forKey: .name) // これは普通にdecode
var unkeyed = try con.nestedUnkeyedContainer(forKey: .events); // arrayの中身を個別にdecodeするため unkeyed containerを取得
var decodedArray: [SequenceEvent] = []
while (!unkeyed.isAtEnd) { // Sequenceプロトコルには準拠してないのでforeachなどは使えない模様
// *** 以下、もうちょっと抽象化できそう ***
// SequenceEventNoteへデコードしてみる
let anEvent = try? unkeyed.decode(SequenceEventNote.self)
if anEvent != nil {
decodedArray.append(anEvent!)
continue
}
// 上記が失敗したらSequenceEventPitchBendへデコードしてみる
let aPitchEvent = try? unkeyed.decode(SequenceEventPitchBend.self)
if aPitchEvent != nil {
decodedArray.append(aPitchEvent!)
continue
}
// 両方失敗した場合は知らん
}
self.events = decodedArray
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name) // ここもnameは普通にエンコード
var evs = container.nestedUnkeyedContainer(forKey: .events) // .eventsキーに割り当てる形で unkeyed containerを用意
try self.events.forEach { // 型を調べて個別にエンコード
switch $0 {
case is SequenceEventNote:
try evs.encode($0 as! SequenceEventNote) //as!は要らないかも
case is SequenceEventPitchBend:
try evs.encode($0 as! SequenceEventPitchBend) //as!は要らないかも
default:
try evs.encodeNil()
}
}
}
}
長い。
が、そんなに複雑ではないです。
アレイを Array<Type>
の形でまるごとencode/decodeするかわりに、unkeyed containerを引っ張り出してきて、それを使って個別に処理しています。
あんまりスマートではないですが、比較的修正範囲も小さいのでまあ逃げ道としてはアリかなあ、という感じですね。
この場合、(元)親クラスが持っていたのが time くらいということで親側の重みが小さかったのでこれが良さそうですが、逆に親側の重みが大きい場合、冒頭のポリモーフィズムを放棄するパターンの方がいいかもです。