今回は、これまでに話した 存在型
と Error
プロトコルの特色的なところ。以前に幾度かその特徴について話してきましたけれど、最近の Swift バージョンではその挙動が大きく緩和されてこれまでの話と様子が変わったので、その補足に当ててみようと思います。もしかすると以前にも話した話題かもしれないですけれど少しややこしいところでもあるので、もし記憶にある人は復習なつもりで聞いてみるのも良いかもしれないです。どうぞよろしくお願いしますね。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #189
00:00 開始 00:29 これまでの訂正の話 02:13 存在型とは 03:14 存在型が自己準拠していると見做される 03:43 以前は存在型は原則、自己準拠しない 06:39 以前にも見られた Error の自己準拠性 08:12 存在型が自己準拠していると見做す仕様に変更 09:13 存在型の自己準拠性による表現力の向上 15:16 型パラメーターでは自己準拠とは見做さない 19:37 存在型の展開とメモリーサイズ 24:16 静的メンバーに起因する制約との上手な折り合い 25:55 静的メンバーを持っていたとしても呼べないだけ 27:48 静的メンバーを動的キャストで呼び出すことは可能 28:36 存在型における静的メンバー呼び出しについてのまとめ ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #189
はい、じゃあ始めていきましょう。今日は「存在型」、エクジステンシャルタイプについて学びます。エクジステンシャルタイプと言えるようになってきましたね。これまでずっと「エクステンシャルタイプ」と勘違いしていましたが、正しくはエクジステンシャルタイプです。
今日お話ししようとしている話題は、以前にも他の勉強会で話したかもしれませんが、何回話しても復習になりますし、特に複雑な部分なので自分も今日話せるかどうか不安なところがあります。もし以前聞いたことがある人がいたら、今回新たに気づいたことがあるかもしれませんので、また興味深く聞いていただければと思います。
エクジステンシャルタイプについてですが、基本的にエクジステンシャルタイプは「自分自身のプロトコルには準拠しない」という特徴があります。しかし、これは例外が存在することもあります。今日はこの点について詳しくお話しします。これまでこの勉強会で話してきた内容の一部を訂正する形になるかもしれませんが、ご了承ください。
まず、存在型とは何かについて説明しましょう。存在型(エクジステンシャルタイプ)という言葉は、プロトコルを型として扱う考え方です。例えば、変数がカスタムストリングコンバーティブルな型であれば、何でも入れることができます。例えば、文字列も整数型も入れることができるといった感じです。最近のSwiftでは、アソシエイティブタイプを使ったプロトコルであっても存在型として扱うことができるようになりました。その際、「Any」というキーワードを使用します。
具体例を示すと、以下のようなコードで表現できます。
protocol CustomStringConvertible {
var description: String { get }
}
class MyClass: CustomStringConvertible {
var description: String {
return "MyClass instance"
}
}
let value: Any = MyClass()
このように、カスタムストリングコンバーティブルな型を変数として扱うことができます。
ここで重要になるのが、「カスタムストリングコンバーティブル」に準拠した型であるかどうかの判定です。以前はこの判定がうまく行かないケースがありましたが、最近ではジェネリクスに渡すことも可能になり、安定して動作するようになりました。
例えば、以下のコードも正しく動作するようになりました。
func printDescription<T: CustomStringConvertible>(_ value: T) {
print(value.description)
}
let value: CustomStringConvertible = MyClass()
printDescription(value)
以前は、このようなコードが期待通りに動かないことがありましたが、最新のSwift(5.7以降)では正しく動作します。この改善により、コードを書く際の柔軟性が増し、エラーも少なくなっています。
この勉強会では、以前はエラープロトコルに関連する議論として、この話をしていましたが、最新のSwiftでは状況が変わってきています。一応、最新の情報も踏まえて記憶に留めておいてください。
それでは、引き続き存在型についての学びを深めていきましょう。一緒に頑張りましょう。 カスタムストリングコンバーティブルをエラープロトコルにして、エラープロトコルに準拠しているエラーを何か入れようとしています。使いやすいエラーが見つからないので、独自に作らなければならないかもしれません。その上で、アクションの引数としてエラープロトコルに準拠したものなら何でもよいというふうに設定します。これでも、このプレイグラウンド上ではちゃんと動きます。
このエラープロトコルの例は、5.7のバージョンでまずエラーなく動くことが確認できます。エラープロトコルに関しては5.6.3でも動くという話です。特定の動作は知る人ぞ知るエラープロトコル特有のもので、他ではうまくいかない場合もあります。このように、カスタムストリングコンバーティブルを用いた取り組みは重要です。
エラープロトコル以外のものを使うとき、Denelixとの兼ね合いで使うことを避ける場面が出てくることもありました。それが改善されて使いやすくなった点は、見落としがちですが重要です。また、some
とany
の強化も5.7から始まっています。この変更により、Denelixが非常に使いやすくなりました。カスタムストリングコンバーティブルに準拠しているものをわざわざパラメータに作らなくても、簡単に記述できるようになったのは5.7の大きな特徴です。
これにより、プロトコル、特に存在型がほぼ普通の型と同等に利用可能になりました。また、Any
キーワードとアソシエティブタイプが使われているものも、存在型として利用できるようになり、コードが動作することが確認できると興味深いです。例えば、AnyCollection
やAnyHashable
として一般的なジェネリックとして受けるという点が重要です。
エニーコレクションの配列を使うときには、例えばイメージとして、エニーコレクションも良いけれど、AnyHashable
にしてみます。そして、この中には何でも入れることができます。例えば、ブール値も入ります。このようにして、コードを書いて動作確認をすると非常に面白いですね。 この部分では、Swiftのプログラミングにおける型やコレクションに関する議論が行われています。特に、コレクションがどのようにファッシャブル(Iterable)な存在型として扱われるかについて説明されています。
コードの中では、values
というコレクションが使われ、それぞれのアイテムがファッシャブルなものであることが必要とされています。例えば、以下のような感じです。
let values = [1, 2, 3]
for value in values {
print(value)
}
このコードの中で、values
の型がAny
であっても、この中の要素がファッシャブルであることが保証されている場合、普通に動くという話です。
具体的な例として、以下のような関数を定義するとします。
func printValues<T: Collection>(_ values: T) where T.Element: CustomStringConvertible {
for value in values {
print(value.description)
}
}
ここで、T
がコレクションで、その要素T.Element
がCustomStringConvertible
プロトコルに準拠している(つまり、description
プロパティを持っている)ことを前提としています。このため、以下のように使うことができます。
let values = [1, 2, 3]
printValues(values)
このように、Swiftではプロトコルに準拠しているかどうかで型を制約することができます。ただし、特定の状況でコンテナそのものがそのプロトコルに準拠しているとは見なされない場合もあります。例えば、セットと配列の違いについても触れられています。
let array: [Int] = [1, 2, 3]
let set: Set<Int> = [1, 2, 3]
printValues(array) // OK
printValues(set) // エラー
ここで、配列は通るがセットは通らない状況があります。これは、セットが特定のプロトコルに準拠していないためです。
また、要素がファッシャブルであるかどうかの制約がある場合、コレクションの動作が変わることもあります。リテラルにおいては、特に記述がなくても型が自動で解釈されることが多いです。例えば、リストリテラルは自動的に配列として解釈されます。
let literalArray = [1, 2, 3] // これは配列として解釈される
このような点を考慮に入れながら、プロトコルを使った型の制約や存在型の扱い方について理解を深めていくことが重要です。これは柔軟かつ安全にコードを構築するための重要な要素となっています。 つまり、配列が特殊なわけではなく、どんな型でも同じです。自分で作った型でも同様で、例えば、struct MyStruct<T>
とすることができます。このT
がHashable
やEquatable
でなくても構いません。どのプロトコルでも適用できます。例えば、CustomStringConvertible
のような単純なプロトコルでも良いのです。
その際、MyValue
としてCustomStringConvertible
を扱うなら、CustomStringConvertible
として扱えないという制約があります。例えば、型パラメータT
では大丈夫なことが、型の二重階層のパラメータでは問題となります。おそらく、型パラメータを提供して使用する必要がありますが、それ以外は問題なく利用可能です。入れ子になっていない型においては、Any
を使用してどんどん進めることができます。
これが非常に強力です。特例として面白い点は、存在型としてではなく、コンパイラがインスタンスの実際の型として展開する場合に特定の振る舞いを見せる点です。表現が難しいかもしれませんが、メモリ上のサイズや型の扱いに注目してみると理解が深まります。
例えば、AnyHashable
は40バイトですが、内部の具体的な型がInt
であれば8バイトに展開されることがあります。これは普通にソースコードを書いていてもできないことですが、コンパイラが最適化を行ってくれるおかげです。
また、String
は16バイト、Double
は8バイト、Bool
は1バイトというように、それぞれの型サイズに最適化されます。プリントや関数に渡す際に本当に40バイトかどうか、最適化によってバイト数が変わる可能性もあります。それを確かめるのは面白いことでしょう。
このように、Swiftのコンパイラは多くの最適化を自動で行いながら型の展開やメモリ効率を最大化する機能を持っているのです。このような仕組みを理解し活用することが、Swiftでの効果的なプログラミングに繋がります。 仮に中間言語で今は 40 バイトにして渡して、ここで 1 バイトのものを使うということになっていたとしても、将来的には最適化が図られ、バイパスして完全に 1 バイトで扱う可能性もあります。もちろん、この段階では少ないかもしれませんが、コレクションの場合は可能性がなくなるかもしれませんね。しかし、もっとシンプルなジェネリックであれば、最適化によって O(n^2)
で扱っていたものが O(1)
で扱えるようになるといったことが起こるかもしれません。そういった最適化の楽しみがあります。
これが Swift 5.7.1 での面白いところで、エラープロトコルの特例だったものが、すべてのプロトコルに展開されるようになっています。この進化は非常に興味深いです。以前問題型の話をしたときに、なぜ自分自身が自分自身のプロトコルに準拠していない仕様になっているかという話がありましたが、それはスタティックメンバーを扱うときにプロトコルの存在型をそれ自身に準拠させると問題が起こるからです。スタティックメンバーに問題が生じるため、仕様としてそうなっていないのです。
この話は広く知られているのですが、それでも自分自身にも準拠するという特例がある場合は、たとえば Objective-C のプロトコルにはスタティックメンバーがない場合に限って自分自身にも準拠するというものがあります。将来的には、Swift ネイティブのプロトコルでもスタティックメンバーがなければ自分自身のプロトコルに準拠するものとして扱える可能性があります。これが面白いのは、許す限り自動的にプロトコル展開してくれるという仕様です。これにより、スタティックメンバーがなければ問題なく動作するので、それ以上の心配は不要です。
具体的に言うと、プロトコルとして MyValue
を定義し、これがスタティックメンバー(たとえば static var
)を要求している状況を想定してみましょう。このとき、エクステンションとして Int
型に対して MyValue
を持たせ、スタティックメンバーとして適当な実装を持たせます。このようにしてから any MyValue
型にした場合、スタティックメンバーは呼べないという挙動になります。
スタティックメンバーを呼べない理由は、プロトコルのダイナミックな型ではメンバーが展開されていないからです。一方、具体的な型に展開された場合にはスタティックメンバーが問題なく呼び出せるので、影響はされません。
展開という概念が素晴らしく、使い慣れるのは少し難しいかもしれませんが、試してみると面白いことがたくさん発見できるでしょう。さらに、Boxing されているため、メモリアライメントも全く問題なく保てます。とても素晴らしい仕様です。
今回はこれくらいにしておきますが、ぜひ取組んでみてください。では、終わりにします。お疲れさまでした。ありがとうございました。