Swift - 整数型でハマる
Swiftの整数型はSwift Standard Library | Apple Developer Documentationにあるように、符号ありと符号なしの幅指定なしのInt/UIntと幅指定有りのInt32/Int64...などがあります。
こんな感じです。
Type | Minimum Value | Maximum Value |
---|---|---|
Int8 | -128 | 127 |
Int16 | -32,768 | 32,767 |
Int32 | -2,147,483,648 | 2,147,483,647 |
Int64 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
Int | 32bit環境ではInt32, 64bit環境ではInt64と同じ | 左に同じ |
UInt8 | 0 | 255 |
UInt16 | 0 | 65,535 |
UInt32 | 0 | 4,294,967,295 |
UInt64 | 0 | 18,446,744,073,709,551,615 |
UInt | 32bit環境ではUInt32, 64bit環境ではUInt64と同じ | 左に同じ |
(Int, UIntを追記:20161023)
符号有り無しの違い
符号有り無しでの違いは
- 当然符号なし整数には負数が格納できない
- 最大値・最小値が違う
- シフト時の動作が違う
上2つは明らかだと思います。シフト時の動作は符号ありの場合は算術シフト、符号なしの場合は論理シフトになります。なので、符号ありの整数のシフトは符号ビット(MSB)は変わらず、(例えば64ビット整数の場合)残りの下位63ビットがシフトします。符号なしの場合は関係なく全部シフトして、空いたところには0が入ります。
整数型同士の計算
四則演算や剰余の演算子が使えます。
The Swift Programming Language (Swift 3): Basic Operators
演算子は同じ型同士の計算でしか使えません。UIntをIntで割ったりかけたりはできません。
あとはビット演算もできます。C言語と同じく&
,|
,^
が使えます。シフトもできます。
The Swift Programming Language (Swift 3): Advanced Operators
整数型同士の変換
これは簡単で
let a: UInt8 = 0b00001111 let b: Int8 = Int8(a)
というように整数型の初期化を使います。ただ
let a: UInt8 = 0b10001111 let b: Int8 = Int8(a)
これはいけない。正しくは
let a: UInt8 = 0b10001111 let c: Int8 = Int8(a &% UInt8(Int8.max))
としないとオーバーフローしてしまう。実際のプログラムはオーバーフローしたところで強制終了します。 Int8の最大値を超える数値を代入してはいけない。UInt8の場合は0b10000000を超える数字はInt8にキャストできません。当然Int16とかにはできます。
オーバーフロー演算子
というわけで、Swiftはオーバーフロー検査がいちいち入っているので例えばInt8同士の計算で
let x1: UInt8 = 0b11111111 let x2: UInt8 = 0b00000001 x1 + x2
はできません。答えがUInt8の範囲を超えるからです。ただ、適当にオーバーフローしても良い場合、オーバーフロー演算子を使えばオーバーフローした後の結果が帰ってきます。つまり、オーバーフローした後の計算の下位8ビットがこの場合は答えになります。
let x1: UInt8 = 0b11111111 let x2: UInt8 = 0b00000001 x1 &+ x2 // Taking least 8 bit of 0b100000000 is 0b00000000 = 0
とにかく罠が多い。安全とも言えます。
The Swift Programming Language (Swift 3): Advanced Operators
IntまたはUIntの運用(20161023追記)
つらつらとわかっている人向けに整数のハマりどころについて書いてきました。言語マニュアルはIntを使うように強く勧めているようです。なので上の話がさっぱりで、整数がビット列としてメモリ上に乗っていることを想像できない人はIntだけを使うのが安全です。これは初心者を侮っているのではなくて本当にそれが安全です(ここ強調)。
整数変数のビットイメージが明確にあって、どういう処理が走るのかμOpCodeレベルでイメージできる人はInt以外を使いましょう。巨大な変数が必要なときは巨大整数ライブラリを使いましょう。"swift big integer"ぐらいで検索するとたくさん出てきます。つってもUをつけても使える最大整数は倍にしかならないですけどね。
じゃあどういうときに幅指定IntやUInt達が必要なの?
幅指定Intが必要な理由は使いたい範囲が広いもしくは狭いからではありません。(超メモリタイトな環境でなければ)そんな理由で使ってはいけません。 整数の範囲検査は基本的には自分でassertするべきです。
幅指定が必要な理由はIntをビット列として使うため、つまりバイナリを扱うときや暗号化に必要なシフト、ローテート演算など、整数をビット列として取り扱うときです。UIntが必要な理由も一緒です。普通の四則演算の整数として使うときはIntを使うのが安全です。たとえ絶対に非負整数だとわかっていてもIntを使いましょう。
オーバーフロー検査なんかいらんわ
という場合はコンパイラの最適化レベルを変えましょう。
プロジェクトの設定のBuild Settings -> Swift Compiler - Code Generation
のところを
と変えるとオーバーフロー検査とかその他もろもろの検査(配列の範囲外検査)とかが実施されなくなって若干早くなります。セキュリティとかに関わるプログラムではやめたほうが良さそうですが、あまり正確な計算が必要でないスピードが必要なプログラム、そう、例えば将棋やチェス、オセロのAIなんかでは役に立ちそうです。