https://youtu.be/aaleSGDLRso
今回も引き続き The Basics
の 型エイリアス
について眺めていきます。おおよその特徴は前回に身終えた感じがするので、今回はそれが使われる場面について着目して行けたらいいなと思います。
それと、そんな話に入る前に、おとといに開催してきた勉強会で 型拡張
についておさらいしていたときに、重複する実装の辺りで記憶と違う挙動を見せて混乱したところがあったので、それについてもみんなと情報交換してみようと思ってます。よろしくお願いしますね。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #126
00:00 開始 00:10 今回の展望 00:56 既存の機能と同じ署名の機能を拡張したときの挙動 01:28 既にある実装を拡張してもエラーにならない 02:51 名前空間と重複実装 04:39 同じ定義を重ねることに感じる恐いところ 06:16 メソッドの隠蔽 08:02 いまいち不安の拭えないところ 08:29 イニシャライザーも重複可能 09:20 型エイリアスのおさらいと今回の展望 10:00 型エイリアスは元の型としても使える 10:39 型エイリアスで別名を付ける例 11:53 C 言語の文化とのブリッジ 12:32 if 文で真偽を判定するとき 14:03 型エイリアスとコードの可読性 16:13 型エイリアスを元の型として使う 17:14 元の型と同じと扱えるのも効果的かも? 18:34 とはいえ違和感も残るところ 19:08 サブタイピングを今日りょしたら上手くいきそう? 20:21 型エイリアスに対する所感 21:10 型拡張と型エイリアスの関係性 23:58 複雑な型表現を簡単にする 28:17 APIKit に思いを馳せてみたり 28:53 ライブラリーを提供するとき 29:38 ジェネリクスと型パラメーター —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #126
今日は前回の続きで型エイリアスのお話をしようと思っていたのですが、先日土曜日に表参道で勉強会を開きました。その際、自分の勘違いだったのか、違う動作を見た気がして、そのことについて話を進めます。
エクステンションについて話します。たとえば、String
型にタッチします。具体的には、以下のようなコードを考えてください。
extension String {
var description: String {
return "example"
}
}
let value = "abc"
print(value.description)
このコードは動くのでしょうか。実装がかぶると思っていたのですが、かぶらないのですね。もちろん、実装を省けば、本来のFoundation
ライブラリのdescription
が動作するということです。
この動作について、自分の中で完全に理解できていなかったので、例を挙げて考えてみます。たとえば、私が次のような型を作る場合を考えました。
struct MyValue {
var action: Int {
return 1
}
}
extension MyValue {
var action: Int {
return 2
}
}
これも同じように、再定義と見なされてエラーが出るでしょう。
しかし、調べてみると、名前の隠ぺい(shadowing)の問題でなければ再定義が実装できることがわかりました。例えば、MyValue
の定義がPlaygroundのルートにあるとき、以下のようにコンパイルが通ります。
public struct MyValue {
public var action: Int {
return 1
}
}
extension MyValue {
public var action: Int {
return 2
}
}
let value = MyValue()
print(value.action) // 2
一方で、別のモジュールで定義されている場合はどうでしょうか。Playgroundのソースディレクトリに新しいファイルを作り、そこで定義します。他のモジュールからアクセスするためには、このようにパブリックにします。
// 別のモジュール
public struct MyValue {
public var action: Int {
return 1
}
}
// 元のモジュール
extension MyValue {
public var action: Int {
return 2
}
}
let value = MyValue()
print(value.action) // 2
この別のモジュールに定義されたMyValue
に対して同じ拡張をすると、問題が解消されるはずです。
エクステンションについてさらに考えてみましょう。次のコードを実行した場合、何が出るでしょうか。
public struct MyValue {
public var action: Int {
return 1
}
}
extension MyValue {
public func f() -> Int {
return self.action
}
public var action: Int {
return 2
}
}
let value = MyValue()
print(value.action) // 2
print(value.f()) // 1
直接action
を呼んだときと、f
を経由してaction
を呼んだときの挙動を見てください。f
経由では1が返され、直接呼ぶと2が返されます。これはC++の隠ぺい(encapsulation)の挙動ですね。
プロトコル拡張も同様に動作します。型の拡張でも、特に問題なく同じ動作が見られます。それが今回の勉強会で再確認できた点ですね。
このような挙動について、皆さんも覚えておくと役立つことがあるかもしれません。今回の紛らわしい動作についても少し整理ができました。 型拡張について説明します。型拡張を用いると、型の隠蔽が可能になります。ただ、オーバーライドになってしまうと問題が発生しますが、普通の構造体であれば問題ありません。例えば、イニシャライザも重複検出されずに使用できます。self.init
などでダミーを作成してもコンパイルは通ります。
例えば、次のようなコードがあります。
let s = String("dummy")
このように、文字列の初期化を行います。少しわかりにくいかもしれませんが、基本的には問題ないです。
次に、型エイリアスの話をします。前回、基本的な動作について説明しました。型エイリアスは typealias
キーワードで定義し、どんな型に別名をつけて便利に使うために書くものです。もう少し具体的に、どんな場面で使われるのかを見ていきましょう。
例えば、外部ソースで特定のサイズのデータを扱うときに、型エイリアスを用いることでわかりやすい名前に変えられます。そしてその名前を使っていけるのです。次に、具体的な例を見てみましょう。
typealias AudioSample = UInt16
このコードでは、AudioSample
という名前を UInt16
の別名として定義しています。これは、UInt16
型のオーディオバッファを提供するライブラリを使用する際に、UInt16
という型名が漠然としすぎているため、AudioSample
という名前を与えています。
こうして定義した AudioSample
は型エイリアスであるため、例えば AudioSample.min
とすると、実際には UInt16.min
が呼ばれます。すなわち、AudioSample
と書かれた部分はすべて裏では UInt16
に置き換えられます。
具体的な使いどころとしては、曖昧なデータ型が存在する場合や、別のフレームワークをインポートする場合があります。例えば、C
ライブラリをインポートする際には、次のようなコードがあります。
typealias CInt = Int32
この場合、CInt
は Int32
の別名となります。また、Bool型を使う際には == true
といった形で比較を行うこともできます。
このように、型エイリアスを用いることでコードが読みやすく、メンテナンスしやすくなると考えられます。どちらも正しい使い方なので、状況に応じて使い分けるとよいでしょう。 例えば、ステータスを int32
として定義しておいて、それが適切でなければステータスは 0
、そうでなければステータスは -1
とします。このようにしてステータスを設定します。ただ、実行してみて考えたのですが、特に問題なくコードが動くことが確かめられれば良いです。
C言語の世界では、ステータスコードを int32
型、つまり32ビット整数として扱うことが一般的です。しかし、32ビットがどういう意味なのか、少し理解しにくい部分もあります。そのため、わかりやすくするために typealias
を使って、ステータスコードを int32
として定義し直すのは有効です。これにより、コードの可読性が向上します。
例えば、C言語由来の exit
を使って、ステータスを渡すとします。ステータス変数がどのように扱われているかを読むためには、14行目まで進めなければなりませんが、6行目で typealias
を使ってステータスが CStatusCode
であることがわかれば、それだけで簡単に理解できます。こうすることで、コードを追うのが少し楽になります。
次に、exit
を呼ぼうとしたとき、元々は int32
を要求していますが、CStatusCode
型のステータスをそのまま渡すことができます。これは typealias
を使っているためで、型変換が不要なのです。この点について説明すると、typealias
を使うことで型が違うことにはならず、そのまま int32
として動作します。
以前、typealias
を使うと他の int32
型にも渡ってしまうリスクを心配しましたが、それは杞憂でした。exit
が CStatusCode
を取ることを想定している場合、そのまま使えないのであれば、むしろ違和感があります。したがって、適切に typealias
を使うことで、コードは直感的に理解しやすくなります。 なので、この辺りも踏まえてみると、型エイリアスとして別名を付けたものが元の型と同等に扱えるというのは、確かに悪くない考えだと思います。ただ、C言語のライブラリを考えた場合、例えば int32
を返す関数があったとして、これにステータスコードを渡せてしまうというのは違和感があります。その理由については前回もお話ししましたように、int32
型を取ると明示されている場面でステータスコードを渡してしまうと、勘違いで誤った値を渡してしまう可能性があります。
こうしたことを総合的に考えると、サブタイピングの発想、つまり互換性のある型という考え方が重要になります。オブジェクト指向プログラミングではサブタイピングがよくあります。型エイリアスが元の型のサブタイプとして認識されると、例えば int32
にステータスコードを渡すことができるわけです。しかし、これでは問題が解決できません。ですから、型エイリアスの使用には慎重になるべきだと結論づけました。
型エイリアスの別名が元の型と同じように使えるというのは実用的ではありますが、型安全性に注目するとリスクがあるかもしれません。特に、エクステンションの話になると、別名を付けた型に対してエクステンションを追加した場合、それが元の型にも影響を及ぼすかどうかという問題が出てきます。
例えば、int32
型とそれにエイリアスを付けた StatusCode
があって、それぞれに独立したエクステンションを追加したとします。この場合、どのタイミングでエクステンションが適用されるか、という問題が出てきます。モジュールがインポートされるタイミングによってエクステンションの適用が異なる可能性があり、そのために複雑な問題が発生しそうです。
そのため、型エイリアスが元の型と同じように扱われるほうがシンプルで、人間にとっても理解しやすいルールになるでしょう。型エイリアスの特徴をきちんと理解し、それが適切に使われるようにすることが重要です。 型エイリアスという機能は、様々な場面で非常に便利に使うことができます。単純に別名をつけるだけでなく、それ以上に役立つ便利な機能が存在しますので、それについて詳しく見ていきましょう。
まず、前回の話の最後の方でも言及したように、複雑な型表現をシンプルに使いたいときに型エイリアスは非常に便利です。例えば、以下のようなコードがあるとします。
typealias HTTPResponse = (code: Int, message: String)
このように定義しておくと、例えば関数でリクエストを投げるときに HTTPResponse
型を返す形にできます。この形にすると、コードが非常にシンプルになります。
また、以下のような関数があったとします。
func handleResponse(response: HTTPResponse) {
// 処理内容
}
このように記述することで、見やすくて理解しやすいコードにすることができます。もし型エイリアスを使わなかった場合、次のように書かなければなりません。
func handleResponse(response: (code: Int, message: String)) {
// 処理内容
}
コードが冗長になり、どのような型かをいちいち思い出さないと書けないという不便さがあります。このような場合、型エイリアスを使うことで手間を省け、コードの可読性が向上します。
さらに、関数型についても型エイリアスは便利です。以下のように型エイリアスを定義することができます。
typealias ResponseHandler = (HTTPResponse) -> Void
これを使って関数の引数を定義すると、以下のようにシンプルになります。
func sendRequest(completion: ResponseHandler) {
// 処理内容
}
型エイリアスを使わないと毎回新しいコードを思い出しながら書かなければならないため、非常に手間がかかります。その点、型エイリアスを使うことで、効率的にコードを書き進めることができます。
以上のように、型エイリアスはコードをシンプルかつ見やすくし、維持管理もしやすくするための強力なツールです。ぜひ積極的に活用してみてください。 単純に自分も最近通信回りのことを書いたので、なるほど、疎遠の問題かという感じですね。昔、ディストピアとか結構使ってましたね。そうそう、APIキッカーとかHTTP通信をするときには持ち出せますよね。あれはすごいですね。
とりあえず、こういう感じで型エイリアスというのが便利に使えるよっていうところがありますね、確かに。だから、ライブラリを提供する側は、そこに意識的に型エイリアスを用意してあげて、これをちゃんと名前と互換にしようとすることが重要です。たとえば、トラック
とリクエスト
型というのを作って、ここで型エイリアスとしてレスポンスハンドラー
みたいに仕上げると、このレスポンスハンドラーがどんな世界観におけるハンドラーなのかが明確になります。さらに、複雑なプロトコル型が名前として出て便利になってくるということですね。
こういったライブラリを作るときに、積極的に型エイリアスを使っていき、その中身も工夫します。型エイリアスと言えば、ジェネリクスの機能として最初は使えなかったけれど、便利に使えるようになったというのがありますね。実際に使っている人は普通に使っていますが、たとえばJSON型みたいなのを作るときに、大体Dictionary<String, Any>
で作る場合が多いです。このとき、あぁ、JSONだから今String
とAny
で作っているんだねという感じですね。
もうちょっと細かいところを作るときにも、型エイリアスが有用です。そして、ジェネリクスの機能と連携して、例えばテーブル = テーブル<Int>
のようにしてイニシャライザを使ったりします。こういうふうに、一部の型だけを捕捉して残りをジェネリクスのままに残すという型エイリアスができるようになっています。これで、たとえばP
がA
のときにInt
型ならa
、W
型ならエラーになるようにする、つまりInt
型にしなさいというような動きもできます。
こういったのも、ちょっとした型エイリアスを提供したいときに便利ですね。安全性も考慮し、良い感じのライブラリを提供したいようなときに役立ちます。ただ、便利なことではありますが、あんまり使わないなという印象もあります。とはいえ、ライブラリを作るときには型エイリアスを積極的に取り入れてみると良い結果が得られるかもしれません。
では、時間になりましたのでこれで勉強会を終わりにします。お疲れさまでした。ありがとうございました。