この記事を書くきっかけ
最近NBTファイルを解析するためのライブラリをSwiftで作りました。
作る前にGithubで探して、swiftのライブラリが出てこなかったような気がしますが、
作り終わってもう一回探してみたら、2つもありました(O.O)....
1つはObjective-CのライブラリをSwiftで使ってるだけなので、無視します。
もう1つは今年2020年前半に作られたもので、ソースファイルをみると、
FixedWidthInteger
とか、BinaryInteger
とか、知らないものがたくさんありましたし、
自分が書いたものよりはるかに上手だから少しショックでした。
調べてみると、
FixedWidthInteger
などはSwiftのStandard Libraryにあるプロトコルのことです。
Swiftで整数をうまく扱うために、
Apple Developer DocumentationとStandard Libraryを読むことにします。
忘れた時のために、そのまとめを記事として残します。
まずは標準の整数型
- 符号つき整数型:
Int
,Int8
,Int16
,Int32
,Int64
- 符号なし整数型:
UInt
,UInt8
,UInt16
,UInt32
,UInt64
この中で、UInt8
は1バイトの値を扱う時に役に立ちます。
プロトコルの適合関係(整数型)
Equatableプロトコル
Swiftの標準のデータ型のほとんどが、このプロトコルに適合しています。
このプロトコルに適合する場合、2つの値が同じかどうかを比較するために、
演算子==
の動作を定義する必要があります。
(!=
もありますが、==
の戻り値を逆転しただけ)
配列などでcontains(_:)
メソッドを使えるのは、配列の要素がEquatableプロトコルに準拠しているからです。
ただ、下の条件を満たせば、==
演算子を定義せずに、: Equatable
と書くだけで大丈夫です
- 構造体なら、その全ての
stored properties
がEquatableプロトコルに適合しています - 列挙型なら、
associated value
がない、またはその全てがEquatableプロトコルに適合している
条件を満たさない場合、staticメソッドとして==
を定義する必要があります
static func == (lhs: Self, rhs: Self) -> Bool
実装しなければならないメソッドはこの1つだけとなりますが、他のオプションもあります。
詳しくは見出しのリンク先を参照してください
AdditiveArithmeticプロトコル
このプロトコルのおかげで、同じ型同士で足し算/引き算ができます。
このプロトコルに適合するには:
: AdditiveArithmetic
-
zero
という名前のstatic propertyを追加する。zero
の型はAdditiveArithmeticプロトコルに適合する。このプロパティは大きさを表すためのもので、インスタンス生成時のデフォルト値となります(Intの0) -
+
+=
-
-=
を定義します
Numericプロトコル
このプロトコルのおかげで、同じ型同士で掛け算ができます。
このプロトコルに適合するには:
: Numeric
- 必要なイニシャライザと、
*
*=
演算子を定義します -
Magnitude
というassociatedtypeを追加する。これは大きさを表せるものです -
magnitude
というインスタンスプロパティを追加する。これは大きさを表すためのプロパティです
BinaryIntegerプロトコル と SignedNumericプロトコル
この2つのプロトコルは、Numericプロトコルに適合しています。
SignedNumericプロトコルは、-
演算子を拡張して、数値の正負を反転できるようにします。
デフォルトでプロトコル内で拡張した-
とnegate()
が実装されているので、
このプロトコルに適合しても、特に何もしなくても良いです。
var number = 100
let a = -number // a = -100
print(number) // 100
// negate()は自身の値を変えてしまいます
let b = number.negate() // b = -100
print(number) // -100
カスタマイズしたい場合、mutating func negate()
を新しく実装してください。
また、符号付き整数の場合、表せる負の数は正の数よりも1つ多いので、
その最小の負数を反転するとき、特別の値を返す、またはエラーを投げる必要があります。
BinaryIntegerプロトコルは、Swiftの標準で提供される全ての整数型の元となります。
算術・論理・比較・複合代入・ビット演算子はここで定義/拡張されています。
このプロトコルに適合れば、違う型でも大小関係を比べられます。(例えばInt(64)<UInt32(100)
がtrue)
浮動小数点数型や他の整数型から、BinaryIntegerプロトコルに適合したインスタンスを生成するために、4つの変換方法が用意されています。
1. 各型のデフォルトイニシャライザを使った変換 (Range-Checked Conversion)
もし変換する値が、対象型が表せる範囲内でなければ、ランタイムエラーが起こります。
let a = Int16(500) // 500
let b = UInt8(500) // error
2. init?(exactly:)
を使った変換 (Exact Conversion)
与えられた数値を、この型で表せるなら、オプショナルのインスタンスが生成されます。
そうでなければ、nilとなります
let a = Int16(exactly: 500) // Optional(500)
let b = UInt8(exactly: 500) // nil
また、浮動小数点数が与えられた場合、整数かどうかもチェックされます
(整数でなければnilが返されます)
3. init(clamping:)
を使った変換 (Clamping Conversion)
2つ目と違って、範囲外の数値が与えられた場合、この型の最大値または最小値に変えられます。
// Int8.min : -128
// Int8.max : 127
Int8(clamping: -200) // -128
Int8(clamping: 300) // 127
4. init(truncatingIfNeeded:)
を使った変換 (Bit Pattern Conversion)
2進数表現での変換と思えばわかりやすいでしょう
例えば2バイトの0b00000000 00011101
を1バイトに変換したり、4バイトに変換したりします。
必要に応じて足りない部分を(2進数の)0または1で埋めたり、溢れ出した部分をカットしたりします。
let number01: Int16 = 9843 // 0b00100110_01110011
// よりバイト数が少ない型に変換。一部だけ切り取るので値が変わる
let a = Int8(truncatingIfNeeded: number01) // 0b01110011 = 115
// よりバイト数が多い型に変換。正の数なので0で先頭を埋める
let b = Int32(truncatingIfNeeded: number01) // 0b00000000_00000000_00100110_01110011 = 9843
let number01: Int8 = -100 // 0b10011100
// 符号つき整数から符号なし整数に変換、値が変わる
let c = UInt8(truncatingIfNeeded: number02) // 0b10011100 = 156
// よりバイト数が多い型に変換。負の数なので1で先頭を埋める
let d = Int16(truncatingIfNeeded: number02) // 0b11111111_10011100 = -100
ちなみに、Swiftの整数値はリトルエンディアン(little endian ⇆ big endian)です。
どういうことかと言うと、
上の例では、Int16型の9843のビッグエンディアン表現は00100110_01110011
ですが、
内部では01110011_00100110
として保存されています。
Int8型に変換する際に、一番最初の1バイトの01110011
だけ切り取ります。
例だけを見れば、後ろから1バイトずつ切り取っていくように感じてしまうかもしれませんが、内部の動作が違います。
FixedWidthIntegerプロトコル
このプロトコルは、BinaryIntegerプロトコルで使える演算子に、ビットごとの演算(&
|
^
~
など)、ビットシフト(>>
<<
など)、そしてオーバーフロー時の処理を追加します。
ビットごとに計算したり、ビットシフトをしたり、オーバーフローを受け取ったり、型が表せる最大値&最小値を取得したりする際に、FixedWidthIntegerプロトコルを制限として適合する、または機能を拡張して適合することが良いでしょう。
(もっと早くこれを知ればよかったT-T)
2つの公式の例をそのまま借ります。
1つ目は、整数値を2進数の文字列に変換するものです
extension FixedWidthInteger {
var binaryString: String {
var result: [String] = []
for i in 0..<(Self.bitWidth / 8) {
let byte = UInt8(truncatingIfNeeded: self >> (i * 8))
let byteString = String(byte, radix: 2)
let padding = String(repeating: "0",
count: 8 - byteString.count)
result.append(padding + byteString)
}
return "0b" + result.reversed().joined(separator: "_")
}
}
print(Int16.max.binaryString)
// Prints "0b01111111_11111111"
print((101 as UInt8).binaryString)
// Prints "0b11001001"
2つ目は、整数値の2乗を計算するものです。
オーバーフローの対応として、nilを返すようにしています。
func squared<T: FixedWidthInteger>(_ x: T) -> T? {
let (result, overflow) = x.multipliedReportingOverflow(by: x)
if overflow {
return nil
}
return result
}
let (x, y): (Int8, Int8) = (9, 123)
print(squared(x))
// Prints "Optional(81)"
print(squared(y))
// Prints "nil"
自作の型にFixedWidthIntegerプロトコルに適合するには:
: FixedWidthInteger
-
init(bigEndian: Self)
とinit(littleEndian: Self)
を定義する - 必要な8つのプロパティを定義する(詳しくは見出しのリンクより参照)
-
ReportingOverflow
で終わるメソッドを全て定義する(詳しくは見出しのリンクより参照)
これ以外のメソッドはstandard libraryが提供してくれます
UnsignedIntegerプロトコルとSignedIntegerプロトコル
負数じゃない数だけを表せるUnsignedIntegerプロトコル、
正数でも負数でも表せるSignedIntegerプロトコル、
どちらも静的プロパティmax
min
を持っています。
勉強することが多い...
ここまでは赤い矢印の適合関係にあるプロトコルでした。
ここからはBinaryIntegerプロトコルが適合したプロトコルを詳しく調べていきます......
と思いましたが、他に読みたいものができたので、
またいつか時間がある時に更新したいと思います。
SwiftやiOS開発関係の本2冊合わせて1000ページくらいの鈍器を入手したので、
最近はこれらを読みながら、UIKitのドキュメントも覗いてみます。
あと来月に応用情報がありますが、受かる気がしないですけどそれの勉強もしないと。。。。
あぁぁぁぁぁぁぁーーーーーーーーーーもっと時間がほしいいいいいいい (ಥ_ಥ)