https://www.youtube.com/watch?v=T6vnuRplrIY
今回は再び A Swift Tour
に舞い戻りまして、制御構文の続きのお話「for-in
」のところから眺めていこうと思います。これまでにも何度も出てきた構文なので、再確認のつもりでじっくり復習してみましょう。それが終わったら続いて while
を見ていく予定です。どうぞよろしくお願いしますね。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #38
00:00 開始 00:36 for ⋯ in 構文 00:58 コレクションとイテレーション 01:56 辞書型と配列型を繰り返し処理する 04:08 辞書を for ⋯ in で繰り返すときの特徴 04:54 素数、フィボナッチ数、平方数 06:38 辞書型に対して for ⋯ in を使ってみる 06:47 辞書型の順序は不定 08:09 支所型には順序がない? 09:30 ハッシュテーブル 11:46 ハッシュとコリジョン 13:21 ハッシュ値の生成について 13:59 Hasher.combine(_:) 16:25 ハッシュテーブルの仕組み 21:31 ハッシュ値は分散させるのが大切 22:40 ハッシュの特徴 23:51 hash と hashValue 27:16 ある値から同じハッシュ値が得られることまでは保証しない 27:42 ハッシュ値とは 28:05 ハッシュを用いたパスワード管理 29:26 ソルトと乱数シード 30:53 質疑応答 31:39 暗号化とハッシュ化 32:56 ハッシュ値を保存しておくのは間違い 33:37 SipHash とハッシュフラッディング攻撃 36:23 ハッシュ値と辞書型の並び順を見てみる 38:18 配列型は順番通りに取り出せる 39:42 配列型とインデックスアクセス 40:25 シーケンスとイテレーション 44:26 無限ループの理由を探る 47:04 自分の型を for ⋯ in に対応させる 47:47 SWIFT_DETERMINISTIC_HASHING 48:31 クロージングと次回の展望 —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #38
はい、じゃあ勉強会を始めていきますね。今回は今までとは違って、資料をSwiftツアーのものに戻して、制御構文の続きを見ていこうと思います。具体的には、スイッチ文の続きからです。
まず、for-in
構文についてです。これは非常におなじみの機能で、これまでにもたくさん出てきましたが、もう一度この資料を基に見ていくと、なかなか面白そうなのでゆっくりとやっていきましょう。
for-in
構文はざっくり言うとコレクションをイテレーションするためのものです。コレクションというのは、複数の要素で構成されている値のことです。それに対して、順番に要素を取り出し、各要素ごとに処理ブロック内の処理を実行させる文です。重要なポイントがいくつかありますので、その辺りをゆっくり見ていこうと思います。
まず、画面上に表示されているソースコードを見ていきます。このinterestingNumbers
というのがコレクションです。具体的には辞書で、キーとバリューの対になった値のコレクションです。例えば、"Prime"
が素数、"Fibonacci"
がフィボナッチ数列、"Square"
が平方数という風に定義されています。
let interestingNumbers = [
"Prime": [2, 3, 5, 7, 11, 13, 17, 19],
"Fibonacci": [1, 1, 2, 3, 5, 8, 13],
"Square": [1, 4, 9, 16, 25, 36]
]
これに加えて、largest
という変数を定義しています。これは後で計算に使います。
var largest = 0
そして、for-in
構文は以下のように記述します。
for (kind, numbers) in interestingNumbers {
for number in numbers {
if number > largest {
largest = number
}
}
}
for-in
の前にあるのが要素を受け取る変数(シンボル)で、in
の右側にコレクションを書きます。そうすると、その処理ブロックがコレクションのすべての要素に対して実行されます。この例では、辞書interestingNumbers
のキーとバリューに対してfor-in
が使われています。
キーと値の組を使って辞書内の配列を反復処理できるのは、辞書型に対してfor-in
を使っているからです。なお、辞書は順序を持たないコレクションなので、キーと値の組は任意の順序で反復処理されます。
具体的にやってみると分かりやすいので、次にinterestingNumbers
を使ってみましょう。例えば、キーだけを取ってプリントする場合、以下のように書けます。
for (kind, _) in interestingNumbers {
print(kind)
}
このようにすると、キーの順番が順不同で出力されることがわかります。例えば、「Fibonacci」、「Square」、「Prime」といった具合です。順序が固定されていない点が、プレゼンの説明と一致しています。
興味深いのは、各種類に応じた特定の数列についてです。"Prime"
は素数で、1とその数字自体以外では割れない数です。"Fibonacci"
はフィボナッチ数列で、1、1、2、3、5のように進む数列です。"Square"
は平方数で、1、4、9、16のように進みます。
このような数列に対して、反復処理を行うときにfor-in
構文が役立ちます。これが基本的な使い方ですが、さらに深掘りしてさまざまなパターンを探っていきたいと思います。まずは、ここまでの説明を基にして実際にコードを動かしてみましょう。 これをもう一回実行すると、また結果が変わるんじゃなかったでしょうか。Swiftではそのような動きが見られます。例えば、平方数、素数、フィボナッチ数のように変化します。
1で割れるのは当然のこととして、その数字で割れるのも当然ですが、それ以外の数字で割り算できない特徴があるはずです。フィボナッチ数列というのは、多くの人にとってイメージしやすいでしょう。最初の数が1で、その次も1、そして次の数は前の2つの数を足したものになります。このようにして数列が続きます。具体的には「1, 1, 2, 3, 5, 8, 13...」のようになります。この数列には数学的な特徴が多く、興味深いですね。
平方数は各数の自乗、つまり1の自乗、2の自乗、3の自乗といった数列になります。個人的には、平方数はそれほど興味深く感じないですが、例として挙げられることが多いです。まあ、これも余談ですね。
次に、辞書(Dictionary)のループについて説明します。例えば、以下のようにキーを取り出してみます。
let 数字の特徴 = ["フィボナッチ": [1, 1, 2, 3], "スクエア": [1, 4, 9, 16], "素数": [2, 3, 5, 7]]
for (キー, _) in 数字の特徴 {
print(キー)
}
このように、キーのみを取り出して表示します。実行するたびに、例えば「フィボナッチ、スクエア、素数」という順番ではなく、順番が変わることがあります。これは、Swiftの辞書が順序を保証しないためです。
もう一回実行すると、再び順番が変わることがあります。順序が定まっていないので、ランダムに見えるのです。この動作についてさらに掘り下げてみましょう。
Dictionaryのキーと値のペアはキーがHashable
である必要があります。これは辞書が内部でキーをハッシュ値で管理しているためです。ハッシュテーブルという仕組みを用いることで、メモリ領域にあるキーに対して迅速にアクセスできます。
ハッシュ値は整数型で表現されることが多く、どんなに複雑なキーでも、一旦ハッシュ値に変換されます。これにより、キーに対応する値を高速に探し出すことができるのです。ハッシュテーブルを使うことで、アクセス速度が速くなります。しかし、キーが複雑な場合には「衝突」(collision)が起こることがあります。
具体的に見てみると、次のような構造があります。
struct データ {
let a: Int
let b: Int
}
このように、a
およびb
がともに整数型であれば、お互いにハッシュ値を持ち合わせています。ハッシュ値の計算は効率的であり、辞書の高速アクセスを実現できるのです。
このように、辞書に順序がないということは、順番に取り出すこともできないでしょう。順序がないのと順序があるという主張も、どの観点に注目するかによって異なるため、論争になることもありますが、いずれにせよSwiftの辞書におけるハッシュテーブルの理解は重要なポイントです。 なので、そうするとハッシュ値がInt型に収まらなくなってくると、複数のインスタンスで同じハッシュ値を出さなければならない状況が出てきます。ハッシュ可能 (Hashable
) に準拠させて、それで…なんだっけ、ハッシャだ。ハッシャ…ハッシュインターフェースか。ちょっとハッシュテーブルの組み方がややこしくなったんで分かりにくいかもしれないですが、これを組んであげると。
リターンゼロとかね。いや、リターンゼロじゃないや。ハッシャーに入れてあげるのか。ハッシャーってインスタンス作れるんだっけ? ちゃんとやったことないですね。最近はforce
で書かなくなりましたね。
IntがHashable
だから合成…なんでしたっけ? そうそうそう、Aのハッシャー。ハッシュインターフェースであるハッシャー。だからハッシュってどう合成するんでしたっけ? 普通にやっていた気がするけど、忘れちゃった。そのままハッシャーにコンバインするはずです。ハッシャーにコンバイン…いくつでもらっているハッシャーがinout
なので、そうかなるほど、こういうのがあるのね。知らなかったです。なるほど、なるほど。
これでAのハッシュバリューを入れちゃえばいいんですか? 違うかな。Aを直接渡せなかったでしたっけ? Hashable
できるんだ。なので、2つともAもBもHashable
なので、この実装を省略しても同じようなことをしてくれます。なるほど、なるほど。あ、じゃあ吉野に作ってくれるのね、ハッシュキーをね。プロトコル構成じゃなくて、Equatable
が対応したタイミングで。なるほど、書いてあげてもこんな感じの雰囲気ですね。省略的で、ちょっと今回説明のために書こうかな。
これをそのまま、特定の条件からのプロトコル適用、コンディショナルコンフォーマンスの目的実装とか言われるんですね。なるほどね。だからこんなのをわざわざ書かなくていいんだ。昔はIntでXOR取ったりとか、回せば確かになるとかいろいろありましたよね。昔は自分でBターンとかやって、Aのハッシュバリューと…。例えばORでも取ってあげればいいのかな、例えばね。こういう風にして。でもORじゃダメだ、255に近づいていっちゃう。フルビット1に繋がっちゃうから。昔はそういった苦労があったんです。
コンバインは便利ですね。便利というか、何やってるのかちょっとわからないですけど。ハッシュが重なると大変というか、ハッシュテーブルっていうのは要は配列みたいなもので、ハッシュ値が0だった場合には0番目に入れるとか、1番目だったらそこに入れるみたいな、そういう風な仕組みになっています。
最も効率のいいハッシュテーブルは、1番最後がIntの最大値です。だからUnsignedInt
としてInt.min
からInt.max
までのハッシュテーブルを用意してあげると、ハッシュが重なるっていう問題はあるとはいえ、これがIntの表現範囲内に収まるデータ型であれば、絶対にInt.min
からInt.max
の配列を作っておけば衝突しないんです。ただし、ハッシュテーブルの何番目は使われていないとか無駄が多くなってくるので、昔的な考え方で言うと、上位を取ってですね、例えば256までのハッシュテーブルにして、うまく割り当てていく。
例えば、256よりも表現が多かったとき、ちょっと厳密には違うんですけど、多かったときにはどうしてもこの領域、例えば0だったら、ハッシュ値が0になった値がこうやって入っていかないといけないわけですね、重複したとき。このときの方法がいろいろあったと思うんですけど、例えば配列で持たせたとして、ハッシュテーブル1つには例えば1個しか入ってなかったみたいなときに、ディクショナリーのキーの値を取り出すときに、このキーのハッシュバリューが1だったときには自動的にこれがXって決まるから速攻取れる。
ただ、ハッシュバリューが0だったときにはAかBかCかDかどれか分からないから、この4つの中からK2を探すみたいな感じでやっと正確な値Bが取り出せるみたいな。そういう細かい動きを、ハッシュテーブルはそういう作りになっていて。 大事なポイントとして、全体を普通に配列で管理した場合、キーをダイレクトに扱うと全てのキーと比較する必要があり、その比較処理が重たい場合、アクセスに非常に時間がかかってしまいます。そこで、ハッシュ値に変換し、そのハッシュ値によって最も効率の良い比較が行えるようにします。さらに、ハッシュテーブルの範囲を制限することによって、ハッシュテーブル全体からの検索性能を向上させることができます。ハッシュ値がぶつかったときには、そのぶつかったもの同士で比較を行うので、全てを比較するよりも高速です。さらに、これを二分探索などで工夫すれば、もっと検索が早くなるでしょう。
ここで重要なのは、いかにハッシュ値を分散させるかという点です。複数のキーが同じハッシュ値になるとアクセス速度が落ちるため、いかに分散させるかが数学者の腕の見せどころとなります。多くの工夫がされているはずです。たとえば、ハッシャーやXORを使う方法もあります。XORを用いた場合、ビットシフトでビットを回転させながらXORすることで、より安全になるという手法もあります。
Swiftにおいても、そのハッシュアルゴリズムはしっかりしていますが、ハッシャーはシード値を使っており、その度に値が変わるため、起動ごとに異なるハッシュ値が生成されます。これは、MD5などのアルゴリズムとは異なります。Swiftでは「ハッシュ」と「ハッシュバリュー」が異なる概念で、ハッシュは確実に1位制が保証されています。
Swiftの場合、標準ライブラリではハッシュに関数が使われています。ハッシュアルゴリズムは「CryptoKit」に含まれていることがあり、これを使えば高いセキュリティを保つことができます。また、ハッシュバリューはデータの一意性を保証するものではありません。起動ごとにハッシュ値が変化するため、データベースにおけるプライマリキーとしては適していません。
例えば、Int
型にはハッシュバリューがないかもしれませんが、String
型にはあります。これは、Int
型は自分自身がハッシュとして使えるからかもしれません。Swift 3や4の頃は同じハッシュ値を返していましたが、シード値(ソルト)が与えられていなかったため、現在は異なる値を返すようになっています。
ハッシュという概念は、ある値から元に戻せない別の値に符号化することを意味します。これは重要なセキュリティ原則であり、逆方向に元に戻せないことがハッシュの本質です。 だから、ハッシュ値から元の値は戻せないんですよ。ある値からハッシュ値を作ることはできますが、ハッシュ値から元の値に戻すことはできません。それをうまく活用しているのが、パスワードをデータベースに保存するときの方法です。
ハッシュ値は、ある値から必ず同じハッシュ値が得られるという約束のハッシュアルゴリズムを使っています。入力されたパスワードをハッシュ化してデータベースに保存しておきます。そして、ログイン画面で入力されたパスワードを再度ハッシュ化して照らし合わせるのです。そうすると、元のパスワードを知っているのはユーザーだけで、認証がハッシュ値だけで行われる仕組みが作られます。
これはあくまでもハッシュの応用です。ハッシュは今言ったように、ある値から別の値を得る仕組みです。基本的に同じアルゴリズムを使うと同じ値が得られますが、その時に元の値と一緒にソルトと呼ばれる値を添えることでセキュリティを向上させます。ソルトによって、スタート地点が異なるため、結果的にランダムシードのような役割を果たします。
それによって、ハッシュ値は一意であるものの、ソルトが違ければ全然異なる値が得られるため、元の値が何だったかを相手に推測されにくくなります。ハッシュをセキュリティに応用するかどうかはともかく、この仕組みは非常に応用力が高いもので、SHA-1やSHA-256、MD5などのハッシュアルゴリズムが活躍しています。
現在、コメントが盛り上がっていますね。そう、ハッシュは難しい部類に入るので、じっくり勉強しないと難しいところでもあります。また、普段知らなくても何とでもなる部分もあります。特にインターネット黎明期には話題に上がっていたとしても、基本情報技術者試験などでは当たり前のように課題として出る可能性が高いです。ですので、試験を通じて覚えるのも一つの方法です。
暗号化とハッシュ化はまた少し違います。非可逆性が大事です。暗号化は元に戻せるという可逆性を持ちますが、ハッシュは非可逆(戻せない)です。ハッシュの用途として、ハッシュテーブルに使うことが多いです。ハッシュはセキュリティのためにも使いますが、基本的にはハッシュテーブルのために使われます。
セキュリティを気にして毎回異なるハッシュが取れるようにする変更が入ったのも興味深いですね。基本的にハッシュ値から元の値を戻せないので、そのハッシュ値をデータベースなどに保存しておくのはプログラム的に間違いです。そのような実装をしている場合は直すべきです。
Swiftのハッシュアルゴリズムには SipHash
が使われているそうです。これはおそらくDoS対策の一環としてSwiftが導入したものです。例として、Int8
型であれば可能性のある値は -128
から 127
までです。それを一つずつハッシュアルゴリズムに当てはめていけば、保存されている Int8
のハッシュ値と一致するものが必ず見つかります。 そうすると元の値は3だったんじゃないか、みたいな感じで元の値を本来は逆に戻せないんですが、層当たりをかけることで見つけていく、という攻撃を意識しているのかなと思います。Swiftでもハッシュフラッシング攻撃について考えられているのですね。個人的にはそこまで乱数を使って毎回違う値を出さなくてもよかったかなという気もします。もちろん、保存するためのハッシュではないですけどね。しかし、アルゴリズム自体が意外と早かったりするので、それはそれで良いのかなと思います。
例えば、ハッシュアルゴリズムには色々な速度のものがあると思うので、安全性も高めることができるなら、その乱数を活用することで新たな使い道が増える可能性もあります。例えば、安心して符号化できるハッシュ値を取得できるなどです。
さて、このハッシュ値を使ってディクショナリーが並べられている話をしていました。必ずしもハッシュテーブルが順番になるわけではないのですが、どうなっているのかを見てみましょう。ハッシュバリューでプリントするとこのように(例: ランダムな数字)になっています。ですが、必ずしも順番ではありません。例えば、-18と3のハッシュ値が一部では順番通りになっていたものの、全てのケースでそうではありません。ハッシュテーブルは内部的に保存場所が再配置されることがあるので、順番は基本的には信用しない方が良いです。
次に、配列について話を進めます。配列は順番どおりに保存される仕様になっているため、順番どおりに要素を取り出すことができます。たとえば、numbers
という配列を取り出してフォーインループで回します。
let numbers = [1, 2, 3, 4, 5]
for number in numbers {
print(number)
}
このようにすることで、配列の要素を順番に取り出すことができます。配列(Array)はインデックスでアクセス可能なコレクションで、そのインデックスは順番にたどることができます。これで配列の使用方法について理解できたと思います。
今後はイテレーションの話も進めたいと思います。例えば、配列として値が values
という名前で定義されていた場合、それをフォーインループで回すというのはイテレーションを行う具体的な動きです。
let values = [1, 2, 3, 4, 5]
for value in values {
print(value)
}
配列はコレクション(Collection)ですが、コレクションは先頭から順番に要素を取り出すことができるシーケンス(Sequence)という特徴も持っています。シーケンスは先頭から順番に値がある限り取り出せるという特徴を持っています。
このようにSwiftの配列やコレクションについて学んでいけば、ソフトウェア開発においてデータの操作がより理解しやすくなると思います。 このイテレーターというのがシーケンスのイテレーターです。これは順序立てて持たれている要素を先頭から取ってくるという役割を持っています。まず、values.makeIterator()
こうやって先頭から要素を取り出せるイテレーターを取得してみます。
次に、while
キープを使ってみましょう。var value = iterator.next()
こう書けるかどうか、確認してみます。いつも書いているか書いていないか、改めて書くと分からなくなることもありますね。でも、前回書いてた気がします。本当に、忘れちゃうんですね。while
どうでしたっけ、みたいな感じです。
こうしてあげると、順番通りに要素を取得できるのかな。int
のオプショナルになっちゃっているので、オプショナルパターンを使わないといけない感じかな。これで、ちょっとプレイグラウンドがちゃんと動いていないような気がしますが、コンパイルエラーはなくなっているので、多分大丈夫でしょう。
イテレーターのnext
というメソッドを呼ぶと、現在位置がイテレーターの中にプロパティとして持たれていて、その現在位置の値を取り出して次の位置を指すようにします。自分の状態を変えてあげて、取得した値を返してくれる感じで動いています。その値をwhile
のパターンマッチングでvalue
に受けて、print
するというわけです。説明するとややこしいけど、ゆっくり分解していけば理解できると思います。
無限ループになってないですかね?自分のプレイグラウンドだと無限になる場合もありそうですが、while
は大丈夫な気がします。Optional
でnil
の時でも入っちゃうので、その場合にはOptional
パターンを使わないといけません。nil
になったら抜けることを期待しています。iterator
が1, 2, 3, 4を返し終わって、nil
を返す感じです。4まで出ましたね。ちゃんと抜けてると思います。
無限ループされてる場合、そのコードが止まってないかもしれないですね。長く放置していると、そういう状況になるかもしれないです。とりあえずイテレーターを使ってコレクションを回す方法はこんな感じです。
自分でfor-in
に対応させたい場合には、シーケンスプロトコルに準拠させて、それでmakeIterator
を実装すれば、自動的にSwiftでもfor-in
構文で自作の型が使えるようになります。これでfor-in
構文の理解はほぼ完璧でしょう。
今日はこんな感じで終わろうと思いますが、最後に面白い話をひとつ。SwiftにはDeterministic Hashingをセットするコンパイルフラグ、ランタイムシードを無効にするオプションがあります。誰が使うのかはわかりませんが、選択肢として存在するのはいいことですね。
ハッシュの概念を知っておくと、ディクショナリーに限らずいろんな分野で基礎として使える技術なので、時間がある時にでもWikipediaなどで調べてみることをおすすめします。
では、今日はこれで終わりにします。お疲れ様でした。ありがとうございました。