はじめに
何かをカウントするプログラムを書くとき、おそらく普通は以下のようなコードを書くのではないでしょうか。
var count: UInt64 = 0
func countUp() {
// +1する
count = count + 1
print("count=\(count)")
}
countUp() // count=1
countUp() // count=2
countUp() // count=3
...
この coutUp
メソッド、仮にcountが最大値( UInt64.max
)に達した次はどうなるでしょうか? 正解はオーバーフローしてクラッシュします。エラーも投げられないので、いきなり死にます。潜在的なバグを仕込んでしまっているわけです。
(ただし -Ounchecked オプションを付けたビルドのときはクラッシュしないはずです: 参考)
本稿では、カウンターの実装例を通して、オーバーフローを回避する方法を紹介します。
Count構造体の作成
カウントが最大値に達した後はどうすれば良いでしょうか? 話は簡単で、 加算前に count == UInt64.max
で比較し、trueなら加算しないようにすれば良いわけです。
ここで以下のようなCount構造体を作ります。
struct Count {
let sum: UInt64
init(_ initialValue: UInt64) {
self.sum = initialValue
}
func countUp() -> Count {
// UInt64の最大値を超えないように確認
if sum == UInt64.max {
// オーバーフローを防ぐために、現在のsumを維持(あるいはエラーを投げるなどの処理でもOK)
return Count(sum)
} else {
// sumに1を加えた新しいCountオブジェクトを返す
return Count(sum + 1)
}
}
}
このCount構造体の使い方は以下の通りです。
let a = Count(0)
print("a=\(a.sum)") // a=0
let b = a.countUp()
print("b=\(b.sum)") // b=1
let x = Count(.max)
print("x=\(x.sum)") // x=18446744073709551615
let y = x.countUp()
print("y=\(y.sum)") // y=18446744073709551615 <- 最大値のまま
x.countUp()
が返した y
は最大値でカウンターストップしていることが分かります。
上記の例では、最大値でカンストするようにしていますが、エラーを投げるようにして適切なエラーハンドリングをするやり方も考えられます。
これで安心して何かをカウントすることができますね。
応用編
ところで、 countUp
メソッドは常に+1するだけでしたが、任意の整数を加算したいこともあります。次に add(value:)
メソッドを考えてみましょう。先程のCount構造体に追加します。
struct Count {
let sum: UInt64
init(_ initialValue: UInt64) {
self.sum = initialValue
}
func countUp() -> Count {
// UInt64の最大値を超えないように確認
if sum == UInt64.max {
// オーバーフローを防ぐために、現在のsumを維持
return Count(sum)
} else {
// sumに1を加えた新しいCountオブジェクトを返す
return Count(sum + 1)
}
}
func add(value: UInt64) -> Count {
// UInt64の最大値を超えないように確認
if sum + value > UInt64.max {
// オーバーフローを防ぐために、現在のsumを維持
return Count(sum)
} else {
// sumにvalueを加えた新しいCountオブジェクトを返す
return Count(sum + value)
}
}
}
このコードは一見正しそうですが、クラッシュする可能性があります。なぜなら sum + value
の演算時点でオーバーフローする可能性があるからです。
ではどうすれば良いでしょうか? SwiftのUInt64構造体には便利な addingReportingOverflow
というメソッドが用意されています。
UInt64以外のInt系の構造体にも addingReportingOverflow
メソッドは用意されています。
以下は addingReportingOverflow
メソッドを利用した実装です。
struct Count {
let sum: UInt64
init(_ initialValue: UInt64) {
self.sum = initialValue
}
func countUp() -> Count {
// UInt64の最大値を超えないように確認
if sum == UInt64.max {
// オーバーフローを防ぐために、現在のsumを維持
return Count(sum)
} else {
// sumに1を加えた新しいCountオブジェクトを返す
return Count(sum + 1)
}
}
func add(value: UInt64) -> Count {
let (next, overflow) = sum.addingReportingOverflow(value)
if overflow {
// オーバーフローしたため、現在のsumを維持
return Count(sum)
} else {
// 加算結果を新しいCountオブジェクトに詰めて返す
return Count(next)
}
}
}
addingReportingOverflow
メソッドは加算結果とオーバーフローした場合trueになるBool値を返します。この overflow
フラグをチェックすることで安全に加算することができます。
以下は add(value:)
メソッドの使用例です。
let a = Count(3).add(value: 2)
print("a=\(a.sum)") // a=5
let x = Count(.max).add(value: 2)
print("x=\(x.sum)") // x=18446744073709551615 <- 最大値のまま
x
はオーバーフローせずに最大値でカウンターストップしていることが分かります。
ちなみに、乗算のときも同様に multipliedReportingOverflow(by:)
メソッドが用意されています。扱う数字が巨大なときは、このメソッドを利用した方が安全かもしれません。
まとめ
現実的には最大値までカウンターが回ることは滅多にないかもしれません。ただ、Intの加算には上限があるんだよ、ということを頭の片隅に入れておくと良いと思います。