Edited at

プロトコル実装における菱形継承問題

More than 5 years have passed since last update.

いわゆるダイアモンド継承問題は、Objective-Cでも発生します。

Wikipediaに嘘が書いてあったので、そのツッコミなど。


菱形継承問題

"多重継承ができない言語(Objective-C、PHP、C#、Java)ではインタフェースの多重継承が可能である(Objective-C ではプロトコルと呼ぶ)。インタフェースは基本的には抽象基底クラスであり、抽象メソッドからなる(データメンバを持たない)。従って特定のメソッドやメンバ変数には常に1つの実装しかないので、あいまいさは発生しない。"

菱形継承問題 - Wikipedia

菱形継承問題は、複数のクラスを継承した際に、親クラスが同一名のメソッド/フィールド等を持っていた場合、それを呼び出した時にどちらの親による定義として呼び出されるかが、あいまいである(言語仕様として未定義である、あるいは「この場合の動作は不定である」等と言語仕様に書かれている)といった問題です。

確かにObjective-Cのクラスは、親クラスを1つしか指定できないようになっていますし、Wikipediaでもプロトコルを用いることを理由に、Objective-Cには「常に1つの実装しかないので、あいまいさは発生しない」としています。

しかし実際には、Objective-Cには同じ名前でメソッドを定義するための仕組みが複数存在します。

今回はその中でもカテゴリを例にとって、Objective-Cにも菱形継承問題が存在するということを確認して行きたいと思います。


実際にやってみる

ProtocolA、ProtocolBというそれぞれ同一名のメソッドを持つプロトコルを、Testクラスで実装してみます。

プロトコルメソッドの実装はそれぞれProtocolACategory、ProtocolBCategoryというカテゴリで行います。


main.m

@protocol ProtocolA <NSObject> +(void)log; @end

@protocol ProtocolB <NSObject> +(void)log; @end
@interface Test : NSObject @end
@interface Test(ProtocolACategory)<ProtocolA> @end
@interface Test(ProtocolBCategory)<ProtocolB> @end
@implementation Test @end
@implementation Test(ProtocolACategory) +(void)log { NSLog(@"A"); } @end
@implementation Test(ProtocolBCategory) +(void)log { NSLog(@"B"); } @end

int main(int argc, char *argv[])
{
[Test log]; // NSLog(@"B");
}


2種類の実装が存在するにも関わらず、片方の実装のみが実行されました。


実装の順番をずらしてみる

前回はProtocolACategoryの後にProtocolBCategoryを実装していましたが、今度はそれを入れ替えてみます。


main.m

@protocol ProtocolA <NSObject> +(void)log; @end

@protocol ProtocolB <NSObject> +(void)log; @end
@interface Test : NSObject @end
@interface Test(ProtocolACategory)<ProtocolA> @end
@interface Test(ProtocolBCategory)<ProtocolB> @end
@implementation Test @end
//@implementation Test(ProtocolACategory) +(void)log { NSLog(@"A"); } @end
@implementation Test(ProtocolBCategory) +(void)log { NSLog(@"B"); } @end
@implementation Test(ProtocolACategory) +(void)log { NSLog(@"A"); } @end
int main(int argc, char *argv[])
{
[Test log]; // NSLog(@"A");
}

なんか最後に実装されているカテゴリが実行されているように見えます。

宣言の順序は関係ないっぽいです。


クラスのメタ情報からメソッドの一覧を取り出して実行してみる

この2つの実装がObjective-Cの中で実際にどのように実装されているか試してみます。


main.m

#import <objc/runtime.h>


@protocol ProtocolA <NSObject> +(void)log; @end
@protocol ProtocolB <NSObject> +(void)log; @end
@interface Test : NSObject @end
@interface Test(ProtocolA)<ProtocolA> @end
@interface Test(ProtocolB)<ProtocolB> @end
@implementation Test @end
@implementation Test(ProtocolACategory) +(void)log { NSLog(@"A"); } @end
@implementation Test(ProtocolBCategory) +(void)log { NSLog(@"B"); } @end

int main(int argc, char *argv[])
{
uint classCount;
Method* methods = class_copyMethodList(object_getClass([Test class]), &classCount);
{
// Testクラスに実装されている、クラスメソッドの数
NSLog(@"%d", classCount);
// 結果:2

// 1つ目のメソッドのメソッド名
NSLog(@"%s", (char*)method_getDescription(methods[0])->name);
// 結果:log

// 1つ目のObjective-Cのメソッドを、C言語の関数として直接実行
method_getImplementation(methods[0])([Test class], @selector(log));
// 結果:B

// 2つ目のメソッドのメソッド名
NSLog(@"%s", (char*)method_getDescription(methods[1])->name);
// 結果:log

// 2つ目のObjective-Cのメソッドを、C言語の関数として直接実行
method_getImplementation(methods[1])([Test class], @selector(log));
// 結果:A
}
free(methods);

// Objective-Cの文法でメソッドを呼び出してみる
[Test log];
// 結果:B
}


・・2つ実装されてんじゃん。


結論

Wikipediaの「メソッドやメンバ変数には常に1つの実装しかないので、あいまいさは発生しない」は

プロトコルのメソッド名は名前がかぶったらアウト


Objective-C Advent Calendar 2012

これはObjective-C Advent Calendar 2012の記事です。

そして投稿が遅れてすみません。

いつもやってるこの問題の解決策が万能ではないことに気付いて

「やっべーどうしようこれじゃ解決できてないじゃん」

と現在進行形でキョドっているおでんです。

取りあえずこういう問題があるよって話だけでも投稿しておこうと思います。