Page icon

第66回

2022/01/14
A Swift Tour
Empty
Empty
Empty
Empty
12 more properties
今回は、前々回から眺めている
プロトコルと拡張
の最後のセクションになります。前回に話しきれなかった
条件付き準拠
のあたりを見てから、続いて
プロトコルを型として扱う
ところを確認していく感じになりそうです。プロトコルを型として扱うことについてはこれまでにも時折ふれていますけれど、復習ついでに改めてそれに着目しなおしてみることにしますね。どうぞよろしくお願いしますね。
———————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #66
00:00 開始 01:09 条件付きプロトコル準拠 04:00 条件付きプロトコル準拠がなかった頃の実装 07:56 条件付きプロトコル準拠が使えるようになると 09:35 型パラメーターの違いがプロトコルの準拠性に影響 10:11 要素の準拠性に応じてそれを内包する型の準拠性も変えていく 10:29 条件付きプロトコル準拠と、プロトコル準拠の合成 13:43 条件付きプロトコル準拠とオーバーロード 16:33 オプショナル型でも条件付きプロトコル拡張が役立つ 17:06 独自の型との条件付きプロトコル準拠の組み合わせ 17:20 プロトコル拡張では条件付きプロトコル拡張できない 19:21 プロトコル準拠の合成が行われるプロトコルの種類 20:58 自動準拠が適用されるプロトコル 21:39 Comparable プロトコルは自動的には合成されない 22:16 ひとつにまとめて比較するか、辞書順で比較するか 23:18 プロトコルが自動準拠されるかどうかは言語仕様で決まっている 24:11 プロトコル名を型として扱う 24:34 インターフェイスとして扱う 25:06 存在型 26:14 存在型の特徴 26:50 存在型にはリテラルが型に変換されてから代入される 28:45 ジェネリックなプロトコルは存在型として扱えない 29:56 Self を返すだけならジェネリックなプロトコルにはならない 31:20 引数で Self を使うとジェネリックなプロトコルになる 32:01 戻り値が Self でもジェネリックとされない理由 35:22 存在型はそのプロトコル自身には準拠しない 37:39 存在型をそのプロトコルに準拠していると見做せない理由 40:49 次回の展望 ————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #66
では、今日は前回・前々回に続いてプロトコルと拡張について学びます。この分野については今日でこのセクションが終わるでしょう。先ずは型の拡張について、前々回たっぷり話しましたが、その中で話しきれなかったコンディショナルコンフォーマンス、この辺りを改めてしっかり見ていきたいと思います。
コンディショナルコンフォーマンスは非常に大事な項目で、前回特別に作ったスライドがありますので、まずそれを見せて整理します。視覚的に確認できるようにしてから、プレイグラウンドで実際にコードを書いていく流れにしましょう。
コンディショナルコンフォーマンスとは、特定の条件が満たされた場合に限り、プロトコルに準拠させる機能です。たとえば、型の拡張でプロトコルに準拠する際に条件を設定できます。この機能はSwift 4ぐらいから搭載されました。そのため、Swiftの学習をそれより前から行っていた人には、もしかすると見落としていたり浅く触れていたりする箇所かもしれません。
書き方はシンプルで、条件付きでプロトコルに準拠することを記述します。例えば配列を例にするなら、要素が整数型 (
Int
) の配列に対してのみ特定のプロトコルに準拠する、という書き方になります。
このコンディショナルコンフォーマンスが導入される前は、プロトコルに準拠させる部分には条件を付けられなかったが、型に応じた機能追加はできました。これができるようになった点が大きな利点です。では、実際にプレイグラウンドでコードを確認してみましょう。
まず、以下のようなコードを書いてみます。
extension Array where Element == Int { func something() { print("This is an array of Int") } }
この場合、配列
A
が整数型の配列なら
something
メソッドが呼び出せますが、配列
B
が文字列型の配列の場合は呼び出せません。こういった条件付き機能追加は以前から可能でしたが、プロトコル準拠に対する条件設定はできませんでした。
次に、もっと複雑な例として、カスタム型を用いて見ていきましょう。例えば以下のようにします。
protocol P { // プロトコルの定義 } struct CustomType: LosslessStringConvertible { // カスタム型の定義 var description: String { return "CustomType instance" } init?(_ description: String) { self.init() } } extension Array: P where Element: LosslessStringConvertible { // 条件付きでプロトコルPに準拠 }
このコードでは、
LosslessStringConvertible
プロトコルに準拠する要素を持つ配列がプロトコル
P
に準拠します。
CustomType
という独自の型を使ってみました。次に、以下のコードでそれを試します。
let arrayA: [CustomType] = [CustomType(), CustomType()] let arrayB: [String] = ["a", "b", "c"] var p: P = arrayA // これはコンパイルエラーなく代入される // var p: P = arrayB // これはコンパイルエラーになる
このように、条件付きのプロトコル準拠を利用することで、より柔軟に型の機能を拡張できます。以上がコンディショナルコンフォーマンスの基本的な使い方と意味です。次回はさらに具体例を通じて理解を深めていきましょう。 ただし、コンディショナルコンフォーマンスの条件に一致しないBの方は、コンパイルエラーとなり代入できません。こういうふうに、同じ配列型であっても、プロトコルの準拠状況が異なり、AとBの間に大きな変化があるわけではないように見えるけれども、中身が
Int
String
で大きく異なるので、目で見てもその差は明らかです。また、プロトコルの準拠性も異なり、コンパイル時にどのようになるかという部分でも影響を受けます。例えば、要素が
Hashable
なら配列も
Hashable
になる、といったことができるのがコンディショナルコンフォーマンスです。
ここまで書いたときに、これと似たものを思い浮かべる方もいるかと思いますが、コンディショナルコンフォーマンスと、例えばトラクトを定義してこれがバリューで
Hashable
としたときに、この中身が
Hashable
なら自動的に実装が追加される、というものとは混同しがちです。しかし、これは別のお話です。たとえば、次のようなコードがあります。
struct Example: Equatable, Hashable {}
この9から12行目まではプロトコルの自動準拠に関する内容です。自動準拠は一般にプロトコルの合成と呼ばれます。こちらがコンディショナルコンフォーマンスです。雰囲気は似ていますが、異なるものです。エレメントが
Hashable
ならプロトコル
P
に準拠させる、というように、全く関係ないものを規定することもでき、そのコードを
P
に準拠する型として扱うことができます。
Int
型と
String
型の配列も
P
に準拠させる、というのがコンディショナルコンフォーマンスです。
これぐらいで話はだいたい終わりです。プロトコルの型として使うのももちろんですが、例えば、関数があってそれが何らかの型
P
を取るときに、その型が
P
に準拠している、というような状況でも渡すことが可能です。コンディショナルコンフォーマンスのおかげで表現力がとても高くなり、エレメントが
Equatable
なら、それも
Equatable
にする、といったことが自由にできるようになり、配列の型であっても
Equatable
を想定した関数に渡すことが可能になります。
以前はオーバーロードをしなくてはいけないような複雑なことをしなければならなかったのですが、これが不要になりました。例えば、以下のようなコードで:
func myFunc<T: Equatable>(lhs: [T], rhs: [T]) -> Bool { return lhs == rhs }
これが、コンディショナルコンフォーマンスのおかげで以下のようにシンプルに書けるようになります。
extension Array: Equatable where Element: Equatable {}
標準ライブラリも、このコンディショナルコンフォーマンスが導入されたことにより、多くのオーバーロードが削減されています。その他にも、オプショナル型にも適用できることがあるかもしれません。たとえば、オプショナル型にもこのようなラップした型で有効にできるでしょう。 Swiftにおける存在型について説明していきます。前回の内容を振り返ると、プロトコルをそのまま型として扱えるというお話でしたね。プロトコルを型として扱うと、その変数はプロトコルで規定されている機能を利用できるようになります。
具体的な例を考えてみましょう。例えば、
SimpleDescription
というプロトコルがあったとして、このプロトコルには
description
というプロパティと
adjust()
というメソッドが含まれているとします。このプロトコルに準拠した型を使って変数を定義することで、その変数はプロトコルに準拠したすべての型の機能にアクセス可能になります。以下はそのコード例です。
protocol SimpleDescription { var description: String { get } mutating func adjust() } struct SimpleStruct: SimpleDescription { var description: String = "A simple struct." mutating func adjust() { description += " Now adjusted." } } var a: SimpleDescription = SimpleStruct() print(a.description) // "A simple struct." a.adjust() print(a.description) // "A simple struct. Now adjusted."
ここで
SimpleDescription
プロトコルを型として使用しています。この方法を存在型と呼びます。

注意点

存在型を利用する際に気を付けるべき点は、自動準拠が全てのプロトコルでサポートされているわけではないということです。例えば、
Equatable
Hashable
Codable
などのプロトコルは自動的に準拠できますが、
Comparable
などのプロトコルは自動準拠されません。これは、比較処理が複雑であり、何を基準に比較すべきか一意に決まらないためです。
以下は、
Comparable
プロトコルに準拠させる例です。
struct User: Comparable { var id: Int var name: String static func < (lhs: User, rhs: User) -> Bool { return lhs.id < rhs.id } static func == (lhs: User, rhs: User) -> Bool { return lhs.id == rhs.id } }
このように、
Comparable
プロトコルへの準拠は手動で定義する必要があります。

まとめ

今回紹介した内容をまとめると、プロトコルを型として扱うことで、汎用的で表現力の高いコードを書くことが可能になります。これは
Conditional Conformance
と呼ばれる機能に近く、条件付きでプロトコルに準拠させることができるため、非常に柔軟性が高まります。
それでは次の話題に進みましょう。次に取り上げるのは「シンセサイジングプロトコル」についてです。
struct NewStruct: Codable { var name: String var age: Int }
このように、
Codable
プロトコルに自動準拠されます。この機能を持つプロトコルがシンセサイジングプロトコルと呼ばれるものであり、Swift言語であらかじめ規定されている特定のプロトコルに限り自動準拠が行われます。
以上、存在型やコンディショナルコンフォーマンス、シンセサイジングプロトコルについて理解が深まったと思います。この知識を活用して、より効果的にSwiftのコードを書いてください。次に進む前に、何か質問があればお答えします。 存在型と呼ぶらしいということで紹介しておきます。世間でいろいろ情報を調べていると、「存在型」という言葉が普通に出てくると思うので、存在型と言われたらこれなんだなと思い出してもらえればオッケーですね。昔はプロトコルasタイプみたいなふうに言ってたと思うんですが、それがいつの間にか表現が変わっていたりするので、表現をブラッシュアップしているのかなと思います。
では、存在型の特徴をざっくりと見ていきましょう。存在型というのはどういったものかと言うと、プロトコル
P
があったときに、これを型としてそのまま使えるよというシンプルなお話です。例えば、
Int
型が
P
に準拠していたとするならば、普通に
P
型として整数
Int
型を入れられるよということです。
あまりこの書き方をしたことはないけど、まず動くかどうか。動くはずなんですけど、ここの解釈が
Int
型のリテラルを
P
に代入しているとき、たぶんこれ最初に
Int
型として解釈して、それをプロトコル型として入れているという、こういう解釈になっているはずです。要は、
Int
型を
P
型に入れるよという話をしたいだけなんですが、こういう動きをしていると思います。
例えば、以下のような感じですね。
var p: P = 10
これも動くでしょうね。タイプエイリアス
IntegerLiteralType
を例えば
Double
としたとき、これはコンパイルエラーになってくれるかな。やっぱりそうですね。まずリテラルが指定の型に変換された後に
P
に代入しようとするという動きです。その結果、このようなエラーになります。
typealias IntegerLiteralType = Double
とりあえず、今回はこの辺りの特徴をおさらいします。あくまでもプロトコルを型として扱えるようになるのは、プロトコル自体がジェネリックでないとき、もうちょっと分かりやすく言うと、アソシエイティブタイプを持っていると、実際に
P
にどんな型が割り当てられるかが状況によって変わってくるので、こうしたときにはプロトコル
P
をそのまんま型として使うことはできないのです。
例えば、以下のような場合ですね。
protocol Q { associatedtype T }
こうしたときに、
P
を型として使うことはできないのです。ジェネリックなものは使えないよという仕様になっています。ただ、これを使えるように一定の条件下で使えるようにしようという話もあったりするみたいですが、とりあえず現状は使えないと思ってもらってOKです。
また、ジェネリックなプロトコルができないということなので、他にも例えば関数として何かがあって、それが
Self
を返すよみたいな関数を求められる場合も同様に使えないのです。以下のような感じですね。
protocol P { func x() -> Self }
こういった場合も
P
を型として使えません。ただ、リターンとして
Self
を返す場合は問題ないです。例えば以下のコードは動きます。
protocol P { func x() -> Self } struct A: P { func x() -> Self { return self } }
一方、パラメータとして
Self
を取る場合はジェネリックと見なされてしまい、使用できないのです。例えば以下のコードはコンパイルエラーになります。
protocol P { func x(v: Self) }
この差は何なのかというと、リターンとしての
P
型は
P
型として扱えると見なせるのに対して、引数としての
P
型はそのままでは扱えないからだと考えられます。
以上で存在型についての説明は終わりです。これで疑問点などがあれば、もう少し掘り下げていくこともできると思いますので、何か気になることがあれば教えてください。 とりあえず、ジェネリクスがエラーになるでしょ。これが要は、
p
がセルフ型だとすると、これが使えたとすると、
x
を呼び出すときに
v
p
を求めてるんだけど、どんな型が渡ってくるか分からない。戻り値がセルフだったときには
p
型が得られるけど、受け取るときにはこの
p
がどの型になるか分からない、みたいな感じになるから、ここがジェネリクスっていうことですね。
なるほど。さっき間違えて書いた、ここが
p
だったときにはジェネリクスじゃないんだ。なるほどね、そういうことか。戻り値とパラメーターでは結構性質が違ってくる感じね。性質の違いっていうと、またいろいろと面白いところがあるんだけど、今回はプロトコルの話だから、プロトコルの型の特徴で話していってみようかな。ジェネリクスに限らず。
とりあえずプロトコル
P
があったときに、それをファンクションが受け取るとして、それで型は何でもいいけど、
P
に準拠しているもの、みたいなふうに書いたりするじゃないですか。ジェネリクスのときね。こういうふうに書いてあげると、どんな型でも受け取ってくれるんですけど、例えばこれが明確にプロトコル型だったとき、こうしたときに
P
型だったときの値というか、渡せるかどうかっていうのが変わってきて、例えばエクステンションでインスタンスがプロトコル準拠して、これでコンパイルを通して、それでこのときにアクションに
P
型としてバリューを渡せないっていうやつがあるんですよ。ジェネリクスのときだとこれを渡せるんだ。
P
型なら渡せるけど、タイプが
P
に準拠しているっていうふうにして
P
を渡したとき、このときにコンパイルエラーになるんだ。そう、ここね、これが面白い特徴。
要は、
P
型のときと、こうやってジェネリクスで
P
P
のときで、
P
というのがこの型として扱えたときに、アクション1と2で明暗が分かれて、
P
型として受けるときはOKで、ジェネリクスとして受けるときはダメ。つまり、このバリュー型は
P
型なんですけど、この
P
型自体はプロトコル
P
に準拠していないよっていう特徴がある。これはなぜかというと、この
P
というのがプロトコル
P
に準拠していると見なせるなら、見なせちゃったほうがいいと思うんですけど、見なせない理由があるからこうなってるっていうところで。
これはどうなんだっけかな。静的メソッドやイニシャライザーの関係でダメなんですよね。例えば、イニシャライザーがとても分かりやすいのかな。例えば、プロトコル
P
がイニシャライザーを求めるみたいなふうになっていたときに、このプロトコル
P
型がプロトコル
P
に準拠するためにはイニシャライザーを持っていないといけないわけですよ。逆に言うと、プロトコル
P
P
型が準拠していたとすると、イニシャライザーを呼び出せるっていうことになりますよね。
そうすると、このアクション1の中で
V
に対して
P
のタイプを取ったとするとか、
P
こっちでいいかな、
type(of: P).init
これが呼べるはずなんですよ。これは呼べちゃうかな、ダイナミックでやってるから呼べちゃうかもしれないな。タイプにしようか。こういうふうなときにね、
P
init
を呼べるっていうことになるわけなんですけど、実際に
P
型っていうのは具体的な型を持ってないわけですよ。そうすると、このイニシャライザーは規定されているけれど、読んだとなると何がインスタンス化されるか、ここが論理的なコードにならない。
ジェネリクスの場合は同じことを書いても、例えばこうやって書いてもね、
P
という型は
P
に準拠しているけれど、具体的な型は別にある。例えば
Int
型とかね、というふうにあるので、このコードを読んだときに、10行目で
Int
型を生成するみたいなコードになってくるんですよ。こういうふうにジェネリクスであれば成立するけど、プロトコル型と成立しない。こういうふうな都合で、プロトコル
P
を型
P
として使ったときに、プロトコル
P
に準拠するわけにはいかないっていう、こういう都合があって、この辺りが呼べなくなってくる。
今ちょっとコード変えちゃったからあれだけど、要は
P
型として扱ったときに、
P
型は
P
のプロトコルには準拠できないよっていう制約が課せられている。ただ、いくつか特別な場合においては、あるプロトコルを型として使ったときに、そのプロトコルにも準拠しているとみなしていいよね、みたいな状況があって、そのときに特別な配慮として自動準拠するようにもなってたりする。その辺りはちょっと今日は時間がないので、また次回に軽くお話ししようかなって感じにしますけど、そういうふうに特別扱いするところもあります。
ちょっと複雑な話になっちゃったけど、また次回おさらいする感じでいってみますかね。その辺りの自動準拠される場面も含めてね。はい、じゃあこれで今日の勉強会は終わりにします。お疲れ様でした。ありがとうございました。