この記事の環境
- 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によって実装されます。
// ユニークな識別子を返す
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 ==()を実装してみます。
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コードを記載します。
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)
}
}