3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Swiftの数値関連のプロトコル

Last updated at Posted at 2020-09-03

この記事を書くきっかけ

最近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バイトの値を扱う時に役に立ちます。

プロトコルの適合関係(整数型)

緑の適合関係をとりあえず無視してください。
プロトコルの関係.png

Equatableプロトコル

Swiftの標準のデータ型のほとんどが、このプロトコルに適合しています。
このプロトコルに適合する場合、2つの値が同じかどうかを比較するために、
演算子==の動作を定義する必要があります。
(!=もありますが、==の戻り値を逆転しただけ)
配列などでcontains(_:)メソッドを使えるのは、配列の要素がEquatableプロトコルに準拠しているからです。

ただ、下の条件を満たせば、==演算子を定義せずに、: Equatableと書くだけで大丈夫です

  1. 構造体なら、その全てのstored propertiesがEquatableプロトコルに適合しています
  2. 列挙型なら、associated valueがない、またはその全てがEquatableプロトコルに適合している
    条件を満たさない場合、staticメソッドとして==を定義する必要があります
static func == (lhs: Self, rhs: Self) -> Bool

実装しなければならないメソッドはこの1つだけとなりますが、他のオプションもあります。
詳しくは見出しのリンク先を参照してください

AdditiveArithmeticプロトコル

このプロトコルのおかげで、同じ型同士で足し算/引き算ができます。
このプロトコルに適合するには:

  1. : AdditiveArithmetic
  2. zeroという名前のstatic propertyを追加する。zeroの型はAdditiveArithmeticプロトコルに適合する。このプロパティは大きさを表すためのもので、インスタンス生成時のデフォルト値となります(Intの0)
  3. + += - -=を定義します

Numericプロトコル

このプロトコルのおかげで、同じ型同士で掛け算ができます。
このプロトコルに適合するには:

  1. : Numeric
  2. 必要なイニシャライザと、* *=演算子を定義します
  3. Magnitudeというassociatedtypeを追加する。これは大きさを表せるものです
  4. 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プロトコルに適合するには:

  1. : FixedWidthInteger
  2. init(bigEndian: Self)init(littleEndian: Self)を定義する
  3. 必要な8つのプロパティを定義する(詳しくは見出しのリンクより参照)
  4. ReportingOverflowで終わるメソッドを全て定義する(詳しくは見出しのリンクより参照)
    これ以外のメソッドはstandard libraryが提供してくれます

UnsignedIntegerプロトコルSignedIntegerプロトコル

負数じゃない数だけを表せるUnsignedIntegerプロトコル、
正数でも負数でも表せるSignedIntegerプロトコル、
どちらも静的プロパティmax minを持っています。

勉強することが多い...

ここまでは赤い矢印の適合関係にあるプロトコルでした。
ここからはBinaryIntegerプロトコルが適合したプロトコルを詳しく調べていきます......

と思いましたが、他に読みたいものができたので、
またいつか時間がある時に更新したいと思います。

SwiftやiOS開発関係の本2冊合わせて1000ページくらいの鈍器を入手したので、
最近はこれらを読みながら、UIKitのドキュメントも覗いてみます。
あと来月に応用情報がありますが、受かる気がしないですけどそれの勉強もしないと。。。。

あぁぁぁぁぁぁぁーーーーーーーーーーもっと時間がほしいいいいいいい (ಥ_ಥ)

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?