https://youtu.be/wJ1r6_d8CXg
今回は A Swift Tour の プロトコルと拡張 から 型拡張 についてを眺めていきます。
Swift に親しむ人にはお馴染みの機能かもしれないですけれど、改めてその機能の特徴についておさらいできたらいいなと思ってます。
逆に Swift に慣れてない人には特殊な機能に感じられがちなところでもあるので、型拡張についてざっくりと確認しておくのにも良い機会かもしれないです。どうぞよろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #64
00:00 開始 01:11 型拡張 01:38 カテゴリー拡張 02:46 型拡張でプロトコルを適用可能 03:48 既存の型に対して拡張可能 04:51 練習問題 04:58 絶対値 06:33 if 文で実装する例 07:05 単項マイナス演算子 08:00 switch 文で実装する例 09:10 条件演算子 10:03 二乗して平方根を取る例 11:00 浮動小数点数の誤差を気にしてみる 12:25 abs 関数をラップする例 14:51 パターンマッチングで実装する例 15:39 いろいろな書き方を見てみる利点 18:16 magnitude 20:43 Signed Numeric 22:10 abs 関数の実装 25:01 magnitude で実装する例 26:44 型拡張の特徴的なところ 26:49 型拡張では保存型プロパティーを追加できない 31:17 objc_setAssociatedObject() 34:42 同一ファイル内でも型拡張で保存型プロパティーは追加できない 36:01 型拡張におけるプロトコル準拠の合成 38:03 訂正:型拡張での保存型プロパティーの追加を試みた例 41:12 SwagGen 42:30 型拡張における構造体のイニシャライザー 44:07 型拡張における静的プロパティー 46:49 次回の展望 ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #64
はい、では始めていきますね。今日は型拡張についてお話しします。セクションとしてはプロトコルとエクステンションの続きです。前回と前々回の続きになりますが、今回はプロトコルだけに限らず型拡張についても見ていく回になると思われます。
この機能としてはシンプルなところでもあり、特徴的でもありますが、話自体はこのスライド1枚で終わっています。なので、とりあえずこのスライドをざっくり見てからエクステンションに見られる特有の動きについて眺めていけたらと思います。そんな感じで話を進めてみますね。
まずエクステンションを使うと、既存の型に対して新しいメソッドや計算型プロパティを追加可能ですよ、というのがエクステンションの基本的な機能になります。ただ、これがObjective-Cの頃の概念に近く感じます。Objective-Cのときは型拡張エクステンションとは言わなかったですよね。
そうですね、カテゴリー拡張のことを指している感じがします。Swiftではこれがエクステンション型拡張に相当します。今表示中のスライドの一行目に書かれている説明も、どちらかというとカテゴリー拡張の視点で見たときの説明に近いと感じます。
特徴的なところとして、プロトコルを既存の型に適用することも可能です。これはなかなか面白い機能ですね。Objective-Cのときはどうだったかすっかり忘れてしまいましたが、この勉強会でわざわざ思い出す必要はないかと思います。
型拡張のポイントとしては、既存の型に対して機能を追加できるというのが大きな特徴です。既存の型というのが、自分が作った型もそうですし、他の誰かが作った型に対しても拡張できるというのが特徴です。たとえばSwiftでは標準で用意されている型、つまりAppleチームが作ったInt
型についても機能を拡張できます。
この例では、プロトコルを新たに追加し、さらにその中で計算型プロパティやメソッドを追加しています。これにより、Int
型が新たにSimpleDescription
プロパティやAdjust
メソッドを備えることができるという話です。
では、先に練習問題をやっていきます。
ダブル型を拡張してAbsoluteValue
プロパティを追加してみましょう。AbsoluteValue
は絶対値のことで、名前から察するに、マイナスだったらその符号を取る感じです。実際に作ってみましょう。
extension Double {
var absoluteValue: Double {
if self >= 0 {
return self
} else {
return -self
}
}
}
このように、Double
型を拡張してプロパティを追加します。エクステンションの中で自分自身の値を取りたいときはself
を使います。例えば、このようにif self >= 0
という条件分岐で実装できます。
この方法で、変数名の頭にプレフィックスオペレーターでマイナスを書くことができます。初めて見たかもしれませんが、普通にできますよね。 そうそう、なんとなく変な感じがするでしょ。でも、できますよね。純粋に単純なプレフィックスオペレーターで、例えばバリューがあって、-30.5
とかにしたときにバリューに対してアブソリュートバリューで30.5
が返ってきますね。はい、できましたね。動きました。
これをもうちょっとシンプルに、というのも大げさですが、他のいろんな書き方を紹介します。例えばスイッチ文だと、こうです。
switch self {
case 0...:
return self
default:
return -self
}
これでもうまくいくかな。はい、ちゃんと動きました。
興味本位でケース0以上
にして、このときはケース漏れを防ぎますよね。やはりデフォルトを使うことになるでしょう。他の書き方としては、参考演算子(コンディショナルオペレーター)を使う方法があります。例えば:
return self >= 0 ? self : -self
これも1行で書けるのでリターンを省略しても良い場合があります。ただ、若干見にくいかもしれません。
他には、二乗してルートを取る方法もあります。これはちょっとかっこいいですが、例えば次のようになります。
return sqrt(self * self)
なかなか面白いメソッドです。スイフトの標準ライブラリにスクエアルート関数があるので、これを利用します。
ただ、丸め誤差が気になる場合もありますので、厳密な場面では注意が必要です。マイナス30.5
については大丈夫でした。ただ、同じ値だと誤差が出にくいですね。極端な値の場合、不動小数点は弱いので注意が必要です。
テストを書く観点も大事ですね。そういった発想も大事です。この方法もあり、数学に親しんでいる感じがして良いですね。
絶対値はC言語のabs
関数でもできます。Swiftでも次のように使います。
import Foundation
let value: Int = -42
let absValue = abs(value)
abs
関数は整数型には対応していますが、不動小数点には対応していません。fabs
関数を使う必要がありますが、通常はCライブラリを利用します。
プロトコル思考的、オブジェクト思考的な方法で絶対値を表現する例もあります。
extension Numeric {
var absoluteValue: Self {
return self >= 0 ? self : -self
}
}
これでNumeric
プロトコルを採用するすべての型に絶対値プロパティが追加されます。
他の方法として次のようにif文を使うこともあります。
if self >= 0 {
return self
} else {
return -self
}
各種の書き方を紹介しましたが、どれを使うかは状況や好みに応じます。
最後になりますが、大変面白い提案をいただきましたのでご紹介しました。コメントでいただいた通り、SignedNumeric
にも絶対値プロパティがあります。こちらもぜひ参考にしてください。 なるほどね。ありがとうございます。つまり、マグニチュードって何ですか?絶対値っぽい値が取れるんですよ。有効数字的なところだけを取り出す感じですかね。不動小数点数の場合だと分かりやすいです。マグニチュードでは、絶対値に相当する値が取れるということです。加数部を取りますね。例えば、10のマイナス何乗
とかにするとどうなるんだろう?これで0.0何とか
なるんでしたっけ?そうなるのか。それでは符号が取れるだけです。
これは整数にも適用できます。例えば、x = -1000
のときにマグニチュード
で普通に1000
が取れるはずです。そうですね、取れます。それができないケースがあるってことなんでしょうか。さっきのコードだと型を合わせていましたから、精度が違うことがあります。ダブル型のマグニチュードは確か整数型だったと思います。ちょっと確かめてみますね。マグニチュードのバリュー型、不動小数点数のほうですね。クイックヘルプで見ればよかったですね。ダブル型か、そうですね。0.0何とか
出ていましたから。
違う場面って思いつかないな。そうすると、サインドニュメリック。このプロパティの定義だけ見ると、多分型がアソシエーティブタイプになっているのかな。ニュメリックですね。マグニチュードを持ってるのは確かサインドです。でも出てこないな。ここで入力しているのかな。
マグニチュードを定義を辿ると、ピンポイントにコンパラブルとニュメリック。アソシエーティブタイプなので、親と型が違う場合も考えられます。コメントでアブソリュートバリューとちゃんと書いてありますね。なるほど、適用した元の型とその絶対値の型が違う可能性があるわけです。ただ、理由がまだ自分の中で納得できませんが、一応考慮されていて、ABSメソッドを実装するときには、ABS関数が取った値と同じ型を返すようになっています。そのため、型が一致しているかを確認し、もし一致していなかったら強制的に変換するという手法をとっています。
さっきのコード見ると、型の評価をした後、P型を取ってP型を返す。ただし、Pがコンパラブルかつサインドニュメリックのときのみです。コンパラブルが必要なのは比較演算子を使用するためですね。Pがサインドニュメリックである理由は、サインドではないと絶対値を求める必要がないからです。厳密にはマイナスのプレフィックス演算子が使えるようにということです。このようなインターフェースの都合上、型を合わせている形です。
Pのマグニチュードを返してもいい気もしますし、またはABSに突入する前にPのマグニチュードとPが一致しているかを判定してもいい気がしますが、強制的に型変換を行っています。アンセーフビットキャストで行うのは大丈夫でしょうか。まあ、そういう議論はまた別の機会にしましょう。
面白いですね。仕様や使い方がちょっと読めませんが、賢い人が書いているので何か意味があるでしょう。このようなコードを読み解くのも勉強になりますが、今回はやめておきます。とりあえず、コメントを見る限りマグニチュードで十分にアブソリュートバリューとして使用できるみたいです。
だから、新しい書き方として、リターンを書かなくてもいいですし、セルフも不要ですね。マグニチュードで実行すると、例えばマイナスが取れたときの挙動は同じです。マグニチュードがアブソリュートバリューなのか、それだけが重要なポイントです。コメントにも書いてありますね。大丈夫そうです。
コメントでいろいろと書いてあるので、そこを読んでいく必要がありますが、基本的にはこのような形で実装できると理解しました。では、スライドの方に戻ります。他に型拡張でまだ見ていないところや、スライドに戻して注目する点を見ていきましょう。
型拡張の特徴として、依存の型にも計算型プロパティを追加可能ですが、保存型プロパティは追加できないという点があります。具体的に見てみましょう。アブソリュートバリューは計算型プロパティですが、例えばバリューをイント型でも保存型プロパティとして持たせようとすると、保存型プロパティはエクステンションできないのでエラーになります。このような制約も覚えておきたいです。 これはなぜ起こるのかというと、メモリーレイアウトの都合が関係しているのではないかと思います。物理的な話に持っていくとそうなのかな、サイズですね。要は、ダブル型のサイズが何にも拡張していないときで、計算型プロパティがあってもいいですが、とにかくダブル型は64ビット、つまり8バイトです。このサイズが決まっています。
もし保存型プロパティをエクステンションに追加できたとすると、そのエクステンションした時点でダブル型に対してさらに追加でint型の保存領域を持たないといけません。int型が8バイトなので、合計16バイト必要になりますね。そうすると、このエクステンションができたとすると、このサイズが16バイトにならないといけないということです。
これが許されてしまうと、コンパイルタイムでダブル型のサイズを決められなくなります。そうすると構造体にとってはかなりダメージが大きいのです。それを許してしまうことによって、構造体をスタックに入れることが非常に難しくなる、というより無理になってしまいます。コンパイル時に入れられなくなり、話が難しくなるのです。
別のモジュールで保存型プロパティをエクステンションに追加した場合、そのモジュールをインポートするかどうかでオプションが使えるかどうかが変わってきます。インポートしていないところでは8バイト、インポートしているところでは16バイトということになります。インポートしているところからインポートしていないところにダブルを渡すと、メモリサイズが合わなくなり、いろいろと厄介な問題が起こります。
以上のような理由から、型拡張のときに保存型プロパティを追加できないというルールがあるのです。これは構造体もクラスも列挙型も同じです。これが型拡張の制約の中で一番出会いやすいものです。
面白いコメントをいただいたのですが、Objective-Cのアソシエイティブオブジェクトについてです。Objective-Cのランタイムでは、誰かが作った型のインスタンスに対して値の保存場所を追加できるという、いわゆる「黒魔術」が存在しています。Swiftが登場した当初、型拡張でプロパティを追加できないという状況を解消するために、Objective-Cのアソシエイティブオブジェクトを使ってインスタンスにプロパティを追加する発想が行われていました。
今ではインスタンスに対して後から追加で保存型プロパティを持てないということが常識として浸透しており、あまり見られなくなりました。しかし、自由度の高いところから低いところへ押し込められると、最初のうちは何とか突破口を見出したくなりますよね。Swiftに慣れている方なら、エクステンションで保存型プロパティが持てなくても基本困らないでしょう。
型拡張でプロパティ保存型を追加できないというルールについては、従うしかありません。同じファイル内のエクステンションならOK?という話もありますが、基本的にはエラーになります。例えば、同じファイル内であってもバー型の保存型プロパティを追加することはできません。
また、自動準拠について話すと、例えば同じファイル内であれば許されることがありますが、違うファイルではエラーが出ることがあります。例えば、プレイグラウンドでソースを分けたときに、別ファイルにある場合はエラーになります。
// ファイルA
protocol SomeProtocol { ... }
// ファイルB
extension SomeType: SomeProtocol { ... }
このように、同じモジュール内では可能ですが、別モジュールではエラーが発生します。
というわけで、型拡張の特性と制約について理解しておくと、Swiftのプログラミングがさらに楽になりますね。 やっぱりファイル単位ですね。だから、配慮しようと思えばできるのですが、思いついてないだけという可能性もありますし、分かりにくいという可能性もあります。さっきプレイグラウンドのコードで紹介したエクステンション、ここ間違っていましたね。エラーが出ていたので、確認としてもう一度プレイグラウンド本体のほうで試してみます。
ここですね、間違えてダブルにしてしまった部分です。こうしてもエラーになりますよね。動いていないのがわかりますか?エラーが出るんです。自動準拠については先ほど説明したように、ファイルをまたがなければエクステンションでも対応してくれますが、ファイルをまたげば対応しなくなるんです。だからこのファイル内でしか実行できないという制限があります。
リファクタリングするときに、自動的に準拠させる機能もあります。リファクターが強化されているので、それを使って対応しています。これが自動か自動でないかというのは非常に大きな違いになり得ます。間違った実装になる可能性もありますからね。
次に、エクステンションで差が出る部分として、構造体ではデザインイティブイニシャライザーを追加することができますが、クラスではできないという特徴があります。また、構造体でデザインイティブイニシャライザーを別ファイルで拡張することができても、別モジュールになるとそれができなくなるという制約もあります。そのため、自分自身の異なるイニシャライザーを呼ばなければならなくなるのです。これは型拡張の特徴の一つです。
後は、安全性やモジュールをまたいだ初期化の適切な実施を確保するための設計上の理由があるのかもしれません。物理的なメモリレイアウトの保護とは異なり、より発展的な理由だと思います。
また、型拡張でできることとできないことについて、スタティックバー(static var
)は持つことができます。これはコンパイル時にスタティック領域にバリューのための領域を作るもので、動いていることを確認できます。しかし、ジェネリクス(Generics
)のエクステンションでは、スタティックバーに限らず、いろいろな制約が発生します。
コメントで新しいツールについて教えていただきました。こういった外部ツールを使うことで、ファイルやモジュールを跨ぐエクステンションで自動準拠ができない問題を解決する手段もあります。これも一つの解決策として覚えておくと良いでしょう。
他にもエクステンションならではの特徴や、できること、できないことがあります。例えば、コンディショナルコンフォーマンスなどもありますね。次回、少しでも触れて紹介できればと思います。
では、時間になりましたので、今日の勉強会はこれで終わりにしようと思います。お疲れ様でした。ありがとうございました。
―――――― お疲れ様でした。ありがとうございました。