前回は、その前の話題から少し脇道に逸れる予定が、時事的な話題からさらにハッシュの話に逸れて見ていったので、今回はその前に予定していた余談「throwing function
と autoclosure
周りの挙動」を思いつくままに確かめながら、様子を伺ってみようと思います。今年最後のやさしい Swift 勉強会、どうぞよろしくお願いしますね。
今回は参加者の一般参加者を募って、ゆめみの外の人たちも訪れての開催になる見通しです。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #198
00:00 開始 00:23 なぜか手元で動作しないコード 01:47 ハッシュ関数と代数学的ハッシュ関数 02:52 ハッシュ関数の特性 06:57 暗号学的ハッシュ関数の性質 08:08 ハッシュとは何かを簡単に言うと 09:28 エラーを送出できるオートクロージャー 16:07 以前は違う挙動だった様子 20:58 オートクロージャーにエラーを返す関数の結果を渡す場合は? 22:21 オートクロージャーと rethrows 24:44 再確認してみるも、問題なさそう 27:39 クロージング ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #198
プログラムに関する話は、特に詳細に調査してきたことに基づいているので、きちんと整理して伝えたいと思います。まず、前回の話題に戻りますと、ハッシュ関数の速度を調べるコードについて言及しました。このコードは、M1 MacBookでは正常に動作しないことがわかりました。コンソールログやエラーレポートを見ても、スレッドの特定ができない状態が続いていました。しかし、同じファイルをM1 MaxのMac Studioで実行すると正常に動作します。この問題は、プレイグラウンド特有のものである可能性が高いです。
話をハッシュ関数に戻しますが、前回の勉強会ではハッシュ関数の用途について、時間が足りずに簡単に説明する形に終わってしまいました。その後、いくつか興味深い話題、特に「暗号学的ハッシュ関数」が挙がっていました。これについても少し調べてみました。Wikipediaでも暗号学的ハッシュについて詳細に説明されていることが分かりました。
一般的なハッシュ関数に話を戻すと、「良いハッシュ関数」として以下の特性が求められます:
- 低コストであること。
- 決定性があること。入力が同じであればハッシュ値も必ず同じ値になる必要があります。
- 一様性。これはハッシュテーブルにおいて非常に重要で、データが均等に分散されるようにマッピングを行うことです。
- 可変な値域。ハッシュ値の範囲がデータのサイズに応じて変化することもあります。
加えて、ハッシュテーブルを拡張するときには、ハッシュ関数が2つのパラメータ(データと生成可能なハッシュ値の数)を入力として持つ必要があります。
その他に、以下の点も重要です:
- 同じ意味を持つと見なされるデータには、同じハッシュ値を生成する必要があること。
- 連続性。類似するデータがなるべく連続してハッシュされることで、検索やデータの取り出しが効率的になることです。
こういった特徴を持つことで、ハッシュ関数はさまざまな用途に適用できます。
そして、暗号学的ハッシュ関数に話を戻しますと、これにはさらに厳しい条件が課されます。例えば:
- ハッシュ値から元のデータを推測することが事実上不可能であること。
- 同じハッシュ値を持つ異なるメッセージのペアを見つけることがほぼ不可能であること。
- メッセージを少し書き換えただけでハッシュ値が大幅に変わること。
パスワード認証やMD5などのメッセージダイジェストの用途に適しています。
これらの特性を理解することで、それぞれのハッシュ関数がどのような場面でどのように使用されるべきかが明確になると思います。 このように、プログラミングにはさまざまな特徴や物が存在しますね。私はあまり意識していなかったので、新鮮に感じました。言われてみると確かにそうで、少しは聞いたことがある内容でした。とにかく、ハッシュ関数というものは一定の性質を持つものであり、これをさまざまな場面で応用して使うものだということです。ハッシュのメリットについては、具体的にはパスワードの保護機能などがあります。キーを知られるリスクを減らすことができるといったメリットがあります。
日常的にこのような話題が増えてきていますが、以前ハッシュの話題になったときに、私は極度の問題で全然他のことばかりに注目してしまい、検討がまるで違っていたので、こういう勘違いも面白いものだし怖いものだと感じました。そんな問題もありましたが、ハッシュの話はこの辺にしておきましょう。
さて、今日の雑談ですが、前々回に話題にしたオートクロージャーとスローイングファンクションについて見てみましょう。この話を改めて考えてみると、ゲッターに throws
をつけられるのでオートクロージャーが少しややこしくなるといった話ですが、普通のエラーを返す関数の戻り値をオートクロージャーに渡すこともあります。同じような問題かもしれませんね。でも、少しやってみましょう。
関数がありますね。これは何かします。このときイント型のバリューを取ります。これをオートクロージャーにしたい気がします。オートクロージャーでこんな感じですね。さらに、これがスローイング関数になるのですが、どうしましょうか。戻り値は今回はイント型でもいいでしょう。これでゲッターとしてバリューを返しますが、このときに、この部分がスローイングファンクションです。こういったときには try
が必要となりますが、API的にはパスしますね。
少し複雑に見えるかもしれませんが、 do
ブロックを作って、そこで try
アクションを行います。例えば、次のようにします。
do {
let value = try someFunction()
print("Inside: \\(value)")
} catch {
print("Catch: \\(error)")
}
これで、 try
のエラーをキャッチしていますね。そのキャッチ部分で print
を行います。このように、 do
ブロックを使ってエラーハンドリングができます。
例えば構造体でプロパティがあって、そのプロパティのゲッターに throws
を付けて、そこからエラーをスローさせるとしましょう。ただし、少しエラー処理の例を示しましょう。
struct MyStruct {
var value: Int
var myProperty: Int {
get throws {
guard value >= 0 else {
throw MyError.valueOutOfRange
}
return value
}
}
}
do {
let myStruct = MyStruct(value: -1)
let result = try myStruct.myProperty
print("Result: \\(result)")
} catch {
print("Error: \\(error)")
}
このように、プロパティのゲッターでエラーをスローさせることができます。先ほどの例のように do
ブロックでエラーハンドリングを行います。
これで、オートクロージャーでスローイング関数をまとめて扱うといったケースでも、適切にエラーハンドリングを行うことが可能です。しかし、最終的にどのように処理されるかは実装によりますので、慎重に行う必要があります。
これで今日の話は終わりにしますが、次回はさらに深い内容に触れてみましょう。 そうですね。Swiftのエラーハンドリングについて話を進めていきましょう。ここでは、スローイング関数の使用方法と、try
、try?
、try!
などのエラーハンドリングの違いについて確認しています。
まず、スローイング関数に関する具体例ですが、例えばこんな使い方ができます。
func canThrowErrors() throws -> String {
// なんらかのエラーが発生する可能性のある処理
return "Success"
}
この関数を呼び出すときには、その呼び出し元でエラーハンドリングを行う必要があります。試しに try
キーワードを使ってみましょう。
do {
let result = try canThrowErrors()
print(result)
} catch {
print("Error occurred: \\(error)")
}
次に、try?
を使った場合の例を見てみましょう。こちらは、エラーが発生した際に nil
を返す形式です。
if let result = try? canThrowErrors() {
print(result)
} else {
print("Error occurred")
}
最後に、try!
を使った場合です。ここでは、エラーが発生しないと確信している場合に使用しますが、もしエラーが発生するとクラッシュします。
let result = try! canThrowErrors()
print(result)
スローイング関数に関しては、エラーハンドリングが正しく行われないとビルドエラーが発生することがあります。たとえば、スローイング関数内でエラーを処理せずに結果を参照すると、ビルドエラーが起こります。
各バージョンの違いについても触れています。古いバージョンのSwiftでは、スローイング関数に関するエラーハンドリングが異なっていたようです。バージョンごとの詳細な違いを確認するために、実際にコードを実行し、エラーメッセージを確認しました。
このように、エラーハンドリングの方法を理解すると、Swiftでの例外処理がよりスムーズに行えます。オートクロージャーについても言及されていましたが、基本的にオートクロージャーがエラーをスローしない場合は、通常のクロージャーとして扱われるだけで特に問題はありません。
いかがでしょうか。これでSwiftのエラーハンドリングについて少しは理解が深まったかと思います。何か追加で質問があれば、ぜひ教えてください。 ちょっと混乱してきたので、やってみますか。ベッダーではなくて、ファンクションとしてですね。例えば、function getValue()
でInt
を返すとします。これがスローイングファンクションになるとしたら、こんな感じになりますね。この関数を投げるとして、getValue()
を渡してみましょう。
このようにすると、インサイドでどう振る舞うのかを確認してみます。スローズを使ってもやはりリスローズもできるんですよね。ただし、この関数がスローできるエラーしか許されない点は重要です。途中でリスローズの挙動が変わった記憶があるものの、よく分かっていません。
独自のエラーを返そうとすると、これはスローズの関数なのでエラーになりますね。ここでは、リスローズが全く使えません。シンプルに書けませんね。
次に、オートクロージャーとスローズ、リスローズについて考えてみます。これらを意識して書くと、もっと適切なコードになるかもしれません。記憶に残りやすくなります。面白いですね。ただ、正面的な動きだけではなく、横に出るようなときを考えます。
横だったとき、つまりインサイドになってエラーをアウトサイドにする場合ですね。このキャッチでスローする状況でも同じです。要は、スローしたときに、エラーがかかってこなくなってしまいます。これがスローイングじゃないときにはタトゥーブロックが来たくないという話を予約できるわけですね。
リスローズの恩恵が得られるだけですが、オートクロージャーでも得られるのは面白いです。安心して使えるというところがポイントですね。オートクロージャーとスローズを組み合わせても、直感的にコードを書くことに問題はありません。プレイグラウンドで確認したところ、直感通りの挙動を示しました。
さて、次にgetValue
のエラー処理について考えます。これをスローズの関数として以下のように変えます。
func getValue() throws -> Int {
// 実装
}
これでスローズのエラーが出なければOKです。ここで、ゲッターのエラーが出ているので、func getValue() throws -> Int
に変えてみます。するとスローズで返せるようになりますが、何か気になる点がありますね。
これが通れば、問題ないはずです。インサイドで「インサイド」と表示されるようになりましたが、「ヘッドスローズ」って何でしょうね。もう一度実行してみます。
コードがバグっているのか、もしくは消し忘れていた部分があるのかもしれません。何度か実行してみて、確かにまだ残っているようなら、リロードしてみます。問題ない範囲で、もう少し検討が必要でしょう。
とまあ、こんな感じですけど、ここで問題が起こってもSwift 5.4の話なので、昔の話として受け止めてください。ご静聴ありがとうございました。