https://youtu.be/XWT3mbYBnKM
今回は、前回に迷子になったサブタイピングを考慮したプロトコル適合まわりの補足というか再確認というかを行ってから、引き続き オプショナル
の初歩的な機能 オプショナルバインディング
辺りを眺めていこうと思ってます。
そして今日から、ゆめみ社外の人も一般公募で募っての開催 になります。顔ぶれは変わってくると思いますけれど普段通りに参加してもらえれば大丈夫ですので、よろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #153 00:00 開始 00:37 今回の展望 01:24 プロトコルの要求にサブタイプでは適合できない 04:15 プロパティーでサブタイプの値を扱える 05:16 引数でサブタイプの値を扱える 06:00 プロトコル準拠時にサブタイプが使えても良さそうだけれど⋯ 09:10 Existential Type 10:06 _openExistential(_:do:) 10:33 ドキュメントコメントに打ち消し線を含める 12:29 失敗可能イニシャライザーなら失敗しないもので準拠可能 15:32 イニシャライザーだけの特例なのが気になる 16:06 プロトコルのイニシャライザーを呼び出すこともある 18:06 オプショナル型のサブタイピングは特別ルール 20:58 オプショナル型とクラス型とで比べてみる 21:54 Int32 も Int64 のサブタイプに成り得るかどうか 26:56 クラスにおけるオーバーライドのおさらい 27:46 既定の実装があるときの混乱に注意 32:11 静的メソッドでもサブタイプでは適合できない 34:22 RawRepresentable でのサブタイプの活用 38:00 クロージングと次回の展望 ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #153
はい、それでは始めていきますね。今日も引き続き、初めて参加する方が多いかと思いますが、まずは強制アンラップオプショナルの強制アンラップについて見ていきます。その前に、プロトコルの適合に関連した共変性と反変性について話します。ただ、これは少し専門的な言葉で、自分も詳しくない部分があるので、間違っているかもしれません。サブタイピングとプロトコル適用を絡めたときに面白い挙動があるよ、という話をしていこうと思います。
たとえば、プロトコル P
があったとして、適合条件として func something() -> Int?
のようにメソッドがオプショナルな Int
で定義されているとします。これに準拠するために、以下のようなコードを書いたとします。
protocol P {
func something() -> Int?
}
struct ConformingType: P {
func something() -> Int {
return 42
}
}
ここで、Int?
は Int
のサブタイプなので、本来は Int
型で実装してもうまくいくはずですが、実際に書いてみたところうまく動作しなかったんです。不思議に思って調べてもコンパイルが通らない状況でした。同様に、オプショナルのプロパティでも試してみましたが、やはりうまくいきませんでした。
次に、以下のように整数型のプロパティを持つプロトコル AnotherProtocol
を定義し、そのオプショナルをサポートするプロパティ value
を持たせてみます。
protocol AnotherProtocol {
var value: Int? { get }
}
struct AnotherConformingType: AnotherProtocol {
var value: Int {
return 10
}
}
ここでもオプショナルではないプロパティとして実装してもダメだったという結果でした。
このように、一見するとサブタイプを利用してうまく動作するように見える箇所が、実際にコードを書いて動作させてみると意図通りにいかない場合があります。これはコンパイラの仕様で部分的にサポートされていないのかもしれませんね。
もし、意味がわかりにくいと感じる方がいらっしゃっても、この場では無理に理解する必要はないので、ゆっくりとした気持ちで聞いていてください。いずれ役立つ時が来るかもしれません。
さらに具体例として、以下のようなコードを書いてみましょう。
protocol RandomValueGenerator {
func generate() -> Int?
}
struct Dice: RandomValueGenerator {
func generate() -> Int {
return Int.random(in: 1...6)
}
}
ここでも、プロトコル RandomValueGenerator
が Int?
型を返すように定義されていても、実装側で Int
型を返す場合には適合できないという結果になります。
つまり、プロトコルの仕様としてオプショナル型が定義されている場合、それに準じて実装を行う必要があります。この応用として、他のプロトコルや型システムに関する話も含まれますが、この点で注意が必要です。
以上、今日はプロトコルの適合とオプショナル型に関する詳細な挙動について取り上げました。興味を持った方は、ぜひ自分でも試してみてください。 「プロトコル型のp
というのがあります。それとして扱うときには、どうしてもsomething
の戻り値はオプショナルとして扱わざるを得ません。ただ、ここでInt
型を想定したときに、このp
型としてはオプショナルとして扱わざるを得ませんが、具体的なS
型を使った場合、たとえば if let value as S
として、確実にS
型という場面だとわかったとしたら、something
というメソッドがInt
型で返されるほうが嬉しいです。それが成り立つはずです。
実際、理屈的には成り立つと思いますし、value.something
でオプショナルであっても具体的なInt
型で取れてもいいと思います。しかし、実際にはダメでした。昔はこれらが成り立っていたということもないです。さすがにコンパイルがしっかりチェックしているので、そのくらいはね。ちょっと違和感を感じるんですが、プラスのオーバーライドとかでは到達できるんですよ。でもプロトコルではダメなんです。
そういうお話をして、ここで一回コメントを拾いますね。エクジステンシャルタイプ…。あ、そうですね、まさにエクジステンシャルタイプということでした。自分の読み間違いをしていた気がします。エクジステンシャルタイプをずっと間違ってエクステンシャルとか言っていたかもしれません。「拡張可能」という意味になっちゃいますので、それは全然違いますね。今までの勉強会のアーカイブでも平気でエクステンシャルと言っていたので、アーカイブを見る人は気をつけてください。エクジステンシャルタイプが正しいです。
続きに戻ります。このオープンエクジステンシャルという関数はこのコンテキストでは使えません。どのコンテキストで使えるんでしょうか。インポートにも入っていませんし、アンダースコアで始まるから裏の関数なんでしょう。定義は具体的には辿れなかったですが、SwiftのCライブラリに実装されていることが伺えます。
また、一つ気になったのが、マークダウンの打ち消し線が効くかどうかです。ちゃんと線も入っていますね、これ。面白いですね、よく分からないですが。マークダウンのドキュメントがあると思うので、どういう書き方ができるのか測るとよさそうです。
さて、エクジステンシャルタイプの話題に戻ります。エクジステンシャルタイプに関しても、開発者がどういった意図で実装しているか知りたければ、GitHubのオープンソース、例えば github.com/apple/...
などで検索すると実装が出てくると思います。コメントも書かれているので、思いがけない発見をしたいときにはオープンソースを調べるといいでしょう。どんな世界観が広がっているのか見えてきます。
さて、本題に戻ります。ここまでの話では、タブタイピングの都合で実装を変えられると思っていたのですが、これはどうやら自分の勘違いだったようです。できませんでした。」 どこから勘違いが生じたのかを考えると、イニシャライザーが特別な動きをすることがあります。イニシャライザーを要求する場合、例えばオプショナルを要求するイニシャライザーがあるとします。この場合、トラックとself
がイニシャライザーとして適切に認識されないことがあるかもしれませんが、ここでは問題ありません。コンパイラーを通過させるために、デフォルトイニシャライザーが勝手に生成されたため、これに対処する方法の一つとしてプロパティにバリューを持たせる必要があります。
具体的には、イニシャライザーが勝手に生成されないように、例えばInt
型のプロパティを持つことで、イニシャライザーが要求する特定のイニシャライザーが勝手に生成されるのを防ぐことができます。このように、例えばself.x
を0
で初期化するイニシャライザーを記述するとします。このプロトコルの要求を満たすことで、イニシャライザーが失敗しないようになり、正しく動作することを確認できます。
この場合、失敗しないイニシャライザーに置き換えてもプロトコルの要求を満たすことができるので、特例として扱われています。プロトコルそのものは実体がなくインスタンス化できないため、このようなパターンが登場しないことも理由の一つです。
ただし、ジェネリックタイプで型を取り扱う場合は、プロトコルPに準拠する型を用いて、ジェネリックパラメータとして受け取ることも可能です。この状況でもPのイニシャライザーを呼び出すことができるため、プロトコルPのイニシャライザーを直接呼び出すことはできませんが、近い雰囲気のコードを書くことはできます。
基本的に、Pを具体的にイニシャライズすることはできないため、Sがイニシャライザーを呼ぶ際にプロトコルPが失敗可能イニシャライザーとして指定していても、Sは失敗しないイニシャライザーとして許容されても問題ないと考えられます。
このように、イニシャライザーについては特殊な書き方ができる特徴があります。また、サブタイピングについてプロトコルとスーパークラスに関しても関連するポイントが多々あります。 基本的には、スーパークラスとプロトコルに関する話から始めましたが、その中で特殊なケースでオーバーロードではなくサブタイピングになることについて議論しています。これは難しい内容ですが、具体的に見ていけばわかるかもしれませんね。
通常、プロトコルの順序に従うと難しいのですが、オーバーロードの場合は可能性があります。具体的には、例えばSomeThing
がInt64
を返す場合と、SomeThing
がInt32
を返す場合。このようなケースです。オーバーロードができるかどうかの話になりましたが、推奨されていないものの、実際には可能ですよね。
また、オプショナル型 (Optional
) とサブタイプの関係についても検討しました。特別な場合として、オプショナルがサブタイプになっているのは、これはSwift言語の特殊なルールによるものです。例えば、Int
型の値をInt?
(オプショナル型)に代入することが許容されている理由も、言語仕様で決まっています。
オプショナル型とベースとサブクラスの関係をもう少し詳しく見てみましょう。例えば、Optional<Int>
とInt
を考えた場合に、Int
型の値をOptional<Int>
に代入できる。これは、互換性があるからです。同様に、スーパークラスとサブクラスでも、サブクラスのインスタンスをスーパークラスの型の変数に代入できるという関係があります。
例として、Base
クラスとSub
クラスを考えた場合、Sub
クラスのインスタンスをBase
クラスの型の変数に代入できる。この関係がベースとサブのサブタイピングに相当するわけです。
オプショナル型の特殊なケースとして、例えばInt?
がInt
の代わりに使えるような場合があります。これはなぜかといえば、Swiftの言語仕様でそのように定められているからです。同じように、例えばInt64
のオプショナル型がInt32
のオプショナル型としてサブタイピングできるかというと、これはできないのです。プロトコルやイニシャライザーでは特にこの点が際立ちます。
特別扱いされていない場合、明示的な型変換が必要になります。サブクラスがスーパークラスのサブタイプとされていない場合も、同様です。仮にInt64
とInt32
の場合だと、互換性がないためにコンパイルエラーが発生することがあります。
このようなサブタイピングとオーバーロードの関係については、特に型の安全性を考慮すべきです。これによって、明示的な型変換が必要となる場合が出てきます。以上がSwiftにおけるサブタイピングとオーバーロードの詳細です。 オーバーロードされていない場合、どのメソッドを呼び出すかが分からなくなる問題が発生します。例えば、Int32
を戻り値とするメソッドと、Int64
を戻り値とするメソッドが共存している場合、そのどちらを呼び出すかが曖昧になります。このため、Int32
とInt32
のオプショナルを使い分けることが求められます。
そのため、ある場合にはオプショナルなInt32
を返すメソッドが定義されます。インターフェースが同じと見なされることで、オーバーライドが可能となり、その結果、SomeMethod
はInt32
型として呼ばれることになります。これは実際にオプショナルなInt32
として渡しても、nil
ではなく32
が返ってくることを意味します。このようにして問題はオーバーロードによって解決されるのです。
次に、プロトコルの話に移ります。プロトコルP
でSomething
がInt
のオプショナル型で返ってくる仕様があったとします。仮にSomething
メソッドが返す型を指定する場合を考えます。プロトコルP
はSomething
メソッドを要求しますが、もしそのメソッドが実装されていなければ、デフォルトの実装が使われます。具体例として、func something() -> Int?
と定義されたデフォルトのメソッドが存在する場合、そのメソッドを実装しなくてもコンパイルが通るようになります。そして、S
がこのプロトコルに準拠している場合、nil
を返すデフォルト実装が呼ばれることになります。
ここで注意すべきなのは、デフォルト実装によって予期しない動作が発生する点です。もしクラスS
の中で具体的な実装を行い、1から100までのランダムな整数を返す
ことにした場合、実際にはデフォルト実装が呼ばれてしまい、nil
が返ることがあります。これはプロトコルP
の仕様によるものですが、実装者がこの点を誤解すると、意図しない動作が発生します。
例えば、S
のインスタンスvalue.something
を呼び出すと、ランダムな整数を期待していたのに、実際にはデフォルト実装のnil
が返ることがあります。このように誤解が生じると厄介な事態になります。
次に、ジェネリック関数とイニシャライザーの話に移ります。あるジェネリックな型E
として、イニシャライザーを定義し、その戻り値がE
のオプショナル型になる場合を考えます。例えば、イニシャライザー内で特定の条件が満たされない場合にnil
を返すことが考慮されます。
このように、プログラミングや言語仕様には誤解や勘違いが起こりやすい点が多いですが、細部まで理解しておくことが重要です。特にSwiftのような高機能な言語を使いこなす場合、仕様や実装の細部までしっかり把握しておくことが求められます。 関数やスタティック関数と同等という話をしていたときに、イニシャライザーについて特別な扱いがあるのであれば、同じような雰囲気のインターフェースを要求された場合、例えば static func instantiate
という戻り値が Self
のオプショナルですよ、みたいなメソッドとイニシャライザーがほぼ同等なインターフェースじゃないですか。わずかに違うんですけど、ほぼ一緒ですね。
そうしたときに、この実装がオプショナルじゃなくても許されるのか試してみました。しかし、これもダメでした。セルフのオプショナルとして実装し、リターンセルフのイニシャライザーなら通りますが、オプショナルをやめると「どこどこに準拠してない」と言われます。つまり、セルフがサブタイピングみたいな関係になっているわけではなく、完全にイニシャライザーが特別扱いされているようです。
Swift の中で失敗可能イニシャライザーがよく使われるケースの一つとして「RawRepresentable」というプロトコルがあります。このプロトコルはロー・バリューを任意の型で内包し、そのプロパティを参照することができること、そしてイニシャライザーとしてロー・バリューからの変換が可能であることが期待されています。ここで、オプショナル付きのイニシャライザーが搭載されています。
例えば、Int
ではなく String
型で ID 型のストラクトを作成し、これを RawRepresentable
に準拠させるとします。そして、ロー・バリューとして String
型を用い、イニシャライザーでセルフのロー・バリューを一つ受け取るようにすることができます。こうすることで、ID
型のイニシャライザーとしてロー・バリューを渡すというコードが実現できます。
このコードを見ると、パラメーターで受け取る String
を 100% 保存できるため、失敗しません。もし例外的な処理をしたい場合は別ですが、通常のケースでは失敗しないので、オプショナルにする必要はないと感じます。このような場合、プロトコルに準拠しながら、成功するイニシャライザーを使うことができます。
今回は、前回の話からオプショナルに関する内容を紹介しました。次回は、オプショナルバインディングの基本的な話に入っていきたいと思います。興味があれば、ぜひまたご参加ください。今日の勉強会はこれで終了です。お疲れ様でした。ありがとうございました。