https://youtu.be/XRToux7Sen4
今回は The Basics
の オプショナル
について、その具体的な特徴について引き続き眺めていきます。これまでのオプショナルそのものに焦点を絞った感じよりかは nil
や初期化周りの特徴といった、少しその外回りに視野を広げていくような感じになりそうです。よろしくお願いしますね。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #142
00:00 開始 00:10 今回の展望 01:17 nil はリテラル 01:47 リテラルがインスタンス化される仕組み 05:21 nil リテラルがインスタンス化される仕組み 06:10 ExpressibleByNilLiteral 07:25 整数リテラルと nil リテラルとで異なる挙動 08:50 総称型とリテラル変換 09:53 型推論で型パラメーターを特定 11:37 配列の内容を nil で初期化するとき 14:21 できないと思い込むこと、わりとよくある 15:22 型推論を使った別の書き方 17:02 Double?.none という書き方 18:12 自身の型を推論させる書き方 18:36 表現のバリエーションを増やしておく 19:31 reduce を用いて表現してみる 20:56 シーケンスと zip や prefix で表現してみる 24:06 AnySequence で乱数発生器 25:32 コンパイル時に適切な値を決め打つ方法 26:41 クロージング —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #142
はい、じゃあ始めていきますね。今日はオプショナルについてお話しします。オプショナルについては以前からよく話題にしていますが、今回はその機能面の基礎をもう少し言語寄りに広げて見ていこうと思います。
まず、画面に映っている「nil」という存在についてです。これまでも何度も出てきましたし、Swiftでコードを書く際には日常のことかと思います。Swift Programming Languageにも「nil」というセクションやサブセクションがあります。今回、この「nil」というものにスポットライトを当て、それがどのような世界を見せてくれるのか、一緒に見ていこうと思います。
「nil」ですが、オプショナルな変数に特殊な値「nil」を割り当てることで、その変数を値のない状態にすることができます。この「nil」というのは値というよりリテラルに近い存在です。ここで少し脱線しようと思いますが、「nil」について話す場合、値とリテラルを区別する場面もあれば、一緒にしても支障がない場面もあります。
例えば、「let v = 1」と書いた場合、この「1」というリテラルは「値」として捉えることができます。ただし、厳密にはリテラルというものは値ではなく、ソースコードに具体的に埋め込むための表記です。リテラル自体は特定の型に所属しておらず、どんな型にもなり得る特殊な表記です。例えば、Int
型に入れればInt
の値になりますし、Int8
に入れればInt8
の値になりますし、NSNumber
に入れればNSNumber
の値になります。
具体的には、このリテラルがどの型になるかは、その文脈や左辺の型情報によって決まります。例えば、NSNumber
に「1」というリテラルを入れる場合、このNSNumber
がExpressibleByIntegerLiteral
プロトコルに準拠しているため、「1」というリテラルはNSNumber
として扱われます。このプロトコルにより、適切なイニシャライザーが使用されてリテラル値が適切な型のインスタンスに変換されるのです。
同じように、「nil」もリテラルとして扱われます。例えば、Int?
(Int
のオプショナル)に「nil」を代入する場合、「nil」自体はどんな型にもなり得る特殊なリテラルです。ここで、Optional
型の定義を見ると、Optional
はExpressibleByNilLiteral
に準拠しています。つまり、Optional
は「nil」リテラルを使ってインスタンスを作成することができるのです。
このプロトコルの定義を見ると、パラメーターとしてボイドを取る「init(nilLiteral: Void)」を要求しています。コンパイラは内部でOptional<Int>
がExpressibleByNilLiteral
に準拠していることを確認し、「nil」からオプショナルインスタンスを生成します。
これが「nil」の基本的な仕組みです。このようにリテラルと特定の型のプロトコル準拠によって、リテラル値が適切に解釈され、適切な型のインスタンスが生成されるわけです。 「Twist」という名前のプログラムについて説明します。さっきは Int
型のリテラルからの変換の話をしましたが、今回はオプショナル型との違いについて少し紹介します。
まず、オプショナルの Int
型についてですが、シンプルに一般的な書き方をすると、Int
のオプショナルに nil
を入れると、それが Int
のオプショナル型の値の nil
として存在します。言い換えれば、それは「存在しない」ということです。しかし、こうやってインスタンスが作られると、型推論で完全に特定できない状況になり、これがコンパイルエラーとなります。これは nil
の特徴というよりは、総称型(型パラメータを取る型)に関する問題です。
例えば、配列リテラルの場合は良しなに推論してくれますが、独自の型を作成する場合や、型パラメータを設定する場合は、これがうまく動作するかの確認が必要です。特に、リテラルの特徴として特殊な状況が生じます。
次に、型推論の例を見てみます。例えば、構造体を作成し、extension
で value
に対して、フェアローバリューが Int
の場合の初期化子を作ります。これにより、コンパイルが通るか確認できます。この初期化子を使った時点でローバリュー(パラメータ)は Int
であると推論でき、正常に動作します。こうした工夫により、型推論が可能になります。
また、nil
の場合は、そのままでは型の特定が難しいため、何らかの方法で型を明示する必要があります。
ここまでの内容で何か質問があれば、お知らせください。
さて、オプショナルの問題ですが、例えば W
型のオプショナルを任意の個数返したい場合、nil
を複数個戻す関数を設定します。この際、repeating
関数を使って次のように書きます。
Array(repeating: nil as W?, count: 5)
このように書くと、nil
をオプショナルで5個返すことができます。パラメータ指定などの場合にも適切に扱う必要があります。
しかし、特定の条件下でリピーターを使う場合に、パラメータの指定に注意してください。このような工夫により、型の問題をクリアし、正常に動作するコードを書くことができます。 最初に試したのですが、ダメだと思ったことがありました。記憶に残っていると、何度も思い出して再挑戦するのは良いですね。編集をしているときに勘違いして、できないものだと思い込んでしまうことも結構ありますね。
先日、ダブルハテナ (??
) を使う話をしていましたね。同じ理論で大丈夫そうです。Swiftの中ではリピーティングが一番きれいなやり方だと思いますが、あまり見慣れないので注意が必要かもしれませんね。型パラメーターを推論してくれる機能もありますね。どう名刺的に指定するかのバリエーションもあります。
推論させる部分がポイントですね。セルフを使って解説した事例がありました。型推論の状況によっていろんな推論の場所があるので、この部分を書けると便利です。
型推論が重要で、いろんなバリエーションを試しておくと良いことがありますね。コメントでもっと詳細に説明するとさらに良いです。
Swiftでは、型パラメーターやオプショナル型の処理方法など、さまざまな表現方法が存在します。具体例として、プロパティやイニシャライザー (init
) の中でバリエーションを持たせる方法があります。
状況によって、どの表現方法が適しているか変わってきます。いろんな可能性を試してみるのは、いつか役立つことがあります。特に、フルで書く方法が適している場面や、反対に簡潔に書くほうが見やすい場面など、状況に応じた選択が大切です。このようにいろいろ試してみることで知見が広がるので、おすすめです。 他に何か面白い企画とか思いつく人はいますか?トータルで見て、こういったところの内容は生じてもそうですし、そもそもリピーティングの頃から一部など、動的なサイズでアップしていくリピーティングとなると、詰め直す感じになりますよね。型が出てきてリピーティングで詰め直せば、このようなノリになりますね。範囲を活用すればいけるんだと思いますが、リピーティングでここで材料を与えるわけです。ダブルのオプショナル型の値を用意して、ここに対して範囲と合わせます。しかし、あまりスマートに感じないこともあるかもしれません。これはあくまでもリピーティングを使ったときの表現方法の一例です。他の方法もあると思います。
例えば、0から5までの範囲をシーケンスに連ねる必要があったりしますが、このタイミングでいろんな方法が考えられます。オプショナルを使う例もありますが、これはあまりお勧めできません。0から5の範囲では、多分インデックスが0からであることを確認しないといけないです。実際のアプリケーションではどのように動作するか、検証が必要です。
さらに、ジップを使う方法もあります。たとえば、0と5という範囲とシーケンスを連ねる場合、異なるタイミングに入ってしまうことも考慮しなければなりません。ここではプリントしないと動作が分からないかもしれません。この方法は時折、ランダムに使える場合もありますが、実際のコードでは見直しが必要です。
また、nilを使う方法やReduceを応用する方法も考えられますが、実装によってはやりすぎになったり、意図しない挙動になることがあるので注意が必要です。例えば、ランタイムに依存する要件であれば、コンパイル時にエラーを検出しておくといった発想も重要です。ローカルにするとテストが大変になったりするので、実際の使用時には慎重な設計が求められます。
以上、今日はこれぐらいにしておきますが、次回はこの話の続きをする予定です。今日の勉強会はこれで終わります。お疲れ様でした。