https://youtu.be/doBk6ajNsik
前回までで The Basics
の 型エイリアス
のところを見終えましたけれど、次のセクションに移る前に、型エイリアスともそれなりに関係性のある プロトコル
の 関連型
に少し寄り道をしてみていこうと思います。それをみ終えたら次は 論理値
のセクションに入っていく予定です。どうぞよろしくお願いしますね。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #127
00:00 開始 00:10 今回の展望と進め方 01:42 型エイリアスと関連型 02:25 ジェネリックな入れ子の型 03:48 プロトコルと関連型 07:16 関連型と総称関数 08:08 関連型の具体的な型は? 09:14 関連型に制約を添える 11:49 関連型を型推論で特定する 12:55 型エイリアスと関連型の関係性 17:18 関連型を型エイリアスで書くか推論させるか 20:39 型拡張するときの関連型の表現のしかた 22:19 プロパティーのオーバーロードはできない 23:29 戻り値によるオーバーロードと型推論 26:59 オーバーロードか、サブタイピングか 27:54 サブタイピングとは 28:48 オーバーロードではなくジェネリクスで対応する場合 ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #127
始めていきますね。今日は型エイリアスについてです。前回、基礎部分の項目は一通り終わりましたが、折角なので利用場面についてもう少し掘り下げてみたいと思います。これは前回最後にお話ししたスライドの続きです。この利用場面から派生して型エイリアスに触れます。今回はプロトコルの関連型にも触れつつ、おさらいをしていこうと思います。
今日は特に構成などは用意せず、フリーな感じで進めていけたら良いなと思います。気になるところや、脱線してでも知りたいことがあれば、ぜひ気軽に質問してくださいね。
まず、プロトコルの話に移る前に、型エイリアスの基本を確認していきます。型エイリアスを理解するために、今回は実際のコードを使って説明します。
例えば、昔の話になりますが、プロトコルを定義したときに、ジェネリックなプロトコルにしようと思ったときに関連型を定義する必要があります。以前は、関連型をtypealias
というキーワードを使って定義していました。しかし、現在ではassociatedtype
というキーワードが使われています。これにより、関連型と型エイリアスが明確に区別されるようになりました。
さて、具体的な例を見ていきましょう。例えば、MyProtocol
というプロトコルがあり、その内部にElement
という型を持つとします。この型を定義するために、昔はtypealias
を使っていましたが、今はassociatedtype
を使います。以下のように書くことができます。
protocol MyProtocol {
associatedtype Element
var item: Element { get }
}
このプロトコルを満たす型を定義する場合、typealias
を使って具体的な型を指定します。例えば、以下のようにMyValue
型を定義できます。
struct MyStringValue: MyProtocol {
typealias Element = String
var item: String {
return "Hello"
}
}
同様に、MyIntValue
型を定義することもできます。
struct MyIntValue: MyProtocol {
typealias Element = Int
var item: Int {
return 1
}
}
これにより、MyStringValue
型はString
型のアイテムを、MyIntValue
型はInt
型のアイテムを持つことができます。このようにプロトコルに準拠した型を作成することで、それぞれの型が異なる具体的な型を持つことができます。
さらに、プロトコルに準拠したすべての型に共通の関数を定義することもできます。例えば、以下のような関数を定義することができます。
func printItem<T: MyProtocol>(value: T) {
print(value.item)
}
この関数を使用して、MyStringValue
とMyIntValue
のインスタンスのitem
を出力することができます。
let stringValue = MyStringValue()
let intValue = MyIntValue()
printItem(value: stringValue) // 出力: Hello
printItem(value: intValue) // 出力: 1
このようにして、プロトコルと関連型を使って柔軟なコードを作成することができます。今回の説明は以上です。質問があれば、どうぞお気軽にお知らせください。 このときにエレメント型という型になっている、このエレメント型が具体的に何型なのかというところまでは、この35行目のアクション関数では触れられていないので、ある意味「Any型」というべきでしょうね。メーカーではそうは言わないけれども、要はマイプロトコルで規定されているエレメント型です。エレメント型は特に制約がされていないので、Any型のように扱われます。
今回、戻り値の型としてAnyを指定しているので、これでコンパイルも通るでしょう。実際にアクションを呼び出すと、ここでマイストリング型のものも渡せるし、マイイント型のものも渡せるといった具合に、ジェネリックプログラミングが可能になるわけです。しかし、ここでアイテムがAnyでは、この先で具体的な操作ができないので、もう少し細かい型制約を提供する必要があります。
例えば、エレメント型がカスタムストリングコンバイルに準拠しているとします。これを指定してあげると、description
プロパティが使えるようになります。このままだと結果が変わらないので、Hashable
に指定を変更します。Hashable
を指定すると、hashValue
が取得できるようになり、例えばストリングが規定しているエレメントがストリング型、イントが規定しているエレメントがイント型であるどちらでも、Hashable
に準拠しているんでアクションに渡せます。つまり、どちらでもちゃんとハッシュ値が取れるということですね。これはジェネリックプログラミングの基礎の話になります。
また、マイプロトコルのところで、例えばアソシエイティブタイプにエレメントを持っていますが、ここで「何でもいいけどHashable
には準拠してね」という制約をかけることができます。そうしてあげると、Hashable
に準拠しているものしか指定できなくなります。今回、ちゃんとHashable
なもの同士なので、そうやって使えるようになります。
例えば、35行目でマイプロトコルのアイテムエレメントがHashable
に準拠している場合は、前述のようなWhere句を書くことも可能です。プロトコル自体にエレメントがHashable
なので、このアイテムプロパティ自体はエレメント型として指定されます。ただ、これがHashable
なのでちゃんとハッシュ値が取れることがわかります。
このように関連があり、タイプエイリアスが普段は使われないかもしれませんが、ここではタイプエイリアスを用いてトリン型として使うことができます。これにより、9行目を書かなくてもコンパイルが通るようになります。
最後に、タイプエイリアスは、元々ある型の別名を持っているわけです。この9行目の書き方を見ても、マイプロトコルの中で定義されているのは関連型なので、入れ子になっている型を別名として扱っているわけでもありません。この関連型エレメントというのは、コメントアウトされていないアソシエイティブタイプとして書かれています。イコールは不要ですが、別にこれに対応するのが9行目というわけでもないのかもしれませんね。 関連づけてプロトコルにおけるアソシエイティブタイプについて考えてみます。私自身、アソシエイティブタイプとタイプエイリアスを1対1で考えていたんですが、これも型推論によって適切に判断されるということなんですね。
例えば、外側から見るとわかりやすいですが、ある型 MyInt
がエレメント型を持っています。これは既に存在する型で、プロトコルに規定されているものです。つまり、MyInt
のエレメントは既に存在しており、未来のどこかの時点でも確実に存在すると仮定できます。これがタイプエイリアスの存在理由として理解できます。
具体例で考えてみましょう。例えば、struct
を使ってエレメントを規定し、このエレメントを用いてタイプエイリアスを使わずに具体的な型を定義します。このようにすることで、既にある型で MyInt
のエレメントを定義することができるのです。また、プロトコルで規定した場合でもコンパイルがうまく動作するようにできます。
独自に型を規定するか、既存の型をタイプエイリアスで使うかについてですが、この選択は状況によって異なります。例えば、struct
で Something
型を具体的に定義する場合と、タイプエイリアスで Something
を他の型から規定する場合は、どちらも同じような感じになります。
つまり、型推論を使ってアソシエイティブタイプを適切に選ぶことで、タイプエイリアスを使用する必要がなくなる場合があるということです。これは一般的には、多くのケースでタイプエイリアスをわざわざ作らずに、エレメントをそのまま使う方がよいかもしれません。
しかし、特定の型が複数回登場する場合(例えば、二回以上)、その時はタイプエイリアスを使う方が読みやすさや保守性の観点から好ましいこともあります。一般的なガイドラインを決めるのは難しいですが、二回以上同じ型が登場する場合には、エレメントの方を選ぶことが多いです。
これは確かに読み手にとってもわかりやすいと思います。特にエレメントが複数回登場する場合に、タイプエイリアスで抽象化した方が良いことが多いです。一方で、一貫性を持たせる必要がある場面では、エレメントという用語で統一するのもよいでしょう。
このようにして、具体的な型と抽象的な型を使い分けることで、コードの見通しや保守性が向上します。総じて、この選択は状況に応じて柔軟に対応するべきです。 ここが「エレメントを想定している」ことが分かりにくくなってくる場合があります。ディスクリプションを持ったときに、ここで使われるストリングがエレメントではないことが見えにくくなるのです。そのため、統一させるためにタイプエイリアスが使われますね。自分の場合、何らかの事情でタイプエイリアスを書かないことがありますが、型推論で済むならとそのままにしてしまうこともあります。しかし、型推論を使うとどこか一箇所で矛盾が生じることがあります。そういうときはタイプエイリアスで推論させたほうが良いのです。
ジェネリック型を拡張するときの話も少し異なります。たとえば、ここでエレメントがストリングだと指定したとしても、実装する際にはあえてストリングと書かず、エレメントとして扱うことがわかりやすくなります。また、エレメントを受け取るときに、明示的にストリングと記述しないほうが適切です。ストリングよりエレメントのほうが一般的で意味が通じやすくなりますよね。
さて、タイプエイリアスのメリットですが、同じインターフェースで別の型を定義する場合などがあります。インターフェースの一貫性を保つために、タイプエイリアスを省略せずに記述するのも良いでしょう。コード量が多いと省略したくなるかもしれませんが、結果的に明示的に記述することで、コードが理解しやすくなります。
プロパティの戻り値のオーバーロードはできませんが、プロパティ自体は明示的な型を持つことができます。例えば、アイテムとして同じインターフェースになるとき、タイプエイリアスを使用しないとコンパイルが通らないことがあります。 MyProtocol
が想定するエレメントがストリング型だと指定している場合、それに従わなければなりません。
ここで、タイプエイリアスを使うことで、エレメントがストリング型であることが明示され、コンパイルが通ります。オーバーロードされている場合でも、タイプエイリアスを使うことでどのメソッドを呼び出すべきかが明確になります。このように、タイプエイリアスはオーバーロードの際などにも役立ちます。
今回の例では、プロトコル MyProtocol
が言うエレメントはストリング型だと指定することによって、意図がはっきりします。このようにタイプエイリアスを使うことでコードの一貫性を保ち、プロトコルに準拠していることが明確になります。 こっちはプロトコルとは関係ない Int
、こっちはプロトコルと関与するよ、というのが明確に示せますよね。普通にわからないこともあると思いますが、こういう場合もあるんです。
もし戻り値の方がいろいろと想定されるような場合、サブタイピングはどんな風に扱われるのでしょうか。サブタイピングの親というのは何ていうんでしたっけ?サブクラスの親はベースクラスですよね。サブタイピングの親子関係は何て言うんでしょうか。ちょっと調べてみますね。
サブタイピングである方と互換性がある方について考えると、ポリモーフィズムの形式ですね。ポリモーフィズムってサブタイピングとオーバーロードがいろいろありましたけど、詳細を忘れてしまいました。上位型と下位型、部分型と言った方がいいかもしれません。サブタイピングについてもう少し話しましょうか。
ポリモーフィズムについてですが、サブタイピングはオブジェクト指向ではよく知られています。しかし、具体的な例があまり出てこないですね。AとBがあって、AがBの部分型、サブタイプとなるという話があります。部分型という考え方が出てこない場合もありますね。
例えば、ここが String
型ではなくて CustomStringConvertible
とか CustomStringConvertible
を想定している場合は、そのような形になります。もし RangeReplaceableCollection
の方が適している場合、こういう風にしてあげます。これでコンパイルが通れば、RangeReplaceableCollection
のアクションがどうなるのかを確認します。オーバーロードしないとのことでしたね。この形を取ることで、特定のケースに対応するというわけです。
さて、いろいろな話をしましたが、タイプAPIのようなものにも使えるかもしれません。良い時間になりましたので、今日の勉強会はこれで終わりにしましょう。お疲れ様でした。ありがとうございました。