GNUstepでobjective-cを学ぶ(4)

objective-cを使うと、「id」という型がよく使われます。どうやら、smalltalkがそうらしいのですが、メソッド(あるいはメッセージ)を実行時にチェックしているために、このid型が使われます。
所謂、voidポインタのように使えるわけですが、objective-cのnilポインタ(NULLポインタのようなもの)に対してメソッド呼び出しをしても、「何も起こらない」という特徴があります。

ええとですね。C++やJavaとobjetive-cのメソッド呼び出し方が違うんですね。

C++の場合は、vtbl構造体に配置されている関数ポインタを直接呼び出します。
つまり。

1
obj->func( ... );

な形になるわけです。

ですが、objective-c の場合は、次のように「func」という関数名を探し出した後に、メソッド呼び出しをするんですね。

1
2
3
4
Func *func = obj->seekMethod( "func" );
if ( func != NULL ) {
 func( ... );
}

当然、実行時にはC++やJavaのほうが早いのですが(更にC言語の呼び出しのほうがアセンブラ的には早い。単純な call 命令だからね)、まぁ、色々便利な機能ではあるのです。

C#では「デリゲート」という機能がありますが、これは、主として windows gui のイベント駆動をうまく動かすための仕組みです。デリゲートがない場合は、java ではリスナー、C++ では関数ポインタなどが使われます。

いわゆる、インターフェースを定義して呼び出す、というリスナーパターンです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// インターフェースを定義する
class Listener {
public:
 void func( void );
};
// 実装クラス
class Window : public Listener {
 void func( void ) {
  ...
 }
}
 
int main( void )
{
 Listener *win = new Window();
 // ここで func メソッドを呼ぶ
 win->func();
}

C++ の場合は、もっと直接的に、関数ポインタを使う方法もあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Window {
 void func( void ) {
  g_func();
 }
}
 
// グローバルな関数
static void g_func( void ) {
 ...
}
 
int main( void )
{
 Window *win = new Window();
 // ここで func メソッドを呼ぶ
 win->func();
}

長い前置きでしたが、これからが本題。
オブジェクト指向の便利なところは、クラス(カテゴリ)に分けておいて、同じメソッド名を使えるというところです。文法でいえば、メソッドが「動詞」で、オブジェクトが「主格」あるいは「目的格」ですね(このあたり、昔から議論がありますが、省きます)。

なので、A クラスと B クラスという2つのクラスに、同じ print というメソッドを使うと、

1
2
3
4
5
A a;
B b;
 
a.print();
b.print();

のような形で、同じメソッド名を使えます。この print メソッドは、基底クラスから継承するようにして、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:
 void print( void );
}
class A : public Base {
public:
 void print( void ) {
  ...
 }
};
class B : public Base {
public:
 void print( void ) {
  ...
 }
};

のように定義しておいて、実行時に切り替えることが可能なのです。

1
2
3
4
5
Base *a = new A();
Base *b = new B();
 
a->print();
b->print();

これは、それぞれの print メソッドが呼び出されます。これはリスナーパターンの基本ですね。

さて、このインターフェースの機能ですが、メソッド名に制約を付ける、という恩恵があるのですが、逆に言えば「基底クラス/インターフェースにあらかじめ、メソッドを用意しないといけない」という制限があります。
当たり前と言えば、当たり前なのですが、インターフェースにを作成するときに「未来の機能を予測しないといけない」というジレンマが発生します。このために、基底クラスが膨大になってしまったり、やたらに仮想関数の多いクラスが作成されてしまいます。
# 残念ながら、デリゲートも同じ制限があります。

残念ながら、我々は予知力がありません(笑)ので、これから作成されるサブクラスの機能を完全に予測することはできません。つまり、同時期に作ればよいのですが、数年後に拡張される機能を、基底クラスに盛り込むことは不可能なのです。

そこで、objective-c の(一見すると)不思議なメソッド呼び出しが功を奏します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface A
- (void)print ;
@end
@implementation A
- (void)print {
 ...
}
@end
 
@interface B
- (void)print ;
@end
@implementation B
- (void)print {
 ...
}
@end

のように基底クラスを「共有しない」で定義しておいて、id 型で受けます。

1
2
3
4
5
id a = [A new];
id b = [B new];
 
[a print];
[b print];

こういう風にすると、まるで「あらかじめメソッド定義している」ように見えるんですね。
まぁ、コンパイル時にチェックが入らないので、インターフェースを使った型の厳密性は損なわれるのですが、あたかもRubyのようにメソッドが追加できるのが面白いです。

と言う訳で、基底クラスを使わないリスナーパターンということで。サンプルを晒しておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#import <stdio.h>
#import <Foundation/Foundation.h>
#import <Foundation/NSObject.h>
 
@interface A : NSObject
- (void)print ;
- (void)printX:(int)x ;
@end
 
@interface B : NSObject
- (void)print ;
- (void)printX:(int)x y:(int)y ;
@end
 
@implementation A
- (void)print {
 printf("in class A\n");
}
 
- (void)printX:(int)x {
 printf("in print A: %d\n", x );
}
@end
 
@implementation B
- (void)print {
 printf("in class B\n");
}
- (void)printX:(int)x y:(int)y {
 printf("in print B: %d %d\n", x, y );
}
@end
 
id CreateObject( int type )
{
 id obj ;
 switch ( type ) {
 case 0:
  obj = [A new];
  break;
 case 1:
  obj = [B new];
  break;
 default:
  obj = nil;
 }
 return obj;
}
 
int main( void )
{
 printf("hello4.m\n" );
  
 A *a = [A new];
 [a print];
 [a printX:10];
 [a release];
 
 id x = [A new];
 [x print];
 [x printX:10];
 [x release];
 // 同じidを使える
 x = [B new];
 [x print];
 [x printX:10 y:20];
 [x release];
  
 // こんなこともできる
 x = CreateObject(0);
 [x print];
 [x release];
 x = CreateObject(1);
 [x print];
// [x printX:10]; // 間違えてもコンパイルエラーにならない。
 [x printX:10 y:20];
 [x release];
  
 // nilに対しては何も反応しない
 x = CreateObject(10); // nil を返す場合
 [x print];
 [x release];
  
 return 1;
}
/**
実行結果
 
hello4.m
in class A
in print A: 10
in class A
in print A: 10
in class B
in print B: 10 20
in class A
in class B
in print B: 10 20
**/
カテゴリー: 開発 パーマリンク