https://youtu.be/Vq6fAKGABjk
今回は、前回の話の中で面白いと感じた NonEmpty
という第三者ライブラリーにみる first
プロパティーの作りのお話ですとか、なんとなくでお話ししていた マングリング
についてを確認したりと、前回を受けての続きの回にしてみます。それを見てから、オプショナル型の振る舞いで少し気になることがあるので、時間があれば掘り下げてみていけたらいいなと思ってます。よろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #152 00:00 開始 00:10 今回の展望 01:22 unsafelyUnwrapped の挙動 03:27 NonEmpty ライブラリーにおける first の実装 06:53 型を Collection に準拠させるのに必要な振る舞い 11:56 必ず存在するのにオプショナルで扱われる 13:44 first の値を非オプショナル型で扱いたい 16:12 適切な API を設計側が用意する 18:01 オプショナルのサブタイプ性で混乱中 24:14 共変性をクラスのオーバーライドで確かめてみる 26:09 オプショナル型の反変性 28:19 クラスにみる共変性と反変性 30:43 プロトコルでもう一度、試してみる 31:51 サブタイプを扱うメソッドはオーバーロードされる様子 33:29 サブタイプを扱うプロパティーもオーバーロードされる様子 34:54 謎が残りながらもクロージング ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #152
はい、じゃあ始めていきましょうね。前回の勉強会の中で話した様々な興味深いポイントを踏まえて、今回はSwiftについて見ていけたらいいかなと思っています。前回の話を元に進めますが、前回の内容が前提知識として必要というわけではありません。前回参加していない方も全然問題なく見ていけるかなと思います。
本編の内容を紹介しておくと、Swiftのオプショナルという機能の一つである「強制アンラップ」について見ていきます。この強制アンラップを見ている中でいくつか面白い話が出てきました。一応バックグラウンドとして、これらの話題をイメージしておくと役に立つかもしれません。
脱線していったきっかけは、「オプショナル型が持っている!
(アンワープオペレーター)がリリースビルドで完全に無視される」という点です。大抵の場合、ゼロフィルされた値が手に入ることが分かっています。もしかすると全てがゼロフィルされるのかもしれませんが、ここまでは確認しきれていない状況です。いずれにせよ、ゼロフィルに近い状況であることは間違いありません。
デバッグビルドではエラーで落ちるけれど、リリースビルドではInt
型のアンラップオプショナル型の場合、ゼロが取れちゃうというお話です。例えば、通常のリリースビルドではnil
が入っていたとしても、アンワープオペレーターによってゼロがプリントされるという状況が起こります。通常、App Storeにリリースされるアプリでは、エラーをハンドルできなくなる状況が怖いですねという話です。
この使い道として、「NonEmpty」というライブラリが活用できそうだという話がとても面白かったので、紹介しようかなと思います。しかし、今回はこのNonEmptyライブラリそのものを紹介するわけではなく、その中で面白い部分、特にオプショナルと関連した部分について話します。
具体的には、ノンエンプティライブラリの中で興味深いオプショナルの使い方について話していきます。例えば、NonEmptyライブラリは空でないリストを保証するものなので、初期化時に必ず少なくとも一つの要素が存在することを前提にしています。これによって、ランタイムエラーの発生を未然に防ぐ設計になっています。
では、ここからその具体的な部分について詳しく見ていきましょう。 とりあえず、これの「ファースト」というプロパティについて説明します。面白いのは、ノンエンプティというジェネリック型で、この型を使うと、要素が何もない状態を排除した配列を扱うことができます。例えば、別のプログラミング言語では G言語と表記されていたりしますが、正しくは Swift です。
具体的には、values
という数値の配列があったとします。この配列の最初の要素を取り出すとき、通常は first
プロパティを使いますが、この first
プロパティはオプショナル型(Optional)になっています。これはなぜかというと、配列が空の場合に要素が存在しないためで、そのときには nil
が返されるからです。現在の状況では最初の要素 1
が取れますが、配列が空の場合には nil
になる可能性があります。
それに対して、ノンエンプティのライブラリを使うと、配列が空でないことが保証されるため、first
がオプショナル型ではなくなります。こうなると、最初の要素が必ず存在することが保証され、 Optional
を気にせずに値を取得できます。
コレクションに関しても話しましょう。例えば、カスタムなデータ構造を作る際に、Collection
プロトコルに準拠させることができます。この Collection
プロトコルが要求するメソッドは、startIndex
と endIndex
のほか、makeIterator
メソッドがあります。また、インデックスの取得方法として index(after:)
メソッドも必要です。しかし、他に必要なメソッドは状況によります。
具体的に言うと、以下のようなプロパティとメソッドを実装する必要があります。
struct CustomCollection: Collection {
var items: [Item]
var startIndex: Int { return items.startIndex }
var endIndex: Int { return items.endIndex }
func index(after i: Int) -> Int {
return items.index(after: i)
}
subscript(position: Int) -> Item {
return items[position]
}
func makeIterator() -> IndexingIterator<[Item]> {
return items.makeIterator()
}
}
このようにして、Collection
プロトコルに準拠するカスタムなデータ構造を作ることができます。その際、Swift のコンパイラがいくつかの部分を自動的に処理してくれることもありますが、基本的にはこれらを明示的に実装する必要があります。 例えば、唯一1つの要素が存在するコレクションを作ったとします。この場合、まず startIndex
を実装する必要があります。インデックス型は Int
にしましょう。これで startIndex
は 1 とします。そして endIndex
も実装しますが、唯一1つの要素を返すので、endIndex
はたまたま2とします。ただ、インデックスが Comparable でなければなりませんので、インデックスを1にします。
次にサブスクリプトを実装します。インデックスを渡すと配列のスライスを返しますが、 self
を返せば良いとします。これにより、どんなにスライスしても要素が自分自身と同じになるということにします。
また、index(after:)
も必要です。あるインデックスの次のインデックスを返すようにしますが、この場合、どんなインデックスが渡されても終端インデックスを返すことにしましょう。これは 1
または endIndex
を返しますが、後者の方がより抽象的で良いですね。必ず1個しか要素がないため、次のインデックスは終端とすることにします。
ここまでで startIndex
、endIndex
、index(after:)
、要素のサブスクリプトが揃いました。これで基本的な実装は完了です。
struct OnlyOneArray: Collection {
typealias Index = Int
typealias Element = Int
let startIndex = 1
let endIndex = 2
subscript(index: Int) -> Int {
return 7 // 唯一の要素
}
func index(after i: Int) -> Int {
return endIndex
}
}
let onlyOneArray = OnlyOneArray()
for value in onlyOneArray {
print(value) // 7が1つだけ出力されます
}
例えば、要素が一つしかない7という値が必ず存在する配列です。この場合、values.first
を取得する際に注意が必要です。このままですと型がオプショナルになっていますが、これはコレクションプロトコルが first
はオプショナル型で要素を参照できるように要求しているためです。
ですが、この OnlyOneArray
は必ず1つの要素しか存在しないため、first
プロパティを独自に実装してオプショナルではない型を返すようにすることもできます。以下のように実装できます。
extension OnlyOneArray {
var first: Int {
return self[startIndex]
}
}
print(onlyOneArray.first) // 7
このようにすることで、first
プロパティを取る際にオプショナル型をアンラップする必要がなくなります。これで、OnlyOneArray
はより使いやすくなりました。 とりあえず、こうやってオンリーワンアレイのファーストをオプショナルを使わずにエレメントを直接返す実装ができるという話です。これを使って、さっきの最初に話したノンエンプティー型についても説明します。ノンエンプティー型は、このようにコレクションに準拠しています。ソースコードがこれです。ノンエンプティー・ストラクトがこちらで、同時にノンエンプティーがコレクションに準拠しています。つまり、ファーストがオプショナルではなく、直接手に入るようになっているので、非常に親切な設計です。
ニルじゃないことが確実なら「強制アンラップ」とか、前回お話した「アンラップ作業の手間」がなくなりますね。オプショナルがあった時にそれが空っぽじゃないとわかっている場合、強制アンラップや「!」を使って処理することもできます。ただ、それをAPI利用側が判断するのではなく、APIをデザインした人がそもそもニルになるはずがない場合には、最初からオプショナルを使わないようにAPIを設計することが重要です。
つまり、ファーストの具体的な型をオプショナルではないエレメントとして提供することで、利用側はニルを考慮する必要がなくなります。これが重要な手法です。この考え方に基づいて、コレクション関連のクラスを設計する際にも、同様のアプローチが有効です。
コレクションだと分かりにくいですが、オプショナルの面白い特徴として、プロトコル内で何らかのプロトコルがあり、それがオプショナル型を要求している場合を考えます。例えば、あるプロトコルがファンクションやプロパティとしてオプショナル型を要求していたとしましょう。このプロトコルに準拠した型を実装する場合、通常はそのプロパティをオプショナル型で実装します。
ただし、ここで問題になるのが、プロトコルの既定の実装と自分で追加する実装が混在してしまう点です。たとえば、既定の実装が .headValue
と呼ばれるプロパティを含んでおり、それがオプショナル型である場合を考えます。実装側は .headValue
に対してオプショナルを期待するでしょう。しかし、自分の設計意図と違っている場合には、それを非オプショナルにすることは難しくなります。
これを試行錯誤する際、「ヘッドバリュー」などのプロパティ名を使ってテストを行ったりしますが、通常は名前もオプショナルで受け取るようにします。実際には、きちんとフィックスされるまでリフレッシュを続けてみることが重要です。
ですから、プロトコルに準拠する時も、オプショナルの有無を慎重に考える必要があります。自分で書いたコードが最終的に通るようにするためには、その点を考慮に入れることが重要です。 ```swift let getValue = value
こうやって、これでコンパイルが通ります。ここでオプショナルをやめても通りますね。あれ、通らないんでしたっけ。混乱しています。何か通る方法がなかったですか。
一旦、誰かが分かることを期待しつつ話を進めましょう。ビックリマークがついていると通るかってことですよね。ビックリマークがついていると通るか、準拠は通らないかも。WebViewのプロトコルデリゲート系のやつがSwiftで、オブジェクトで書かれているデリゲートプロトコルをベースにすると、ビックリマークになっていて、ビックリマークでもオプショナルでも効果になるような変更がどこかで始まる。
そうですよね、別にObjective-Cだけに限らず、同じようにSwiftでもそうなりますよね。それに関連する部分で何か変なことを言おうとしましたが、そういえばオーバーライドでの対応がありますね。オーバーライドでOKというケースがあります。特にObjective-CでUIキットのデリゲートでオーバーライドが可能だった気がします。
例えば、`getValue`メソッドで`Int`オプショナルの値を返す場合、オーバーライドする`getValue`メソッドで普通の`Int`でもOKです。これは、混乱しやすいですが、オプショナル型を返すメソッドをオーバーライドして普通の型を返すことも可能だからです。
サブクラスの方が`Int`を返すけど、親クラスのメソッドは`Int?`を返すので、それが通るということです。つまり、親クラスのメソッドがオプショナル型で、サブクラスのメソッドが通常の型であっても、オプショナルとしても同じ型として扱われるので、このように通るということです。
まとめると、サブクラスにおける`getValue`メソッドは`Int`を返すが、親クラスのメソッドはオプショナル型なので、これもオプショナルとして受け取れるということです。戻り値についてはこのような逆の関係になるんですね。 この辺りのサブタイピングが重要です。オプショナルについて話しましたが、ベースクラスとサブクラス、プロトコルと具象型の関係も同様に扱えます。サブタイプの関係として、オプショナルも一例です。
APIを提供する側が、これは必ず上書きしなければならないという状況で、例えば `getValue` がオプショナルではなく、必ず `Int` 型を返す場合、オプショナルを使わずに `Int` 型としてオーバーライドできます。
具体的には、ベースクラスとしてオブジェクトをインスタンス化して使うとき、`getValue` はオプショナルとして扱われます。しかし、サブクラスとして使う場合、オプショナルの心配なく具体的な型として扱えるようになります。これにより、柔軟かつ安全なインターフェースが提供されます。
一方、具体的にサブクラスとして使えない状況、例えば親クラスを扱わざるを得ないときは、`getValue` がオプショナルのままになります。このように汎用的な場合にはオプショナルですが、具体的に使えるときにはオプショナルではないという形でインターフェースを提供できます。これは非常に便利な特徴です。
プロトコルに関しても同様のことができたような気がしますが、どうだったか確認してみましょう。たとえば関数 `setValue` を使ってみたとします。最初はオプショナルで試してみますが、納得がいかない部分もありますね。ここで、もう少し試してみる必要があります。
何かの事情で、プロトコルでそれを実現できるかという質問が出ていますが、このケースではできないようですね。ただ、オプショナルが処理できる場合、通常の処理も可能になるはずです。記憶では、実際に活用していたことがあるような気がします。
プロパティに関しても同様に試してみます。例えば、以下のように `value` を `Int` 型として宣言し、ディフォルト実装を追加します。すると、コンパイルは問題なく通ります。
```swift
extension A {
var value: Int {
return 42
}
}
続けて、独自の実装を行う場合でも同様です。このように柔軟な実装が可能です。
再度確認したい場合には、もう一度詳細を確認して試してみましょう。また、新たな発見があればお知らせします。