Oops!
Range ( 0...10
みたいなやつ) をメンバとして持つ Hoge
型が、コンパイルに失敗してしまいました。
どうも Swift4の Range1 は Codable
に適合していないようです。
困った、どうしたもんでしょうね。
そうだ、 Codable に後付けで適合させる extension 書けばいいじゃない!
extension CountableClosedRange: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rangeDescription = try container.decode(String.self)
let rangeElements = rangeDescription.components(separatedBy: "...")
guard
rangeElements.count == 2
else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "\"\(rangeDescription)\" is invalid closed range expression"
)
}
guard
let l = rangeElements.first.flatMap({ Int($0) }) as? Bound,
let u = rangeElements.last.flatMap({ Int($0) }) as? Bound
else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "\"\(rangeDescription)\" is composed by non-int bounds"
)
}
lowerBound = l
upperBound = u
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("\(lowerBound)...\(upperBound)")
}
}
実行結果
struct Hoge: Codable {
let range: CountableClosedRange<Int>
}
do {
let jsonData = """
{
"range": "5...10"
}
""".data(using: .utf8)!
let t = try! JSONDecoder().decode(Hoge.self, from: jsonData)
print(t)
// Hoge(range: CountableClosedRange(5...10))
}
do {
let t = Hoge(range: 3...9)
let data = try! JSONEncoder().encode(t)
let s = String(data: data, encoding: .utf8)!
print(s)
// {"range":"3...9"}
}
do {
let jsonData = """
{
"range": "5..<10"
}
""".data(using: .utf8)!
let t = try! JSONDecoder().decode(Hoge.self, from: jsonData)
// Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(
// codingPath: [__lldb_expr_100.Hoge.(CodingKeys in _ABCFAB28040915CEF6AA3347992A1FE1).range],
// debugDescription: "\"5..<10\" is invalid closed range expression", underlyingError: nil))
}
do {
let jsonData = """
{
"range": "a...z"
}
""".data(using: .utf8)!
let t = try! JSONDecoder().decode(Hoge.self, from: jsonData)
// Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(
// codingPath: [__lldb_expr_102.Hoge.(CodingKeys in _79FE350F6AE58BCB4510A745A8A0AB74).range],
// debugDescription: "\"a...z\" is composed by non-int bounds", underlyingError: nil))
}
Hoge
型が無事にコンパイルを通りました。
しかも、 decode, encode, さらにはJSONで想定外な range 表現が現れたときの例外処理も適切に行われています。
Codable とエラーハンドリング
今回のコードで注目してほしいのは、以下の部分です。
guard
rangeElements.count == 2
else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "\"\(rangeDescription)\" is invalid closed range expression"
)
}
guard
let l = rangeElements.first.flatMap({ Int($0) }) as? Bound,
let u = rangeElements.last.flatMap({ Int($0) }) as? Bound
else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "\"\(rangeDescription)\" is composed by non-int bounds"
)
}
as? Bound
によって、具体的な Bound
を知らなくても init(from decoder: Decoder)
を書くことができました。2
しかも異常な rangeの表現が与えられたときは guard ... else { throw ... }
で具体的に失敗要因を知らせて失敗させることもできました。
このように throws
を活用した強力なエラーハンドリングとの統合が、 Codable プロトコルの強みです。
If any of those look wrong, then the JSON decoder can throw an error and stop the decode right there. After that, we want to convert from things like JSON arrays, and dictionaries, and strings into your types, commits and authors.
That is, after all, the entire point of this API. But there may be more that we can do, and we call that domain-specific validation. (...) These kinds of things can be difficult to express in Swift's type system, but we do think we have a great tool for handling those, and that's just simply writing more Swift code.
What's New in Foundation - WWDC 2017 - Videos - Apple Developer
そういうわけで、 Codable ってやっぱりよくできてるなあというお話でした。
おまけ: こういうextensionはダメでした
今回、 Bound
がたまたま Int
以外ありえない例だったらからよかったものの、本当なら as? Bound
でダウンキャストさせるんじゃなくて、 Bound
が Int
の場合に限定したコードを書きたいものですよね。
じゃあ、こうしてみましょうか。
extension CountableClosedRange<Int>: Codable {
...
}
おっと。怒られました。 OK、 where で制約を書けばいいんだね?
……と思いきや、
extension CountableClosedRange: Codable where Bound == Int {
...
}
やっぱりダメでした。
Swift4.0 では、制約条件がついている場合、プロトコルへの適合は不可能なのです。
なお、この問題については Swift Evolution-0143 Conditional conformances で解決される予定です。
swift-evolution/0143-conditional-conformances.md · apple/swift-evolution
現在 Accepted 状態です。 Swift4 に入らなかったのは残念……。実装が待ち遠しいですね。