https://www.youtube.com/watch?v=eCKSG8QMIEo
この勉強会では引き続き Swift.org の About Swift
から、Objective-C から比べた Swift の 追加機能
について見ていってます。その中の今回は最後の項目 defer
についてじっくり見ていきます。どうぞよろしくお願いいたしますね。
————————————————————————— 熊谷さんのやさしい Swift 勉強会 #8
00:00 開始 00:39 defer 03:06 好きなタイミングで defer を使う 03:54 後回しにしたい処理 10:55 必ずしも実行したくないとき 13:51 対になる処理を近くに書ける 16:15 質疑応答 17:24 デイニシャライザー 18:46 周辺スコープ 22:19 複数の defer を使う 27:09 defer 文の実行時点で予約される 35:06 アクティビティーインジケーター 37:32 セマフォ 39:03 戻り値を返してから更新 43:10 安全性 44:28 安全ではない部類の仕様を排除 45:26 意図を簡単に示せる 46:46 フォールスルーへの対応 49:58 シンプルなキーワードで定義 52:35 言語表記のチューニング 57:00 次回の展望 —————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #8
では、前回の続きとして、SwiftのDefer
についておさらいしていきましょう。
Defer
は、あらかじめ処理を予約しておいて、最後にその予約しておいた処理を実行するという機能です。言葉だけでは分かりにくいので、プレイグラウンドで実際にコードを見てみましょう。
例えば、以下のようなメソッドがあります。
func exampleMethod() {
print("Action")
defer {
print("Defer")
}
print("Another Action")
}
このメソッドを実行すると、以下のような出力が得られます。
Action
Another Action
Defer
ここで重要なのは、defer
ブロックに記述した処理は、そのスコープを抜けるタイミングで実行されるという点です。つまり、スコープを抜けるときにdefer
ブロックが実行されます。
もう少し複雑な例を見てみましょう。以下のコードを考えてみてください。
func multipleDeferExample() {
print("Action A")
defer {
print("Defer A")
}
print("Action B")
defer {
print("Defer B")
}
print("Action C")
}
このメソッドを実行すると、以下の出力が得られます。
Action A
Action B
Action C
Defer B
Defer A
defer
ブロックは、逆順に実行されるという特徴を持っています。今回は、defer
の順序を理解するために役立つ例です。
では、defer
が具体的にどのような場合に便利かについても確認しておきましょう。例えば、ファイルハンドルを開いた際に必ず閉じる処理を行いたいとします。このような場合、defer
を使うと確実に後始末の処理を実行することができます。
次の例を見てみましょう。
func fileHandlingExample() {
let fileHandle = openFile("example.txt")
defer {
closeFile(fileHandle)
}
// ファイルに対する操作を実行
writeFile(fileHandle, content: "Hello, World!")
// ファイルを正常に閉じる
}
このように、defer
を使うことで、スコープを抜ける際に必ずファイルハンドルを閉じる処理を実行できます。
まとめると、defer
は後始末の処理を確実に実行するために非常に便利な機能です。スコープを抜ける際に必ず実行したい処理がある場合、それをdefer
ブロックに記述しておくことでコードの安全性が高まります。
この後は、具体的な使用例に即した細かな動きを見ていく予定です。続きも楽しみにしていてください。 そうですね、例えばファイル操作の例を考えると、ファイルをオープンしたら必ずクローズしなければならないということが基本です。しかし、コードの中でそのクローズ処理を忘れてしまうということがよくあります。そのような場合、Swiftのデファー文を使うと便利です。
例えば、ファイルをオープンして、何か処理を行った後に必ずクローズ処理を行いたいとします。このとき、デファーを使用することで、処理が終了する際に必ずクローズが行われるようにできます。
以下がそのサンプルコードです:
if let fileHandle = FileHandle(forReadingAtPath: "example.txt") {
defer {
fileHandle.closeFile()
}
// ファイルの読み込み処理
while let line = fileHandle.readLine() {
// 何らかの処理を行う
}
}
ここでdefer
ブロックを使うことで、スコープを抜ける際に必ずfileHandle.closeFile()
が呼び出されることが保証されます。関数が正常に終了した場合も、エラーで途中退出した場合も、このdefer
は必ず実行されます。
例えば、何らかの条件で処理を中断する場合も考えてみましょう。以下のようになります:
if let fileHandle = FileHandle(forReadingAtPath: "example.txt") {
defer {
fileHandle.closeFile()
}
while let line = fileHandle.readLine() {
// 何らかの処理を行う
if someCondition {
break
}
}
}
このように、defer
を使うことで、ファイルハンドルを確実にクローズできるようになります。
更に複雑なロジックが必要な場合でも、defer
を使うことでリソースの後始末を漏れなく行うことができて、コードの可読性も保てます。デファーを使って、ファイルオープンとクローズの処理を近くにまとめて書けるため、関連するコードを見やすく保つことができるのです。
個人の価値観もあるかもしれませんが、このように対になっている処理を明確にすることで、コードのメンテナンス性が向上します。デファーの性質をよく理解して、それを上手く活用していければ、不具合の混入を減らすことができると思います。 デファーは一般的に「処理を遅らせる」とか「スコープを抜けるまで後回し」と言われることが多いです。予約しておいて最後に動かす、といった使い方が特徴ですね。具体的には、カメラセッションやデータベーストランザクションの開始とコミットのような、必ず対になって動かすものなどで使われることが多いです。
Swiftでは、デイニシャライザーも似たような感覚で使えますね。デイニシャライザーはクラスにだけ用意されていて、イニシャライザーで何かの処理をして、インスタンスが解放されるときに後始末をすることができます。これはローカルスコープ版のデファーと似ているので、オブジェクト指向でイニシャライザーとデイニシャライザーを理解している人なら、同じ感覚でデファーも理解できるかもしれません。
ご視聴ありがとうございました。デファーのシンプルな使い方はこんな感じです。デファーは、入れ子で使えたり複数回使えたりするので、そこで更に面白い使い方ができます。
デファーが登場するよりも前に宣言した変数や定数はデファーの中で使用できます。これはクロージャーがクロージングオーバーするのと似たような感じで、外のスコープにある変数をデファーの中で使うことができます。しかし、変数が登場する前にデファーで使うことはできません。
具体的なコード例としては、次のようなものが考えられます:
defer {
print(a)
}
let a = 10
これはa
を宣言する前にデファー内で使おうとしているので、エラーが発生します。また、複数のデファーを使うこともできます。
例えば:
func example() {
defer {
print("First defer")
}
defer {
print("Second defer")
}
print("In function")
}
この場合、出力は「In function」「Second defer」「First defer」の順になります。
デファーについてもうひとつ強調したいポイントとして、デファーのブロック内で変数を使用できるということがあります。この補足が終わりましたら、本題に移りたいと思います。 こんな感じで書くわけです。そうするとどういうふうに動くのかっていうのがポイントなんですけど、これはスタックのように動きます。スタックっていうアルゴリズムを使うことができるわけですよ。
例えば、Aに依存するリソースBを作ったり、ここでもリソースを作って、そのリソースをリリースしなければいけない時に「defer」が役立ちます。「defer」がスタックになっているおかげで、スコープを抜けた時に一番新しい「defer」が先に動きます。つまり、Aに依存しているBがAが解放される前にリリースでき、その後でAがリリースできるっていう段階を踏むことができます。
例えば、Bをリリースする際にAに依存する処理になっていた場合、これがもしスタック順ではなくキュー順、つまり入れた順になっていたとすると、Aが先に解放されてしまい、解放されたAを使ってBをリリースしようとしてしまう可能性があります。だから、このスタック順になっているのは何かと嬉しいというわけです。
次に、Bの定義の前にguard
文が入っていたりして、それを実行させたくないと思った時、defer
のBリリースはどうなるのかという疑問があります。こちらはまだ予約されていないので、そこまで処理が来ていなければ何もされずに終わります。Aだけがリリースされて綺麗に終わります。
「defer」は、このスコープ内の最後に実行されるスタックに処理を入れ込んでいるイメージです。愚直にコードを書いていくこともできます。例えば、Finalizerのようなものを作って、init
やaddFinalizer
でアクションを追加するクロージャーを作り、それを配列に入れ込んでおきます。
private var actions: [() -> Void] = []
func addFinalizer(_ action: @escaping () -> Void) {
actions.append(action)
}
// スコープが抜けたときに逆順でアクションを実行
deinit {
actions.reversed().forEach { $0() }
}
こういうコードを書いておいて、例えば、何かのスコープでlet finalizer = Finalizer()
を初期化し、finalizer.addFinalizer { print("A") }
のように追加します。そして、finalizer.addFinalizer { print("B") }
などの処理を行なってからスコープが終わると、Finalizerがリリースされ、deinit
が動いて開放される仕組みになります。
このようなことを自分でも作ることができます。RxSwiftのDisposeBag
も同じような感じで実装されているのかなと思っていましたが、確かメソッドチェーンの一番最後にdispose
のようなメソッドを書きますよね。あれもスタックにどんどん入れて最後に一気に解放する感じです。
こういった手法は非常にスタンダードで、多くのところで使われています。自分の慣れ親しんでいるFinalizerを思い浮かべてみると、「defer」はそれが言語の組み込み機能として用意されている、と捉えることができます。 もう一つ、defer
には入れ子にできる機能もあります。これについては、少々遊びのようなものかもしれませんが、defer
の中に再びdefer
を入れることもできます。これも冷静に考えると簡単です。ただし、簡単と混乱しないというのは別の話です。
例えば、以下のように書いたとします:
print(0)
defer {
print(4)
defer {
print(2)
}
print(3)
}
print(1)
このコードがどの順序で表示されるかを考えながら見てもらえると、defer
の処理の感覚が見えてくるかと思います。順序を確認しながら書くと、まず0が表示され、その後4が表示され、1が表示され、最後に3と2が表示される形になります。したがって、順序としては「0, 1, 4, 3, 2」となります。
さらに、ここに別のdefer
を追加してみます:
print(0)
defer {
print(4)
defer {
print(2)
}
print(3)
}
print(1)
defer {
print(5)
defer {
print(6)
}
}
このコードを見て順序がすぐにわかる人はなかなかいないかもしれませんが、順序としてはまず0が表示され、その後4が表示され、1が表示され、さらに5が表示されます。そして、最終的に3、2、6が表示される形になります。したがって、最終的な順序は「0, 1, 4, 5, 3, 2, 6」となります。
これくらい把握できれば、defer
については良いでしょう。どう話したか覚えていませんが、多分合っています。これでdefer
は一通り終わりですね。
次に進みましょう。次はObjective-Cと比べた追加機能についての話です。画面のここに書いてある内容が気になっていたのですが、何か質問ありますか?
「はい、どうぞどうぞ。」
「defer
についてもう一度確認したいのですが。例えば、非同期長い処理が発生する関数の中でアクティビティインディケーターのstartAnimating
を呼ぶと思いますが、その直後にdefer
でstopAnimating
と書いておけば、関数のスコープを抜けたタイミングで確実に呼ばれるので便利だと教えてもらいました。」
「そうですね、自分もそれを教えてもらって結構使っています。例えば、以下のように書くと良いでしょう。」
func someLongRunningTask() {
activityIndicator.startAnimating()
defer {
activityIndicator.stopAnimating()
}
// 長い処理
}
「これで非同期処理が終了したタイミングでインディケーターが停止するので、便利です。特に、try
やcatch
のブロック内でエラーが発生した場合でも、必ずインディケーターを停止してくれます。」
「そうですね。その通りです。defer
はマルチスレッドでも役立ちますし、セマフォやリソースの確実な解放にも使えますね。」
「そうですね、まったくその通りです。」 セマフォアっていうのはロック機構の一つで、セマフォアでリソースを管理します。この仕組みを使って「このリソースは使用中ですよ」とマークをつけた後に、そのセマフォアを開放して他の人も使えるようにする、いわばリソース共有の仕組みです。しかし、セマフォアを開放し忘れるとデッドロックなど大変なことが起こりますので注意が必要です。
確かに、ファイルハンドルを閉じるとか、そういった程度のミスであれば忘れても大したことにはならない場合が多いのですが、セマフォアは開放し忘れると本当に危険ですね。非常に便利な仕組みですが、扱いには万全を期す必要があります。
実行開始と終了がセットになるような場面では、defer
が使えるのではないかと考えることができます。例えば、ユーザーデフォルトに保存して値を返すときにもよく使うかもしれません。
また、似たようなケースとして、例えばC言語におけるインクリメント演算子++
を思い浮かべるとよいでしょう。インクリメントメソッドを作って、戻り値を返す仕組みにする際に、通常は以下のように書くかもしれません:
let result = source
source += 1
return result
しかし、defer
を使うと以下のように書くこともできます:
defer { source += 1 }
return source
このようにすると、インクリメントを呼び出した時に最初は0が取得され、次にインクリメントされた1が取れ、内部ではちゃんと2になっています。defer
の使い方として、なかなか良い例かと思いますので、ぜひ参考にしてみてください。
では、次のスライドに行きますね。
ここで紹介するのはSwiftの安全性に関する機能です。Swiftのコンセプトのうちの一つに「セーフ」というものがあり、それがこのスライドに書いてある内容に現れています。 えーと、15分ですね。ここがとても面白くて、本当にそうなの? という感じの部分がいっぱいあります。この話題は次回も含めて話していくことになりそうです。多分今週いっぱいはこの話をしている気がします。
まず、他の言語、特にC言語ベースのプログラミング言語よりも安全に設計されているのがSwiftの特徴です。ここでC言語ベースと比較する理由は、SwiftがC言語を視野に入れて作られた言語だからです。それと比べて安全性が向上しているというわけです。
安全に設計して、安全ではないコードとして一般に言われるもの、例えば変数の初期化忘れやオーバーフローチェックがされないことなどを除外しているんです。たとえば、16ビットの整数の場合、65535の次がマイナスになるかどうか、詳細は忘れてしまいましたが、オーバーフローするとゼロになるか、マイナス65536になるか。そんなふうに、全然違う値になってしまうことがあります。また、メモリ管理の解放し忘れなどもあります。そういった安全性に欠ける可能性のある仕様を除外しているんです。
Swiftの紹介文にも「安全性を高めるために言語構文を調整しました」と書いてあります。まず、この部分の話をしようと思います。他の上記の2つの安全性に関する設計についても触れていますが、安全性に欠ける部分を排除しています。それでも実際にSwiftを見ていくと、完全に排除しているわけではなく、区画を分けて存在させていて、安全ではないコードを書くことも可能です。ここをぜひ紹介したいので、次回とその次あたりでじっくり見ていこうと思います。
では、今回はあと10分ほどあるので、「言語構文をチューニングした」という一番最後の部分を見ていこうと思います。さらに、面白いコメントがありました。前回ぐらいに話したスイッチ文のフォールスルーについてです。
C言語ではbreak
を書かないと自動的に次のケース文へ処理が進んでいきますが、Swiftでは基本的にbreak
を書かないでそのケース文が終了します。そして、fallthrough
を書いたときだけ次のケース文に処理が移るという設計です。フォールスルーがイメージできない人には分かりにくいかもしれませんが、この設計も安全性を高めるための工夫です。
以前、Appleがスイッチ文のbreak
がなかったために重大なバグを引き起こした事件がありましたね。具体的な詳細は思い出せませんが、確かオブジェクティブCの頃にありました。その経験を踏まえて、Swiftではこのような安全性を考慮した設計が取り入れられたんだと思います。それで全然安全性が変わってくるんですよ。
C言語やオブジェクティブCの経験がない人にとっては...... 恐らくここら辺は想像しにくいかもしれませんね。そうですよね、C言語だと誰もが通る道ですけど、ブレイクをかけるのが普通です。でも、Swiftの方が好みです。仕様として、必要なときだけフォールスルーにすればいいというのが良いですよね。Swiftはその点で安全に設計されているというところの一つです。
今お話にあったように、昔の経験がないとピンとこないというのがありますが、新しい方は気にしなくても大丈夫です。それで、もう少し話を続けます。この「意図を簡単に示せるようにチューニングした」という点も、安全性として掲げられています。Swiftの面白いところ、お茶目なところと言ってもいいかもしれませんが、例えば変数や定数の定義をシンプルな3文字のキーワードで定義するようにしていることです。
普通に読むと、「それが言語のチューニングなのか?」と思うかもしれませんが、例えば、var
と書くことで変数を定義し、let
と書くことで定数を定義するということです。こういった仕様をわざわざ例に挙げています。他の言語、例えばC言語だと定数にしたい場合は const
と書いたり、特別な扱いをする必要がありますが、Swiftではシンプルにしています。 Cで素直に書くとこんな感じになるわけですけど、個人的には、イントとコンストイントって見た時に、イントが標準でコンストイントが何か付加属性がついているような印象を持つような気がします。これが let
と var
になっていることで、立ち位置が対等だと感じるところが個人的には好きです。安全性とは全然関係ない話になっていますけど、こうすることで理解が対等になり、平等になって、これは書き換えられない変数だからと感じる人もいると思います。余計な誤解を招かないというのも、安全性の一つだと思います。
Swiftでは、例えばセミコロンがいらなくなったりしていますよね。セミコロンがなくても意味が通じるので、ないほうが過読性が上がります。また、余計なことを考えなくて済むようになります。例えば、if
文で if(condition) { value = 10 }
なんて書くときも、一番外側の括弧はなくてもいいとか、guard
で条件を満たさなかったときに else
を加えることで、その後のブロックが条件を満たさない場合だというのが明確になったりします。他には、C言語でお馴染みの for(int i = 0; i < 100; ++i)
のような書き方も、Swiftでは for i in 0..<100
とシンプルに書けるようになっています。括弧もいらなくて、こういったシンプルな書き方を導入することで、コードを書くのがどんどん簡単になり、安全性に繋がっています。実際に、読みやすく誤解を招きにくいコードが書けるというのは、読むプログラマーや書いている本人も、うっかり勘違いしてバグを生んでしまうことが少なくなります。
こういった部分に関しては、Swiftの安全性というコンセプトが反映されているのだと思います。about Swift
を読んでもいまいち掴めなかったんですけど、きっとこういうことなんだろうと思いました。
初期のSwiftでは範囲演算子が ..<
ではなくて、逆の ..>
でしたが、明確に「ここは含まれない」というのが分かりやすくするために変更したのはいいチューニングだったと思います。
最初に見た時には、「そこを変えるのか」と思いましたけど、過去にこだわらずに良いものへ変更していく姿勢は素晴らしいです。Swiftはその姿勢がかっこいいと思います。
では、ちょうどいい時間になりましたので、今日のSwiftの勉強会はこれで終わりにして、次回はこの安全性の最初から見ていこうと思います。皆さんお疲れ様でした。ありがとうございました。