https://www.youtube.com/watch?v=wqQQ_nuOnlU
今回は A Swift Tour
の「制御構文」の最後のところ、while
と 範囲を使った繰り返し
について眺めていこうと思います。それが終わるとクロージャーの話に入っていくことになるのですけれど、今回はそこまで行くかどうか、ひとまずは繰返処理の話を終わらせるのを目標に進めてみることにしますね。どうぞよろしくお願いします。
———————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #39
00:00 開始 00:33 while 構文と repeat-while 構文 02:20 条件を最後に判定する場面 04:37 while 06:18 removeFirst, popLast, popFirst 06:56 型パラメーターの推論 08:05 プッシュとポップ 10:33 談笑タイム 11:04 ArraySlice の popFirst の挙動 12:36 while 文の基本的な書き方 13:29 while 文の外側に条件式で使う変数が必要 14:40 for 文で終了判定に使った値がその後も有効かどうか 16:09 repeat ⋯ while 17:55 repeat ⋯ while を使う場面 19:34 while 文でパターンマッチングを使う 21:23 repeat ⋯ while のスコープ 21:55 repeat ⋯ while ではパターンマッチングが使えない様子 23:40 バックティック 26:01 repeat ⋯ while には swift らしさが足りない印象 26:46 実際に repeat ⋯ while が使われる例 34:49 乱数を取ってから条件を満たすまで繰り返す例 37:11 repeat ⋯ while と確定初期化の組み合わせ 37:43 repeat 文は外側に働きかける制御構文かもしれない? 40:18 質疑応答 41:23 クロージング ————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #39
はい、今日は繰り返し処理の最後の項目について勉強します。これまで繰り返し処理として for-in
文を見てきましたが、今回は while
文と repeat-while
文について学びます。
while
文は、条件が真の間はコードブロックを繰り返し実行します。これに対して repeat-while
文は、ブロックを先に実行してから条件判定を行うというものです。つまり、while
文は最初に条件を判定し、条件が真である間ループを実行します。一方、repeat-while
文は一度ブロックを実行してから条件を判定し、条件が真であれば繰り返し実行します。
while
文と repeat-while
文が同列に書かれているのは、どちらも条件が変わるまでループを繰り返す基本的な構文だからです。「どちらも while 文」というように捉えて、条件の判定タイミングが異なる点に注意して使い分けます。
repeat-while
文が出てくることは稀ですが、適切に使えば効果的です。例えば、少なくとも一度は処理を実行したい場合に活用できます。この構文が最適な場面を見極めて、適切に使っていくことで、コードの洗練度が高まります。
実際に Playground で while
文と repeat-while
文の動きを確認してみましょう。以下のサンプルコードで while
文の動作を見ていきます。
var randomNumber = Int.random(in: 1...10)
while randomNumber != 5 {
print("Random number: \\(randomNumber)")
randomNumber = Int.random(in: 1...10)
}
このコードでは、randomNumber
が 5 になるまで繰り返し randomNumber
を生成して表示します。同様に repeat-while
文も確認します。
repeat {
randomNumber = Int.random(in: 1...10)
print("Random number: \\(randomNumber)")
} while randomNumber != 5
この場合、最初に一度 randomNumber
を生成して表示し、randomNumber
が 5 でない場合に再びループを実行します。
さらに、古典的な while
文の使い方として、配列から要素を取り出して処理する例を見てみましょう。例えば、配列が空になるまで要素を取り出して処理する場合、以下のようになります。
var values = [1, 2, 3, 4, 5]
while let value = values.popLast() {
print("Value: \\(value)")
}
このコードでは、values
から最後の要素を取り出して表示し、配列が空になるまで繰り返します。popLast
メソッドは、配列の最後の要素を取り出して削除し、その値を返します。
optional
を用いて nil
チェックを行うことで、配列が空になったときにループを停止させます。ポップとプッシュ操作を用いたスタックの基本的な操作がバックグラウンドにあります。
このように、while
文と repeat-while
文は、それぞれの特性を理解した上で適材適所で使うことが重要です。 配列のスライスについての話ですが、元の配列自体には手を加えません。スライス自体がどうメモリ領域を扱っているのかはわかりませんが、スライスは値が変更されることがあります。確かにメモリ領域に関しては、スライスの性質上、メモリ領域が「1, 5, 8, 9, 20」のままの可能性もあります。
例として、変数 redB
を配列 A
のスライスにして、例えば2番目から4番目までをスライスしてみます。このようにスライスを使用すると、元の配列 A
は変更されません。プレイグラウンドが動かないので、詳細な確認は後回しにしますが、基本的な流れとして、ターゲットの配列から先頭要素を取り出し、それを使い次の要素を続けて取り出しながら更新し、要素が無くなるまで繰り返します。これが基本的な while
文の使い方です。
while
文の外側に条件式で使う変数を用意しなければならない点に関しては、if
文も同様かもしれませんが、便利なポイントでもあります。例えば、C言語の for ( int i = 0; i < 10; ++i )
のようなループでは、ループが終了した後も変数 i
が使えます。しかし、Swiftのようにスコープがしっかりしている言語では、for
ブロックの中で定義された変数 i
は外部で使用できません。
例えば、繰り返しの処理を repeat-while
文で書く場合、条件式を下に書くことになります。この書き方だと、ブロックに一度侵入した後に条件評価が行われるため、特定の場面ではエラーを発生させにくくなります。配列の先頭要素が存在しない状態でポップしようとするとランタイムエラーが発生しますが、要素が一つでもあれば期待通りに動作します。適切な場面で repeat-while
文を使用するのが良いでしょう。
例えば、初期要素が必ず存在し、その後特定条件で処理が続く場合には、repeat-while
文が適しています。最初の要素が必ずあり、次から特別な条件で処理を行う場面に有効です。ガード文を使って、必ず初期要素が存在するようにチェックしておくことも良い方法です。
別の場面で期待通りに動くやり方がきっとあるでしょう。配列にこだわっていますが、条件式がどんなに複雑でも最終的に評価されれば良いので、もっと複雑な条件処理の際にもリピータブルファイルが役に立つかもしれません。
次の話に移ります。ファイル文もオプショナルバインディングやパターンマッチングが使えるはずですので、その書き方によっても雰囲気が変わってくるかと思います。例えば、リピートでどうなるか試してみましょう。
let ターゲット = ファイルケーズレッド
前回や前々回でも話しましたが、結論は条件式は全部使えるということですね。リピートも使えるかどうか気になりますが、どうでしょうか。リピートファイルでも、ファイルの後にケースレッドがいけるのではという期待があります。ただ、スコープの問題がありそうです。本当にターゲットという変数が使えるのか、楽しみですね。
とりあえず、ファイルケースレッドの場合、オプショナルパターンを使って受けると、状態を更新する手間を省けたコードを書けます。パターンマッチングが成功する限り、ループを回し続ける構文になります。
if let ターゲット = values.popFirst() {
// 状態を更新する処理
}
プレイグラウンドが動いてくれないこともありますが、このファイル文はちゃんと動くはずです。リピートファイルもこれから書いてみますが、スコープの問題はどう考えてもありそうです。ターゲット変数を使えないという問題があり、両方のケースを書いて検証していきましょう。
リピートでブロックを書いて、ここでプリントターゲットするのは難しいです。
repeat {
if let ターゲット = values.popFirst() {
print(ターゲット)
}
} while 条件
リピートファイルコンディションでエラーが出ています。特に3番目のlet
キーワードが使えないようです。他のエラーは意味をなしていないかもしれません。このエラーは初めて見ますね。バッククオートで名前をエスケープする必要があるのかなと思いましたが、やはりケースレッドが使えないということのようです。
リピートファイルではパターンマッチングができないということですね。バックティックは予約語を使う際にエスケープするためのものです。しかし、リピートファイルの場合はオーソドックスな書き方が求められます。
var ターゲット: 型? = nil
repeat {
ターゲット = values.popFirst()
if ターゲット == nil { break }
// 処理
} while 条件
この方法だとスイフトらしさが失われるかもしれませんが、リピートファイルの制限で仕方ないですね。最近のスイフトの特徴を生かすには、他の方法もあるかもしれません。また、よりスイフトらしい書き方にするために、オプショナルバインディングを使って工夫することも考えられます。
repeat {
guard let ターゲット = values.popFirst() else { break }
// 処理
} while 条件
このように、ガードステートメントを使うと、もう少しスイフトらしい書き方になります。 こうやって試してみると、やっぱり不利ですね。これをうまく使っていこうというときに、できないことはないでしょうけど、一番最初はパスさせて次以降は除外させたい場合など、どうしても使っていくことになると思います。その際、昔の書き方しかできないのは、少し気持ち悪いかもしれません。
AppleのSwiftでもリピートファイルを実際に使っています。このあたりを見ていくと面白そうですね。少し見てみましょう。
やたらテストコードが多い印象です。テストコードか、なるほど。ちょっと見てみたいですね。アンダーライブアリのコアのフィルターとかでも使っているので、手間を省いている感じなのでしょうか。リピートファイルは、応用するときに使えたりしますかね。
例えば、if x == 10 { break } while true {}
これはファイルではなくても良さそうですね。Swift LintのRELMのSwift Lintを見ていると、while x != 10
と書けば良いのではと思います。違うのでしょうか?なんだかwhile true
やwhile false
を使っている場合がAppleの方でもありますね。
ループの条件ラベルを使いたいからそこに戻るためとか、別のラベルなら分かりますが、自由だと無限ループですかね?もしくは最適化のためでしょうか?あまり考えづらいですが、リピートです。一回中を見るので、ループ分岐が必要ない場合などに使う感じでしょう。
while true
の場合、ほぼほぼ無限ループになりそうな気がします。なぜ x == 10
の条件を別にしているのか理解しにくいです。おそらく x
が絶対中になるはずです。エリア名に関しては中を見ると例題っぽいですね。イントロールのトリガーとノントリガーの例として挙げただけなので、これ自体には意味はなさそうです。
他も見ていきますか。x += 1
これあまり参考にならなそうですね。
次に、まず precondition
で中端になっていないことを確認します。その上で formIndex
で自分自身のインデックスを現在のインデックスの次に移動させます。index
が条件を満たして、さらに predicate
という関数でフィルターを受け取っています。これをクリックすると前後が見えますね。
predicate
を別の関数で受け取っていません。formIndex
の実装がこんな感じになっています。内部的に終端判定の条件を持っているようです。エクステンションだから LazyFilterSequence
が持っているのかな。なかなか複雑なフィルターですね。
precondition
で endIndex
が判定されていて、そうでなければ例外を投げる仕組みです。その後の while
文は終了条件です。endIndex
だった場合にループを抜けるという処理になっています。
最初に formIndex
メソッドに突入したとき、すでに endIndex
だった場合にはランタイムエラーにしたい要求があります。最適化を図るために、precondition
を省くことによって全体的なパフォーマンスを向上させ、基本的に空ではない時に素早く動く設計です。最初の endIndex
判定をwhile
文ではなく、precondition
で行います。
これが while
文を使うときには、最初の repeat
で endIndex
の判定を入れる必要があり、そのため処理が重くなる可能性があります。その分、最適化を図るために precondition
で先に判定を行い、repeat
文で一気に処理を行うのがポイントです。
ほかには、リピートファイルはあまり役には立ちそうにないかもしれません。left
と right
を求める関数などがありますが。 これが40と43みたいな、うん、これはリピートがここにあるので、インデントが見つかるということですね。あー、びっくりしました。自分もさりげなく一瞬ハッとしてました。さっきのあれはリートコードの結果ですね。多分、その人がリートコードの試験を受けていて、自分がインデントを間違えただけです。
なるほど、インデントは大事ですね。本当にびっくりします。他に、乱数を使って処理する方法ですが、まあ、あるかな、ないかなという感じです。いずれにしてもリピートの外に変数を用意しないといけないので、それが個人的には気持ち悪いんですよね。
要は、これは多分ホワイル文には置き換えられない気がします。ホワイル文に置き換えようとすると、ブロックの外で先に result =
と書かないといけないので、2回書かなきゃいけない部分があります。たとえば、57行目でランダムに値を割り当て、59行目のホワイル文で判定して、条件が満たされない場合は再度59行目でランダムを生成します。これだとランダム処理を2回行うことになります。
一回で済ませたい場合、判定条件の前に初期値を設定する必要があります。リピート文を使うことも一つの解決策ですね。この例は面白いです。無限ループになる可能性もありますね。バグっていると無限ループになってしまうので、条件を確実に担保する必要があります。
外部で条件を管理すると、59行目が常に変な値を返すといった問題が発生します。そのため、ループ内でランダムの範囲を設定する方が安全です。確かに、初期値を指定しない Defer Initialization
と組み合わせて、このようなプロセスを管理するのは面白いアプローチです。
とにかく、指定した条件に一致するまで値を生成して、それを結果として返すようにします。ホワイル文はブロック内を処理する方法ですが、リピートホワイル文は繰り返しに着目しています。何回かトライをして確定させる。その後で使うという構文です。たとえば、ファイルから行を読み込み、条件が満たされるまで繰り返す方法などが考えられます。
リピートホワイル文のシナリオとして、パスワード自動生成がうまくいかなかった場合、もう一度生成するような例が挙げられます。もっと賢い方法があるかもしれませんが、このような感じです。リピート文についての理解が深まりました。
今日はこれで終わりますが、何かコメントや質問などありますか?ホワイルコンディションについてコメントがありましたね。リピート文内で初期化を実行するというポイントです。なるほど、今日はリピート文について多くの気づきが得られたと思います。 はい、では今日はこのあたりで時間となりましたので、勉強会を終了しようと思います。お疲れ様でした。ありがとうございました。
さて、振り返ってみると、いくつか考えさせられるポイントがありましたね。具体的には、リピートの外に変数を用意しないといけない点が気になるところです。
要は、これですね。おそらく while
文は置き換えられない気がするんですよね。while
文に置き換えようとすると、ブロックの外で先に result = x
という形で書かないといけないので、57行目と59行目の2箇所で記述しなければなりません。具体的には、57行目でランダムな値を生成し、58行目で while
判定し、その条件が満たされない場合は59行目で再度ランダムを生成するという流れです。
ランダムを2回行うため、1回にしたい場合は、判定条件の前に前提条件としてランダムな値を先に生成する必要があります。この点について、repeat
文を使って記述することも考えました。
この例は面白いです。無限ループになる可能性があるため、バグがあると本当に無限ループになってしまいます。そのため、60行目の条件を書かずに済むようなランダム関数にしたいと思っています。ランダム関数のメソッド内でこの処理を行わせたい感じですね。
外部で60行目の条件があることに対して、59行目が常に変な値を出していたら同じ問題が発生します。そのため、基本的にはループの中で Int
のランダムのレンジを作るところまで進めるべきですね。初期値を何も指定していない Definite Initialization
との組み合わせもなかなか興味深いです。
何度か試行して値が決まらない場合は条件に合致するまで繰り返し、その条件に一致した時に結果として返すという形になります。while
文はブロック内の処理に注力していますが、repeat-while
になると繰り返しに注目した構文になりますね。
例えば乱数の生成や、リトライのような処理を行う際に、repeat-while
が有効に使えるかもしれません。雰囲気的には、readLine
でリピートさせたり、ファイルから読み込みを行う例を考えてみると、何となく感覚が掴めます。もちろん、無理やり書いている感じもしますが、repeat
文を使うことで、繰り返しの処理をトライ&エラーの形で表現することができますね。
最後に、コメントとして while
文で条件を外に定義し、リピートループ内で初期化フェーズを実行するという方法もありました。例えばパスワードの自動生成をして、条件を満たさなかったらもう一度生成するなど、賢いやり方もあるかと思います。
今日は、リピート文のイメージが少し変わった気がしています。まずまずの成果が得られたと思いますので、この辺で終わりにしましょう。お疲れ様でした。ありがとうございました。