通常ではSwiftは予期しない挙動を防ぐために、メモリへの直接アクセスを許可していません。しかしUnsafe APIを使うことでそれが可能になります。ここではUnsafe APIを必要最低限な箇所にだけ使い、可読性が高く、安全なコードを構築する秘訣を教えます。
未定義の動作 (Undefined Behavior)
今日はUnsafe Swiftの安全性についてお話します。まずは未定義動作から始めましょう。未定義動作は普通良くないものです。開発者はプログラムのクラッシュの可能性を懸念して、未定義動作を嫌います。動くかもしれないし、クラッシュするかもしれません。1時間後に変なことが起こるかもしれません。つまり、未定義動作はソフトウェアスケジュールを破棄し、コードの安全性を確保するためにSwiftのコードを長くします。メモリへの直接アクセスを許可していませんし、初期化されていない変数にはアクセスできないのがデフォルトの挙動です。
Swiftのポインタ
しかし、C言語のようなUnsafeな言語で動作させる場合や、パフォーマンスの向上や低レベルのAPIにアクセスしたい場合、これにアクセスする必要がある場合があります。Swiftにはこれを行うためのツールがたくさん用意されています。ポインタは1種類だけではなく、たくさんあります。MutableなものやMutableではないもの、RawまたはTypes、バッファ、CollectionまたはStrideです。
また実際、今年始めに、raywenderlich.comにこれらポインタの種類に関する説明をした、無料のチュートリアルをアップロードしました。ここにもストリーミングデータ圧縮アルゴリズムと型安全な乱数生成器など、いくつか例を���せています。
DictionaryとSet
しかし今日は、別の側面や別のアプリについて、DictionaryやSetに関する話したいと思います。これらのデータ構造は定数時間で探索ができるところがすばらしいですが、それはすべて良いハッシュ値を持っているからです。ハッシュ値が良くなければ、その保証はまるで無くなります。突然、線形時間探索となり、線形時間探索は、二次時間から1ループ離れているだけです。
実際、ハッカーはハッシュアルゴリズムの欠陥を利用してサービス不能攻撃を開始しています。だからSwift標準ライブラリはたくさんの種類のハッシュ値を提供しています。ご自身のコードでこれを活用することができます。
ハッシュ値を使った良い例を用意しました。
struct Angle: Hashable {
var radians: Double
var hashValue: Int {
return radians.hashValue
}
}
より大きな型についてはどうでしょう。排他的論理和(XOR)演算が使えますね。
struct Point: Hashable {
var x, y: Double
var hashValue: Int {
return x.hashValue ^ y.hashValue
}
}
もしxとyが同じ値だったら、ハッシュは0になってしまいます。データセットに多数の同じ値がある場合、0で多くの衝突が発生することを意味します。用意したデータセットによっては、これはあまり良くないハッシュアルゴリズムかもしれません。なので、これを偽造しているのを実際のコードで見てきました。このようにオブジェクトをStringとしてレンダリングし、そのStringに対してハッシュ値を求めます。
struct Point: Hashable {
var x, y: Double
var hashValue: Int {
return "\(x),\(y)".hashValue
}
}
これはHashableプロトコルの要求を満たしていますが、ヒープアロケーションが必要となるためよくありませんし、もっと早くしたいと思っています。ヒープアロケーションはコストが高いです。もっとよくできます。
protocol HashAlgorithm {
init() // 1
mutating func consume(bytes:) // 2
var finalValue: Int // 3
}
ハッシュアルゴリズムは、この基本的な形で初期化され、バイトを消費し、最終的な値を吐き出すということが分かります。
だから、ハッシュアルゴリズムの作者はバイトを消費する必要があります。これはあまりコードにはよくありませんが、実際にはUnsafeなコードはありません。
struct FVN1AHash: HashAlgorithm {
private var hash: UInt64 = 0xcbf29ce484222325
private let prime: UInt64 = 0x100000001b3
mutating func consume<S: Sequence>(bytes: S)
where S.Iterator.Element == UInt8 {
for byte in bytes {
hash = (hash ^ UInt64(byte)) &* prime
}
}
var finalValue: Int {
return Int(truncatingBitPattern: hash)
}
}
クライアントサイドには、このFVN1Aのハッシュ値があります。
var hashValue: Int {
var hash = FVN1AHash()
hash.consume(x)
hash.consume(y)
return hash.finalValue
}
スタック上でそれを宣言し、xとyを消費し、ハッシュ値を吐き出します。そして、ハッシュアルゴリズムで隠されているのは、アクセスしているバイトまたは型へのアクセスを提供するこれらのプロトコル拡張です。Unsafeなコードを使用してバイトにアクセスするので、隠されてしまいます。
extension HashAlgorithm {
mutating func consume<I: Integer>(_ value: I) {
var temp = value
withUnsafeBytes(of: &temp) { rawBufferPointer in
consume(bytes: rawBufferPointer)
}
}
}
これがポイントです。SwiftはこのようなUnsafeなアクセスを提供しますが、このような方法でAPIを構築することで、Unsafeなコードを分離して、使いやすいAPIを得ることができます。この話、またはこの話の資料は、私が取り組んでいるRay Wenderlichのビデオチュートリアルシリーズから来ています。このビデオチュートリアルシリーズは、うまくいけば今年の後半に出ます。がんばってみなさんに見ていただけるようにしたいと思います。ありがとうございました!
About the content
2017年3月のtry! Swift Tokyoの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。