https://youtu.be/fo8jSg7iyWk
前回から A Swift Tour
の 列挙型
について眺めていっていて。
今回もその続きになりますけれど、時間の都合でこれまでずっと飛ばし気味だった練習問題も面白そうな感じがするので、前回の話題向けに用意されていた練習問題 列挙型で2つの値を Raw 値で比較する関数を書いてみましょう
というところから見ていってみようと思います。
それが終わったら時間の限りで、これまでに飛ばしてきた練習問題を遡って見ていく感じで、今回は進めてみようと思います。どうぞよろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #55
00:00 開始 01:16 練習問題 02:23 列挙子の一致判定 04:55 等価比較の実装 07:20 より Swift らしい等価比較の実装 09:34 最も Swift らしい等価比較の実装 10:32 Equatable がない場合の等価比較 13:36 Objective-C らしい等価比較 16:36 最も Objective-C らしい等価比較 19:44 質疑応答 23:18 演算子を定義する場所による違い 27:18 プロトコル適合の合成との兼ね合いを検証 29:30 クラス継承を踏まえた等価比較の検証 32:53 メンバー演算子 41:08 プロトコル型との兼ね合いを検証 43:27 演算子の実装を型の内側と外側とで実装してみる検証 48:58 NSObject の isEqual(to:) メソッド 51:26 クロージャーの同値比較 55:01 クロージング ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #55
はい、じゃあ今日は列挙型のお話の続きここから始めていこうかなと思います。続きといっても、今まで一通り話し終えた後、この次が練習問題だったときにはさりげなく飛ばしてその次へ行ってたんですけど、この練習問題もなかなか面白いなと個人的に思っているのと、あとこの練習問題が登場するのは「A Swift Tour」のこのセクションここだけなんですよね。これより先行くと練習問題みたいなものが出てこない感じになってくるので、飛ばしてばかりいてもなんかもったいないなと思うところがあったので、ちょうどいいのでこの練習問題を今日はやっていくところから始めていこうかなと思います。
で、列挙型はローバリューを設定するとローチを持てるよっていうお話を前回しましたけど、これを使って2つの列挙型の値を比較する、要は一致判定ですよね。比較って難しいですね。等価比較なのか、大小関係の比較なのか、まぁいいや、ちょっと忘れちゃった。とりあえずそういった一致判定とかもうちょっと広くもせっかくだからやってみればいいんですね。練習問題だからね。とりあえずこの一致比較からやってみましょうというところなんですけど、そもそもの話として列挙型はローバリューがなくても一致判定ができる。原則付属値がついてくるとちょっとできなくなるんですけど、だから例えばこうやって列挙型があったとき、ちょっとばかりケースが多すぎると分かりにくいですね。ジャック、クイーン、キングぐらいにしましょうか。
一致比較がまず標準でできるんですよ。例えば let A = Rank.jack
と let B = Rank.jack
として、一致する方からやってみましょうかね。一致する方わざわざ変数に同じ値設定しなくてもいいや、こうやって2つの変数与えたときに、これで if A == B
が一致しますかみたいなことが列挙型は普通できると。こういうふうな済みになってますけど、一致しない値を設定するなら一致しませんと、こういうふうな動きになっていて、これでローチを持つ場合、例えば case jack = 11
みたいな感じで、case queen = 12, case king = 13
とまあ、こんな感じでやっても一致比較ができますよね。
なので、さっき言ってたローチで比較する関数を書いてみましょうというのは、大小比較のことなのかな。とりあえず等価比較のお話から実装していこうかなと思うんですけど、あまり意味ないですけどね。ここで等価比較をするって言ったときにいろいろと方法があるんじゃないかなと思うんですけど、まず最初に Swift で一般的な等価比較を実装する方法、そこからやってみますかね。慣れてないとわかりにくい書き方になるかなと思うんですけど、Swift では演算子を使って等価比較をするっていうのが一般的ですよね。なので、そういう方法をやるとすると、そうだな、従来の一般的な言語での考え方とすると、外側にフリーな関数を定義して、ここで比較判定を実装していくっていうのが一般的だと思うんですよ。そういうのでちょっと変わってくるんですけどね。
これで func == (lhs: Rank, rhs: Rank) -> Bool
ですね。今回練習問題はローチを使って比較しようっていう話なので、こうやってローチを使って判定してみますけど、こういう書き方が一つのアイデアっていうんですか、こんな感じで定義してあげるとこれでどっちの関数が呼ばれるんだろう。これ試してみます。print("通ったよ")
っていうことを書いてあげたときに、これ通るのかな。通ったね。フリーな関数がとりあえず優先されるみたいですね。これで一個等価比較をする関数を書いてみた。多分正解、出題者に意図があるとすると三角で、ついでにもうちょっと Swift っぽく書こうとすると、この四角演算子、特にレフトハンドサイドがこの型のとき、Rank 型ですね、今、このようなときにはレフトハンドサイド側こちらに対して static func ==
として演算子を定義する、これが Swift では一般的かなって。ちょっと動かしてみると、通りますよね、ちゃんとパスが表示されてる。
ここまででも出題者に意図があるなら三角ですよね。で、三角で思い出しましたけど、最近というか、Twitter でときどき「先生にこの回答をバツにされた」とか「例えば掛け算の順番でリンゴが5個あります」みたいなときにリンゴの個数、この価値観わかんないからどんな回答が正しいか正確に出せないですけど、例えば100円のリンゴが5個ありますみたいなときに 100 * 5
なら丸、5 * 100
はバツ、みたいな話でおかしいでしょみたいな話がときどきちょっと前か湧いてたりしましたけど、せいぜい三角ですよね、きっとね。どうでもいい話なんですけど、ちょっとバツはやりすぎかな。意図があるにしても。算数だ、算数、数学じゃなくて小学校の算数、小学校じゃないのかな。まぁいいや、全然余談ね。
とりあえず Swift 的にはこの書き方までは三角ですね。Swift だと Equatable
に準じて ==
と !=
が動いてくれる。でも Equatable
なくてもなんとか動く気がするな。ちょっとやってみましょうか。どうなる? やっぱバツ呼ばれちゃうね。何かしらの方法でここまで来ちゃって、そうですね。 ```
それか、ちょまどいなわぬ場合、なんか最初にデフォルトがあるようでなければ、自動的に Equatable
が適用されるところですかね。そうですね、それが影響してここまでたどり着いているのかもしれないですね。
では、これを Equatable
にできなくするために、やっぱり付属値ですよね。これをカットして押して、今ジャックとクイーンを使っているから、まあいいや。他の何でもいいや。これで例えば、Int
を持たせるとかすると変わってくるんですかね。ちょっとやってみましょう。
全然違うこと言ったらごめんね。思い切り変えないといけなくなってきますね。ここで通過比較をしないといけない。今回は通過するか試したいだけなので true
を返すことにして、これでうまくいきますかね。
!=
はダメになった。==
は通るよね。通らないかな、通るよね。よかったよかった。うまくいきましたね。こうやってとりあえず ==
だけ動く。ここで Equatable
を適用してあげると、まず ==
はちゃんとパスする。さっきエラーだった !=
が動きますよね。これでOKですね。
なので、ちょっとコードを戻しますけど、let
キャッシーだけで付属値がないパターンね。これで戻しましたけど、つまりこうやって Equatable
に準拠させた上で static func ==
を搭載してあげるというのが一層的に正解っていう感じです。
とりあえず通過比較はOKですね。で、これがObjective-Cスタイルだったときには何が正解かっていうところを、今さらいらない知識な気もしますけど、ちょっと紹介しておけますかね。Objective-Cスタイルの場合は enum
でなくてもいいんですけど、逆に enum
でやる必要が全くない気がします。でも、Objective-Cの頃には isEqualToRank
というメソッドを搭載してあげて、この中でリターンとして自分自身の rawValue
と受け取った rank
の rawValue
が一致するかみたいな、こういう書き方をするのがObjective-C流になります。
で、それによってどういうことをしてるかっていうと、オーバーライドだったかな。Objective-Cでは今は違いますけどね。こういうふうにすることによって、NSObject
がこの isEqualTo
を持ってるんですけど、それをここから継承した方が、自分に特化した比較演算をここで実装してあげることにより、Objective-Cのオブジェクト指向の継承の中で正確に比較表現ができるという、そういうふうな積みになってますね。
だから、Objective-C流って言ったけれど、オブジェクト指向流って言うんですかね。Swift
だともう少し違ってきますけど、昔風の書き方だとこういうふうな感じになって、それで比較演算は外に持っておいて、それで何かしらのベースクラス NSObject
こんな感じの比較演算を外に持たせておいて、return leftHandSide isEqualTo rightHandSide
みたいなふうに書くのが昔流ですね。
でも、これでもオブジェクト指向としては正解。Objective-Cとしては限りなく丸に近い三角って感じになっていて、isEqualTo
だと汎用的すぎて比較パフォーマンスが落ちたり、正確に比較できないこともあるんだったかな。忘れてしまいましたけど、そういった諸々の事情があって、func isEqualToRank
っていう。こっちがランクで、こっちが NSObject
。間違えた。そう、ね、不具合・不都合がありますのを思い出した。こうやって書いてあげて、ランク同士での比較をするよみたいなふうに配慮してあげるのがObjective-Cでは基本的な流儀になっていましたね。
こうやってランク同士の比較なら、rightHandSide
が rawValue
を持ってるってのがわかるでしょ。そして、このコンテンツだとね、NSObject
を使ったタイプだと if let
、あ、guard let rank = rank as? Rank else
と return
、まあ、false
かな普通はね。特殊な場合もあるかと思うんですけど。
で、ここでやっと始めて自分自身の rawValue
とランクの rawValue
が比較可能になる。要は判定してね、その後一致判定をするっていうことを、オブジェクト指向の汎用型の方を使ってる都合ね、やらないといけない。これもギリギリ丸の三角ですかね。普通はここで isEqualToRank
に投げてあげて、コードの修正を13行目に集約して、変な動きが起こりにくくするみたいな感じにしていくことになるんでしょう。これでObjective-Cの比較演算の実装としては正解になるんですかね。
コメントいただいたObjective-Cの仕様だと動的ディスパッチ、ここまでOKですね。演算子なら静的ディスパッチになるのか。==
。多分 NSObject
に対する比較演算が実装されてると思うんですよね。多分ね。ここで例えば、わかりにくいから struct
にして NSObject
に準拠させて、仮にそういう人も考慮して Equatable
にしたとして、これで NSObject
の import
、Foundation
ぐらいしておきます。
これでバー
ローバリオ を持たせちゃえば、ちゃんと動くのかな。ストラクトだからイコール 0 いらないや。これでこういうふうに実装してあげて、`s オブジェクト` 0 いれて、こうしてあげたときに、
Equatable``` 既に準拠されてるから。NS オブジェクト
のほうで多分 NS オブジェクト
のほうでこれが搭載されてるでしょ。そうしたときに、なんかストラクトになってた static
. ね。全然違ったという実装になってると思われるから作演算をしたときにはイコールイコールまでは静的ディスパッチで、ここの is Equal To
を呼ぶ段階で動的ディスパッチみたいな感じになるんですかね。なんかイコールみたいな感じになるですね。
でもし処理の効率化を図りたいときには static func
、クラスファンクになってるかが大事なんですけど。仮にオーバーライド func イコールイコール
が許されているんだとしたら、オーバーライドで is Equal To
ランクのほうを呼んであげて、より最適化をしつつ、ここが動的ディスパッチみたいな感じになるんでしょうかね。ちょっと見落としがあるか分かんないですけど、なんとなくこんなイメージを持っています。
次の質問もいってみましょう。3つもあるんだ。イコールイコールをグローバルスコープで実装するのと static func
の違いですね。そうですね。これスイスリントとかだと static func
ネームスペースなんか分かりやすくなのか分かんないけど static func
で実装優先するっていうルールがあったりするんですけど、グローバルスコープで実装されているものもあって。これって何か違いあるのかなっていうところが疑問です。
まずそこ見ていきましょうか。多分違いがある感じあるんですか。Equatable
が関係してくる気がする。例えばテスト型。Equatable
準拠とした場合が一番気になります。そうですね、そこがきっと影響してきそうな気がして、テストが A
っていうプロパティを持ってたとして、それで外側に定義したものは static func
扱いですから。ここでテストハンドサイドとしてテスト、ライトハンドサイドとしてテストで、ブールを返すでリターンしてテストハンドサイドのA
とライトハンドサイドのA
が一致するみたいなことを書いたときに、X1
がテストのA
の1で、X2
がテストのA
の2って書いたときに、X1
とX2
は一致しますか。一致しません。X1
とX2
は一致しませんかってエラーじゃない。そうですよね。ここが大きな違いかと思われます。これで例えば Equatable
をここで定義したときどう動くんだ。ここでもエラーになってください。これはオッケーなんです。オッケーなんですよ。
進化してることになるんで、あんま変わんないなっていうか。なるほどね。こうしたときには定義 not equal。パフォーマンスどうなんだろう。多分 Equatable
一緒か。そんな変わんなそうですね。ここまでちょっと把握してなかったから多分違うってお話を今進めてたんですけど、これが動くとなると確かにちょっとばかりややこしい話になってきそうですね。同じなんですかね。静的には解決してそう。ここで static func
でこの Equatable
が準拠できたのは暗黙のシンセサイザーですよね。日本語よく忘れちゃった。合成だけ、それ入ってるんですかね。ここで。
そっちが呼ばれてるのかな。ここプリントするとどうなるのか。X出ますか。出るよ。じゃあ自前実装ってことですよね。そうね。ちゃんとX2個出てること考えるとnot equal
も効いてるから。やっぱり違いがわからなくなりますね。こっちでXが出なかったとしたら、合成されたほうが呼ばれてそうみたいな。合成されたほうだよね。合成されたほうが6行目のやつを呼んでいる。あれ否定とかでしたっけ。否定とかですよね。呼んでますよね。外にあるのに呼んでる。否定を書いたらどうだね。5行目を否定の実装にして、イコールのときに呼ばれる。呼ばれないかもしれない。俺は呼ばれなそうだけど。
1個呼ばれる。1個。普通に14で呼ばれるだけだから。めぐりめぐってここへ来てるのか。わかんないですね。そうね。じっくり見ていけばわかりそうな気もしますけどね。内部的には変わってそうですけど、わかんないね。最終的に最適化とかが測られて同じになる可能性がありますよね。プラスだとどうなんでしょうね。
プラスでしょ。それで、イニシャライザを用意。年度だからイコールゼロにして、イコールゼロ、イミットにして、セルフAイコールAにして、ここまではオッケーでしょ。オッケーですよね。これ継承するとどうなんでしょうね。クラス、サブ、テスト、テスト、イコータブルを載せたときに、オーバーライドできるのか。オーバーライド、スタティックファンクイコール。テストハンドサイドがテスト、ライトハンドサイドがテスト、テーブル。既にラーが出てんのか。そうでしょうね。コロン間違えましたね。そうしたときに、セルフを取らないといけない。セルフでいいんだ。セルフ求めてるからこれでいいんだ。なんかちょっと自信がないな。セフトハンドサイドの、ここまでとりあえず書いてみよう。
こうしたときに、これはダメか。サブテスト、このフィクスは間違ってる気がするな。オーバーライドできない方なんですかね、実装が。求めてるのはセルフだから、やっぱりここが本来テストになっていて、あとはファイナルかそうじゃないかになってくると思うんだけどな。エラーいっぱい出てましたね。さっきと一緒か。ここでプラスファンク書いてあげると。でリターン、セフトハンドサイドのAとライトハンドサイドのA。こうしたときに、タイレットですね。 まだエラーが出ています。さっきのプラスファンクションだからファイナルになるんですよね。これ、何でしたっけ?回避する方法もあるんです。ファイナルにしないで何だっけ…覚者ルールでしたっけ、ファイナルにしないといけないとか。自分の中ではクラスファンクションでもうまくいっていたような気がしていましたが、仕様的には static func
と final class func
は同じ扱いになりますね。オペレーターだからそうなっているのかもしれませんね。
プロトコルで RT
を定義して、これで static func x
というメソッドを持たせたとして、クラス C
に static func
として func x
を定義します。これで動くかどうか試してみましょう。ちゃんと動作するか不安なのでインスタンス化してみて、これだとまたエラーが出ました。思考が先走っていますね、今日は。
これで動くか確認してみます。OKですね。static func
と ==
を使って、デフォルトで Void
にセットしてみましょう。興味深いのは、Void
同士で比較できるかという点です。Void
は Comparable
だから、できるはずですね。
ただ、static func
を使うと self
を使わないといけなかった気がします。イコータブルじゃないので、今 static
を使っていて、自分自身を指定しないといけないんじゃないかと思っていますが、多分大丈夫な気がします。static
自体にはその制約はなくて、純粋にクラスのオペレーター(関数)として存在しているだけだから問題ないでしょう。
質問に対する回答ですが、==
の場合はエラーが出るのはオペレーターの制約のためだと思います。プロトコルの static
を使うときは引数として最低でも self
を使わないといけないという制約があるはずです。これに関して、自分の記憶では引数が最低でも一つは self
を使わないといけない気がしますね。それがないと不自然ですよね。
これは ==
オペレーターの特殊な性質のためですかね。==
オペレーターを使用する場合、特にインフィクスオペレーターの場合、それに対応するメソッドをプロトコルに追加するときにセルフ制約が付くようです。例えば、==
オペレーターを定義する際には static func ==
として、それをクラスに組み込む必要があります。
以前、私も同じようなエラーに遭遇したことがありました。final class func
として定義している場合、static func
と同じように振る舞うものの、継承先での使用に関しては制約が出てくることがあるため、それを考慮すると質問の意図がさらに複雑になります。
クラスの継承を行う場合、継承先で呼ばれない可能性が出てくると思うんですけど、どうでしょうか。あるいは、プロトコル型を使用したときの挙動です。static func
にして、任意のプロトコル P
を定義する場合、そのクラス Test
が P
に準拠しているとして、どうなるか。
まず、イコータブルに準拠しない場合、Test
クラスの ==
を使うとエラーになります。次に、プロトコル P
がイコータブルを要求する場合、それに準拠させた上で、Test
クラスでのコードが正常に動作します。しかし、P
がイコータブルを求めていない場合はエラーが出ますね。
結局のところ、両方定義してみたところ、使用するとアンビギュアス(曖昧)というエラーが出ました。つまり、static func
と final class func
は実質的にはほとんど同じということです。
static func
をグローバルに呼び出せないんですかという質問に対して、曖昧というエラーが出る場合、それぞれのメソッドが存在しており、優先順位の問題でエラーが出ています。普通のプロトコル定義とクラス宣言のメソッドでは優先順位がありますが、今回はそれが曖昧で区別が付かないためにエラーが発生していると思われます。 出ないですよね。それが出るっていうことは、スタティックだとしないわけですね。試し利用もないですもんね。同じ扱いなのが両方です。実装までいけたんですけど、呼び出しだけできないのがね。もしかすると、このあたりが配慮が行き届いていなくて、両方同等なはずなのにバグとして定義ができてしまうっていう可能性がまず考えられますね。
この書き方、プロトコルは存在していたけれど、比較演算子は外に出すっていう書き方だったんですよね。それが Swift 2 になって、こっちにスタティックファンクとして載せるよっていう仕様に上がった。多分これが…。でも Equatable
ありましたよね。でも忘れちゃったな。今言ったの、もしかすると自分が把握してなかっただけかもしれない。この書き方があるのに外に書いてただけかもしれないですけど、その可能性ありますね。
いや、でも Swift 1 のときのプロトコル、確かセルフはなかったはず。そうか、当時プロトコルはなかったから、そもそもセルフっていうキーワード自体がなかったはずですね。セルフでキーワード使えないと、スタティックファンクで定義できないはず。やっぱり記憶、多分合ってる。Equatable
って書いた上で外に定義したんですよ、確かですね。多分そうだったはず。それが所属関係の都合で、自分に所属するものは自分に入れましょうっていう Swift API 外にありますけど、あれに則った形に定義したために、もしかすると後からこうやって定義されたがために特殊なおかしな違いが見えなくなってきてる。厳密に言うと同じみたいなふうなものかもしれない。
そもそも A == B
ってやってこのスタティックファンクが呼ばれるっていうこと自体がなんとなく直感的じゃない。グローバルにある気がする。そうですね、ちょっと特殊な仕様ですね。そうかもしれない。ゆくゆく巡って違いがないって言えたりするのかもしれないですね。確実なことは言えないですけど、違いなさそうねっていう気がしてきた。
そうですね、他の質問。2つ目はアプリさんがおっしゃったとおり ===
の企画があるんですけど、悩むときというか NSObject
を継承しているときの NSObject
の isEqual
ってあれってどういう仕様でしたっけ。同一インスタンス…一致しないというか、同値で一致しない気がしてて。それは実装次第。オブジェクト指向なので NSObject
が isEqual to
を持っていて、この中では多分純粋なインスタンス比較だと思うんですよ。そういうインスタンス比較しかしてなくて、実装するときに…。
この17は結構特殊な話で、普通はこの Equatable
っていうプロトコルの目的としては同値とか等価。等価ってインスタンス上げてもいいんですっけ。言葉の問題。同値とか等価。同値と等価って日本語として同じですかね。同値比較、等価。同じ価値、同じ値、だから…。その場合 Equatable
の目的としてはインスタンスなくて値が一致してればいい。なので、プロパティ、サードプロパティとかが全部一致してれば、同値、等価と言える。
言えますね。この17の実装ですっごい、ちょっとなんだっけかな、みたいなと思うんですけど。そうですね、値が等価であるかっていうのが ==
の目的なので、インスタンスがどうとかそういう話ではない。
最後の質問があと1分で…。クロージャーとかポインター、関数ポインターの場合どうすべきかっていう。クロージャー、サードプロパティで例えばストラクトとかがサードプロパティでクロージャーを持ってる場合の Equatable
の準拠はどう書けるのかと、どうすべきかっていうのが迷って。
で、クロージャー。僕の認識というか、知識が…。クロージャーの比較が難しいので、ラップした値型とかにして全部 true
で返すとか、比較しないっていう方法を取ることがあるんですけど。どうすべきなのかなっていう。無視しても同値であるって判断できるんなら、それでいいと思います。僕は完全にデータが何を表現しているか次第で、そもそも等価比較できないよねってなるのか…。
例えば今書いてるものみたいに、比較演算のときに初めて実行して一致比較できないっていうとか。こういう感覚、そうか、そうだったって。確かに、もちろん実行が走ってしまうので、コンプリーションハンドラーみたいなのでこれをやっちゃうと大問題なんですけど、それはもうそもそも一致比較できませんよっていう状況。
この関数型の比較ってできるんでしたっけ。関数型の比較はできないですね。じゃあ無視するしかないですね。自前で実装して。そうですね。でも無視していいかどうかが問題です。そうですね、等価比較でね。そうなんですよね。そこは考え直すべきみたいな問題な気がする。同時は参照型だけど、アドレス比較はできるのかな。やっぱできないね。できないんだ。ポイント当て感じないですね。完全に一致比較とか、そういう世界の話ではないので。
それを持っちゃった時点で素直な一致比較、論理的な一致比較は汎用的なものはありえないので、一般的にはできない。ただし、さっきのレイジーデータ型みたいに、これは遅延実行して比較するものなんだ、みたいな設計のときにはできますよね。こういう感覚になりそうな気がします。そうですね、設計次第で何が正しいかが決まるから。
ありがとうございます。はい、こんな感じです。いただいたコメントを何とか回収して、オーバーしてるんで何ともですけど、今日はこれで勉強会終わりにしようと思います。ありがとうございました。お疲れ様でした。