LoginSignup
2
0

More than 5 years have passed since last update.

Equatableプロトコルと継承について

Last updated at Posted at 2018-09-06

この記事の環境

  • XCode 9.4.1
  • Swift 4.1

この記事の対象者

  • Equatableプロトコルを実装したクラスを作って、さらにサブクラス(継承)を作ると挙動がおかしいぞ、と思って困っている人

問題のある実装

Main0はnameプロパティを持っていてEquatableプロトコルをnameプロパティを使って実装しています。
これは特に問題がないです。


open class Main0: Equatable
{
    public private(set) var name: String

    public init(_ name: String) {
        self.name = name
    }

    // Equatable
    static public func ==(lhs: Main0, rhs: Main0) -> Bool {
        return lhs.name == rhs.name
    }
}

問題は次のMain0を継承した下のSub0クラスです。
Sub0クラスはdetailプロパティも持っているので2つを比較して一致すれば同一のEqualだと判断します。しかしながらこちらのEquatableプロトコル実装(つまり==)は呼び出されません。


// ダメな継承
open class Sub0: Main0
{
    public private(set) var detail: String

    public init(_ name: String, detail: String) {
        self.detail = detail
        super.init(name)
    }

    // Equatable
    static public func ==(lhs: Sub0, rhs: Sub0) -> Bool {
        // 呼ばれない
        return (lhs.name == rhs.name) && (lhs.detail == rhs.detail)
    }
}

これはプロトコルの実装を継承した場合の制約になります。
ではどのようにすればSub0で==が正しく使えるようになるでしょうか。

期待通りの動作をする実装

下記のMain1クラスのように、func ==()に直接比較処理を記述するのではなく、equal()関数を実装してそれを呼び出します。


open class Main1: Equatable
{
    public private(set) var name: String

    public init(_ name: String) {
        self.name = name
    }

    internal func equal(to another: Main1) -> Bool {
        return self.name == another.name
    }

    // Equatable
    static public func ==(lhs: Main1, rhs: Main1) -> Bool {
        return lhs.equal(to: rhs)
    }
}

Main1クラスを継承したSub1クラスではfunc ==()は必要ありません(呼び出されないので)
代わりにMain1クラスのequal()関数をoverrideします。overrideするために引数がMain1クラスである必要があるのでこのままではdetailプロパティの比較ができません。
そこで下記の例では一度Main1で受けた後にSub1クラスへのCastを試みています。


open class Sub1: Main1
{
    public private(set) var detail: String

    public init(_ name: String, detail: String) {
        self.detail = detail
        super.init(name)
    }

    public override func equal(to another: Main1) -> Bool {
        if let b = another as? Sub1 {
            return super.equal(to: another) && (self.detail == b.detail)
        }
        return super.equal(to: another) // ここ微妙
    }
}

Castが成功すればdetailプロパティにアクセスできますので、superクラスのequal()関数とANDを取ります。
考えどこは渡されたto another: Main1がSub1でなかった場合です。
falseを返すか、super.equal(to: another)を呼び出すか、これはどのような比較をしたいのかによるのではないでしょうか。

さらにEquatableをProtocol Extensionで実装し、サブクラスを作成した場合の挙動は下記のようになります。下記のNameGettableは、nameプロパティを持っており、かつ==で比較できることがprotocol extensionによって実装されます。

Swift
// ユニークな識別子を返す
public protocol NameGettable: Equatable
{
    var name: String { get }
}

extension NameGettable
{
    // Equatable
    static public func ==(lhs: Self, rhs: Self) -> Bool
    {
        return lhs.name == rhs.name
    }
}

このような場合に同じくfunc ==()を実装してみます。

Swift
open class Main2: NameGettable
{
    public private(set) var name: String

    public init(_ name: String)
    {
        self.name = name
    }

    internal func equal(to another: Main2) -> Bool {
        return self.name == another.name
    }

    // Equatable
    static public func ==(lhs: Main2, rhs: Main2) -> Bool
    {
        return lhs.equal(to: rhs)
    }
}

open class Sub2: Main2
{
    public private(set) var detail: String

    public init(_ name: String, detail: String)
    {
        self.detail = detail
        super.init(name)
    }

    public override func equal(to another: Main2) -> Bool {
        if let b = another as? Sub2
        {
            return super.equal(to: another) && (self.detail == b.detail)
        }
        return super.equal(to: another) // ここ微妙
    }
}

結論は、Main1、Sub1と同様になり、extension NameGettableは上書きされた形になります。
これを用いれば通常はextension NameGettableで挙動を定義しながら、継承などの場合を考慮したクラスは独自のfunc ==()を実装することができることになります。

下記にTestコードを記載します。

Swift

class EquatableTests: XCTestCase {

    // =============================================================================
    // MARK: - 1
    // =============================================================================

    func testMain0Equality() {

        let a = Main0("123")
        let b = Main0("123")

        XCTAssertEqual(a, b)
    }

    func testMain0NotEquality() {

        let a = Main0("123")
        let b = Main0("456")

        XCTAssertNotEqual(a, b)
    }

    func testSub0Equality() {

        let a = Sub0("123", detail: "456")
        let b = Sub0("123", detail: "456")

        XCTAssertEqual(a, b)
    }

    func testSub0NotEquality() {

        let a = Sub0("123", detail: "456")
        let b = Sub0("123", detail: "789")

        //XCTAssertNotEqual(a, b) // XCTAssertNotEqual failed: ("Sub0") is equal to ("Sub0") -
        XCTAssertEqual(a, b)
    }

    // =============================================================================
    // MARK: - 2
    // =============================================================================

    func testMain1Equality() {

        let a = Main1("123")
        let b = Main1("123")

        XCTAssertEqual(a, b)
    }

    func testMain1NotEquality() {

        let a = Main1("123")
        let b = Main1("456")

        XCTAssertNotEqual(a, b)
    }

    func testSub1Equality() {

        let a = Sub1("123", detail: "456")
        let b = Sub1("123", detail: "456")

        XCTAssertEqual(a, b)
    }

    func testSub1NotEquality() {

        let a = Sub1("123", detail: "456")
        let b = Sub1("123", detail: "789")

        XCTAssertNotEqual(a, b) // OK
    }

    func testMain1Sub1Equality() {

        let a = Main1("123")
        let b = Sub1("123", detail: "456")

        XCTAssertEqual(a, b)
    }

    func testMain1Sub1Equality2() {

        let a = Sub1("123", detail: "456")
        let b = Main1("123")

        XCTAssertEqual(a, b)
    }

    // =============================================================================
    // MARK: - 3
    // =============================================================================

    func testMain2Equality() {

        let a = Main2("123")
        let b = Main2("123")

        XCTAssertEqual(a, b)
    }

    func testMain2NotEquality() {

        let a = Main2("123")
        let b = Main2("456")

        XCTAssertNotEqual(a, b)
    }

    func testSub2Equality() {

        let a = Sub2("123", detail: "456")
        let b = Sub2("123", detail: "456")

        XCTAssertEqual(a, b)
    }

    func testSub2NotEquality() {

        let a = Sub2("123", detail: "456")
        let b = Sub2("123", detail: "789")

        XCTAssertNotEqual(a, b) // OK
    }

    func testMain2Sub2Equality() {

        let a = Main2("123")
        let b = Sub2("123", detail: "456")

        XCTAssertEqual(a, b)
    }

    func testMain2Sub2Equality2() {

        let a = Sub2("123", detail: "456")
        let b = Main2("123")

        XCTAssertEqual(a, b)
    }
}

2
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0