SSブログ

共用型の比較 [Swiftプログラミング]

勉強と慣れをかねてObjective-CをやめてSwiftでいろいろ書いてる。今日書いてて、Swiftのassociated valueつきのenumについて、ふとわからないことができた。誰か教えて....

Swiftの共用型(associated valueとかunion typeというのか、名前がよくわからない、タプルをくっつけられるやつ)は便利なのでかなりの頻度で使ってしまう。そのメンバ名が同じかどうかを比べたいとき、どうすればいいのかよくわからない。

たとえば複数の構造体をまとめるようなenumで
enum CertainUnion: Equatable, OtherProtocol {
    case aCase(StructA)
    case bCase(StructB)
    case cCase(StructC)
    
    static func == (lhs: Self, rhs: Self) -> Bool {
        //  ?
    }
みたいにしてEquatableにconformして、メンバ名が同じならtrueを返すようにしようとした。ところがメンバ名をどうやって取り出したらいいのかわからない。 悩んだあげくに
enum CertainUnion: Equatable, OtherProtocol {
    case aCase(StructA)
    case bCase(StructB)
    case cCase(StructC)
    
    func id() -> Int {
        switch self {
        case .aCase:
            return 0
        case .bCase:
            return 1
        case .cCase:
            return 2
    }
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.id() == rhs.id()
    }
あるいは、少なくとも区別する値を自動的にするように
    func type() -> String {
        switch self {
        case let .aCase(s):
            return String(describing: type(of:s))
        case let .bCase(s):
            return String(describing: type(of:s))
        case let .cCase(s):
            return String(describing: type(of:s))
        }
    }
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.type() == rhs.type()
    }
みたいなことをしてしまった。どっちにしてもswitchで似たような記述を繰り返さないといけなくて、いかにもボイラープレートでカッコ悪い(GObjectだとプリプロセサを駆使してそうな書き方だな)。

単純な列挙型の場合はEquatableを宣言するだけでいいらしいけど、共用型だと宣言だけでは
Type 'CertainUnion' does not conform to protocol 'Equatable'
と、コンパイラに怒られる。「等しい」というのはタプルの中身まで同じでないとtrueを返すべきではなくて、中身にまでコンパイラで責任は負えない、ということだろう。タプルの中身が全部Equatableにconformしてるとコンパイラが「==」を生成してくれるのかもしれないけど、やりたいのはメンバ名の比較で、完全一致が必要なわけではない。

共用型に対してこういうEquatableより「ひとランク低い比較」が欲しいとき、どうするんだろう。Swiftの共用型でそういうのはそもそも使い方として間違ってるんだろうか。

誰か教えて。
nice!(0)  コメント(4) 

nice! 0

コメント 4

ちくら

できないみたいですね、いずれにせよ列挙することになるみたいです
static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.aCase, .aCase): return true
case (.bCase, .bCase): return true
case (.cCase, .cCase): return true
default: return false
}
}
実際にはa,b,cがなんらかの選択肢であるならば
enum CertainEnum: Equatable {
case a
case b
case c
}
enum CertainUnion: Equatable, OtherProtocol {
case aCase(StructA)
case bCase(StructB)
case cCase(StructC)

var enumCase: CertainEnum {
switch self {
case .aCase: return .a
case .bCase: return .b
case .cCase: return .c
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.enumCase == rhs.enumCase
}
}
としてしまうのもよいという場合が多いように思います
by ちくら (2020-12-08 10:46) 

decafish

コメントありがとうございます。

やはりそうなんですか。
示していただいた実装の仕方は両方理解しました。


たとえば
enum CertainEnum {
case a, b, c, d, e, f, g
case h(Double)
}
というようなenumにコンパイラは文句言わないのですが、hを加えたばっかりにEquatableでなくなってしまうのは、理屈としては理解できるのですが「なんとかならんか」という気がしてしまいます。

例えば、付加情報を持ったエラーをenumで表したときに2つのエラーを比較する、なんてことは起こらないので、そもそも使い方が間違ってる、という気がしないでもないのですが。
by decafish (2020-12-08 18:31) 

ちくら

その場合はDoubleがEquatableなので、Doubleの値も比較する形で==の暗黙的な実装が入るみたいですね。先の例も中身まで比較するなら
struct StructA: Equatable {
let verA: Int
}
struct StructB: Equatable { }
struct StructC: Equatable { }
protocol OtherProtocol { }
enum CertainUnion: Equatable, OtherProtocol {
case aCase(StructA)
case bCase(StructB)
case cCase(StructC)
}
let a1 = CertainUnion.aCase(StructA(verA: 1))
let a2 = CertainUnion.aCase(StructA(verA: 1))
let a3 = CertainUnion.aCase(StructA(verA: 2))
let b1 = CertainUnion.bCase(StructB())
print(a1 == a2) //true
print(a1 == a3) //false
print(a1 == b1) //false
とできるようです
by ちくら (2020-12-08 23:12) 

decafish

ありがとうございます。
そこのところは理解できます。
Equatableは厳密な同一性が定義できる場合に定義するのが原則だと思うので、やっぱりメンバ名だけの同一性をEquatableで表現するのはまずいですね。

クラスでEquatableとは別に===演算子があるように、メンバ名だけの同一性に関するEquatableよりひとレベル低いプロトコルあってもいいような気がします。Similarbleプロトコル"a ~ b"みたいな(「~」って何かに使ってましたっけ?)。しかし、それは共用型enumでしか使わないことになってしまって、使い道はあまりないですか。うーん。
by decafish (2020-12-09 18:01) 

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。