Failable Numeric Conversion Initializers
01 何が問題だったのか
Swift の数値型には、異なる数値型から値を変換するための一連のイニシャライザがあらかじめ用意されています。しかしその挙動は、呼び出し側のコードが何を期待しているかによっては扱いにくいものでした。
浮動小数点数から整数型へ変換すると、小数部分は黙って切り捨てられます。
let x = Int(3.7) // 3(切り捨てられる)
また、ある数値型の値が変換先の型で表現できる範囲を超えている場合、イニシャライザはランタイムトラップで停止します。
let big: Int64 = 200
let small = Int8(big) // 実行時に停止(Int8 には収まらない)
外部から受け取る値でとくに困る
JSON などの外部ソースから受け取るデータは、ゆるい型付けで渡ってくることが多く、モデルオブジェクトを作るときに期待する具体的な数値型へランタイムで変換する必要があります。このようなケースでは、値が収まるかどうかを変換前に確信できないのが普通で、変換に失敗したときに落ちてしまうのでは安全なアプリケーションを書くのが難しくなります。
小数部分の切り捨ても、「たまたま整数値だった浮動小数点数だけを整数として受け入れたい」「Int32 に収まる Int64 だけを安全に扱いたい」といった用途では、黙って情報を失うのではなく「変換できなかった」ことを呼び出し側で検知したい場面があります。
この提案の前は、標準ライブラリとしてそのような「情報が失われるなら失敗する」変換を提供する共通の手段がなく、呼び出し側で範囲チェックや小数部分チェックを手で書く必要がありました。
02 どのように解決されるのか
すべての数値型に、情報を失わずに変換できた場合のみ値を返し、そうでなければ nil を返す失敗可能(failable)なイニシャライザ init?(exactly:) を追加します。Swift 3.1 で実装されています。
init?(exactly:) の使い方
整数型同士・浮動小数点数同士、そして整数と浮動小数点数のあいだで、値がそのまま(厳密に等しく)表現できるときだけ成功する変換として使えます。
// 整数間の変換: 範囲内なら成功、範囲外なら nil
let a = Int8(exactly: 100 as Int64) // Optional(100)
let b = Int8(exactly: 200 as Int64) // nil(Int8 の範囲を超える)
// 浮動小数点数から整数への変換: 小数部分があるなら nil
let c = Int(exactly: 3.0) // Optional(3)
let d = Int(exactly: 3.7) // nil(切り捨てが必要)
let e = Int(exactly: Double.nan) // nil(有限の整数値で表せない)
// 整数から浮動小数点数への変換: 精度が落ちるなら nil
let f = Float(exactly: 16_777_216) // Optional(16777216.0)
let g = Float(exactly: 16_777_217) // nil(Float の仮数で厳密表現不可)
失敗可能イニシャライザなので、オプショナル束縛と組み合わせて扱います。
func parseCount(_ value: Int64) -> Int? {
return Int(exactly: value)
}
if let count = Int(exactly: json["count"] as! Int64) {
// count は Int として安全に使える
} else {
// Int に収まらなかった場合の処理
}
追加される変換の一覧
すべての数値型に対して、次のオーバーロードが追加されます。整数型には全整数型と全浮動小数点型からの、浮動小数点型にも同じく全整数型と全浮動小数点型からの失敗可能イニシャライザが揃う形です。
// 全整数型からの変換
init?(exactly value: Int8)
init?(exactly value: Int16)
init?(exactly value: Int32)
init?(exactly value: Int64)
init?(exactly value: Int)
init?(exactly value: UInt8)
init?(exactly value: UInt16)
init?(exactly value: UInt32)
init?(exactly value: UInt64)
init?(exactly value: UInt)
// 全浮動小数点型からの変換
init?(exactly value: Float)
init?(exactly value: Double)
#if arch(i386) || arch(x86_64)
init?(exactly value: Float80)
#endif
既存のイニシャライザはそのまま
これは純粋な追加変更で、既存のイニシャライザ(切り捨てたり、範囲外でトラップしたりするもの)の挙動は変わりません。情報の損失を許容したい場面ではこれまで通りのイニシャライザを使い、損失が起きたかどうかを検知したい場面では init?(exactly:) を使う、という選び分けになります。