C++プログラマ(というか自分)のためのObjective-C講座(というかメモ)

目次

  1. 全般
  2. クラス
  3. メッセージの送信
  4. idとNSObject
  5. その他

全般

まあ、/Developer/Documentation/Cocoa/ObjectiveC/index.htmlを見れば、細かいことは分かるので、ここではC++に慣れた人から見たObjective-Cの概略をまとめるということで。

まず。

Objective-Cコンパイラは、C++コンパイラに比べるとコンパイラとしてよりもプリプロセッサに近い動きをする。
/usr/include/objc以下にいろんなヘッダがあるんだが、これらをあらかじめincludeしたC、という感じに近い(もちろん文法の拡張がいくつかあるので、それらの解析はObjective-Cの重要な機能なんだけど)。

それは例えば、「動的バインドされるメソッドにアクセスする関数」「動的にクラスを追加する関数」のような、言語仕様の内側部分をいじくっちゃうような機能がドキュメントに載ってるあたりから見ても判断できる。
C++で言えば、前者は「仮想関数テーブルにthisからアクセスできるような関数」が公開されてるのに近く、後者は「newの内部動作が公開されていて、既存のクラスのインスタンスをnewする場合と同じように自分でコーディングすれば(!)クラスを実行時に追加できますよ」という何だかアクロバティックなことに近い(ほんとかよ、て感じ)。

この辺の細かい話は前出のドキュメントのThe Objective-C Runtime Sysytemのところに出てる(いずれまとめたい)。

いずれにせよ、ベースはCなので、C++ともそんなに違わないと言えばそんなに違わない(だいぶ違うと言えば違うが)。

で、C++とObjective-Cは同時に使うことができる!

同時にとは(当初考えていた)「C++のクラスを extern "C" を使ったCラッパーをカマせて、Objective-Cから使う(あるいはその逆)」ではなく、「C++のクラスのメンバ変数としてObjective-Cのオブジェクトを持たせる(あるいはその逆)」「C++のメンバ変数の中から、Objective-Cのオブジェクトへメッセージを送信する」というレベルで融合可能なのであった。ビバ!

件のドキュメント(どうしたATOK15!「くだん」が変換できんぞい!)では「Objective-C++」とか書いているが、そう偉いものではなく、C++とObjective-Cを同時に使えますよ、という話なので、C++とObjective-Cを掛け合わせて使えますよ、というものではない(Objective-CのメソッドをC++のクラスから直接呼ぶ、例えばNSObject pObject = new NSObject;とかできるというワケではない。どちらかのクラスからもう片方のクラスを派生できるというわけでもない)。

「オブジェクト」と「インスタンス」

基本的に、C++では「オブジェクト」も「インスタンス」もほとんど同じに扱われる言葉なのだが、Objective-Cでは微妙に異なる気がする。

クラス自身もオブジェクトになるためではないかと推測する。

クラス自身がオブジェクトになる、というのは変な話に聞こえるが、「クラスとはインスタンスを作るためのオブジェクトである」、つまりデザインパターンでいうところのFactoryなのだということらしい。
なので、クラスオブジェクトというのはあってもクラスインスタンスというのは存在しない(実際にはインスタンスが1個しか存在しない、という言い方の方が適切な気もするが)。
この辺のニュアンスは、もう少し勉強してからまとめます。


クラス

クラス宣言の比較

C++で以下のようなクラス宣言があったとする。

C++ Objective-C
#include "BaseClass.h"


class MyClass : public BaseClass
{
public:
                    MyClass();
    virtual         ~MyClass();



    virtual int     GetValue() const;
    virtual void    SetValue(
                        int     inValue);

    bool            IsValid() const;

    static MyClass* GetInstance();


private:
    int             mValue;
    static MyClass* sInstance;

};

#import "BaseClass.h"

@interface MyClass : BaseClass

{
    int             mValue;

}
- (int)         getValue;
- (void)            setValue:   (int)   inValue;
- (BOOL)            isValid;

+ (MyClass*)        getInstance;    

@end

書き方の違いはあるものの、ざっくり言えばだいたい同じ。@interfaceなんかはJavaに似てる(というかJavaが似せたのか)。
で、どこが異なるか、気づいた点を挙げておく。

  1. #importというプリプロセッサ記述子が追加されている。
  2. 継承時のアクセス記述子がない。
  3. コンストラクタ・デストラクタの宣言がない。
  4. メンバ変数・メンバ関数のアクセス記述子がない。
  5. 関数の引数がよく分からん。
  6. constがない。
  7. staticメンバ変数がない。
  8. virtualがない。

#importというプリプロセッサ記述子が追加されている。

#importは#include+#pragma once機能を持つ。これが優れているのは、#pragma onceが読み込まれるヘッダ側に書かれるのに対し、#importは読み込むほうに書くという点。
どういう違いがあるかというと、「#importはファイルを探さない」。#pragma onceは2回目以降の取り込みでもファイルは検索されて、そののち#pragma onceに出会った時点で残りをスキップする。
ヘッダファイルが100個も200個もある場合、この違いは大きい。同様のことをやろうとすると、すべてのヘッダおよび実装ファイルで、

#ifndef  INCLUDED_BASECLASS_H
#include    "BaseClass.h"
#endif

とする必要がある。

継承時のアクセス記述子がない

継承はすべてpublic継承。これはこれでよいと思う(protected継承とかましてprivate継承とか使わんしな)。
しかし、ほとんど使われていないそうだがアクセス記述子があることはあるらしい。

コンストラクタ・デストラクタの宣言がない

後述。

メンバ変数・メンバ関数のアクセス記述子がない

メンバ変数・メンバ関数のアクセス記述子がないのは、変数はすべてprivate、関数はすべてpublicだから(指定子はあることはあるが使わないということらしい)。
これもこれでOKだろう。欲しいとしてもprotectedメンバ関数ぐらいで、それ以外のは別にいらんし、protectedメンバ関数もなくても全然困らん。

関数の引数がよく分からん。てか関数?

後述。

constがない

不明。あるのか?ないだろな。


staticメンバ変数がない

これも不明。あるかも。

virtualがない

すべてvirtualだから。C++でも、効率さえ考えなければすべてvirtualになるべきだろう。効率を考えてるから「デストラクタはデフォルトでvirtualではないから、基底クラスのデストラクタには忘れずにvirtualをつけるように」なんていうルールがあるのだろうが、Objective-Cはそういう意味で、いさぎよく実行時の効率を捨てている(のだろう)。

クラス定義(実装)の比較

忘れてたけど、ヘッダはC++/Objective-Cともに、.hに書く。うまくすればC++/Objective-C/Cのいずれからも使えるヘッダが作れる(てゆうか標準ライブラリのヘッダがそう)。
実装は、C++は.cpp/.cpに書くが、Objective-Cは.mに書く。拡張子についてはコンパイラオプションなどのカスタマイズでどうにでもなる話ではあるが。

で、さっきのクラスの実装を書くと、

C++ Objective-C
#include "MyClass.h"
#include    <assert.h>


MyClass*    MyClass::sInstance = 0;





MyClass::MyClass()
    : mValue(0)
{
}



MyClass::~MyClass()
{
    mValue = -1;
}



int
MyClass::GetValue() const
{
    return (mValue);
}



void
MyClass::SetValue(
    int     inValue)
{
    assert(IsValid());
    mValue = inValue;
}



bool
MyClass::IsValid() const
{
    return (0 <= inValue && inValue <= 1000);
}



MyClass*
MyClass::GetInstance()
{
    if (sInstance == 0) {
        sInstance = new MyClass();
    }
    return (sInstance);
}


#import "MyClass.h"



static MyClass* sInstance = 0;



@implementation MyClass
/*
- (id)              init
{
    mValue = 0;
}



- (void)            dealloc
{
    mValue = -1;
    [super dealloc];
}
*/


- (int)         getValue
{
    return (mValue);
}



- (void)            setValue:
  (int)         inValue
{
    NSParameterAssert([self isValid]);
    mValue = inValue;
}



- (BOOL)            isValid
{
    return (0 <= inValue && inValue <= 1000);
}




+ (MyClass*)        getInstance
{
    if (sInstance == 0) {
//        sInstance = [[MyClass alloc] init];
     sInstance = [MyClass alloc];
    }
    return (sInstance);
}

@end

C++プログラマがObjective-Cを見て気になるのは以下の点ではないか?(少なくとも私は気になった)

  1. コンストラクタ・デストラクタはどうなってるのか?
  2. 派生・継承はどうなってるの?
  3. てゆうか関数呼び出し?
  4. てゆうか先頭の+/-は何?
  5. てゆうか関数?

コンストラクタ・デストラクタはどうなってるのか?

まず、Objective-Cにはコンストラクタ・デストラクタがない。

変数の初期化を行うという本来のコンストラクタ・デストラクタの機能を超えて、今やC++で一番の便利機能になっちゃった感はするが、実際のところオブジェクト指向と関係があるのかどうか、よく分からん。
どちらかというと、(代入)演算子のオーバーライドがあるために必要になってしまったので付けた、みたいな感じなんじゃないだろうか?(便利だけど)

上の例でコメントアウトしている、initとdeallocがコンストラクタ・デストラクタに近い働きをする。で、これはNSObjectの関数なので、それをオーバーライドする格好になる(上の例ではNSObjectから直接は制してるわけではないのでコメントアウトしてる)。

newに相当するものが[MyClass alloc]の部分。これは後で解説。

派生・継承はどうなっているの?

継承はJava同様、単一継承に限定。
Objective-Cは(Visual Basicなどと同様)動的バインドの言語なので、どんなオブジェクトにどんなメッセージを送ってもよい(受け取りに失敗したときに例外が発生しないという点がVBとは異なる)。
またやはりVB同様、本当の型を知っているオブジェクト(具体的に言えば、そのクラスのポインタあるいはその基底クラスのポインタで指しているオブジェクト)は、コンパイル時にメッセージを受け取れるかどうかチェックが効く。

ところで、 このような動的バインドを持つ場合、インターフェイスを提供するためだけの純粋抽象基底クラスっていうのは要らないということになる(インターフェイスがなくてもコンパイルできるから)。
あと、多重継承みたいなことをしたい場合 は、Javaのimplementsのような格好(Objective-Cではプロトコルという)でクラス宣言が書ける。

てゆうか関数呼び出し?てゆうか先頭の+/-は何?てゆうか関数?引数は?

まとめて。
C++でいうところの関数呼び出しは、上の対比で見たとおり、ObjectPtr->Function(); から [ObjectPtr message]; になる。
ややこしいのが、メソッドセレクタで、C/C++でいうところの関数名+引数リストになんと名前がついた!というような代物になっている。

例えば、C++で、

int SetValue(int inValue, int x, int y);
なんて関数があったとすると、呼び出し側のコーディングに、
p->SetValue(x, y, value);
みたいな記述があったとしてもコンパイルが通るし、目視してもなかなか気がつかんでしょ、ということらしい(のではないかと推測)。
Objective-Cの場合は、
- (int) setValue: (int)inValue atX: (int)x atY: (int)y;
のように宣言することで、
[p setValue: value atX: x atY: y];
は正しくて、
[p setValue: x atX: y atY: value];

っていうのが明らかにおかしいと分かる、ということらしい(のではないかと推測)。

で、Objective-Cのワケわからなさは、いったいどこから来ているのか?というと、やはり「関数名になじめん」ということになるのではないか?
「関数名」(メソッド名)って考えてしまうと、メソッドセレクタの表記がなかなか理解しにくい。

メソッドセレクタは「"setValue:atX:atY:"で一つの関数名みたいなもん」というとらえ方をするとすっきりする。
C++の修飾名(NINameList NINameList::Duplicate() const → _Duplicate__C10NINameList みたいな奴ね。コマンドラインから nm XXXX.oでオブジェクトファイルのシンボルテーブルダンプをすれば見れる)を考えれば、そんなに不思議ではない気もする。

で、「関数名の途中途中に引数を埋め込む」のである。setValue:atX:atY:がホントの関数名なんだけど、引数を間にぶち込んでsetValue:value atX:x atY:yで呼ぶ。

ただし、(1) 区切りがコロン(2)括弧がない(3)カンマがない、っていう3点は、どうしても変な感じがするのは確か(これは慣れだと思う)。
ちなみに、C++で引数の順番を入れ替えると(型が違っていれば)別の関数としてオーバーロードされるのと同様、メソッドセレクタでも上記例のatX:とatY:を入れ替えれば別メソッドになる。

C++で以下のように書いているPowerPlant派の人は、

int
   SetValue(
        int inValue,
        int x,
        int y);

Objective-Cでは

- (int)
   setValue : (int) inValue
   atX      : (int) x
   atY      : (int) y;

てな感じで書けば比較しやすいかも知れない。なんとなくsetValueがメソッド名のような気がしてしまうが、もいっぺん強調しておくと「setValue:atX:atY:で関数名(メソッドセレクタ)である」

ほんでもって、先頭の-とか+は、C++でいうところのスタティックメンバ関数かどうかを表わす。-が普通のメソッド(インスタンスメソッド)、+がクラスメソッド


メッセージの送信

あらためてメソッドセレクタ

C++でいう「メンバ関数」に相当するものが、「メソッド」で、「関数プロトタイプ」に相当するものが「メソッドセレクタ」になる。

C++でいう「メンバ関数呼び出し」に相当するものが、「メッセージ送信」で、「送信先オブジェクトからメソッドセレクタにマッチするメソッドを見つけて処理を行なうこと」になる。

メソッドセレクタの宣言の仕方

ドキュメントからそのままパチったうえ、例を書くと([ ]内の要素は省略可ということらしい)、

項番 内容 文法詳細
1 クラスメソッド定義 + [メソッド型] メソッドセレクタ [宣言リスト] 複合ステートメント
+ (int) getValue { return (sValue); }
2 インスタンスメソッド定義 - [メソッド型] メソッドセレクタ [宣言リスト] 複合ステートメント
- (int) setValue: (int)inValue;
3 メソッドセレクタ

単項セレクタ
キーワードセレクタ [ , ... ]
キーワードセレクタ [ , パラメタリスト ]

setValue
setValue: (int)inValue, ...
setValue: (int)inValue, int inX, int inY
4 単項セレクタ セレクタ
setValue
5 キーワードセレクタ キーワード宣言子
キーワードセレクタ キーワード宣言子
setValue: (int)inValue atX: (int)x atY: (int)y
6 キーワード宣言子

:[メソッド型] 識別子
セレクタ : [メソッド型] 識別子

: (int) whateverYouLike
whateverYouLike: (int) whateverYouLikeToo
7 セレクタ 識別子
whateverYouLike
8 メソッド型 (型名)
(int)

のようになる(3のパラメタリストについては例がウソかも。そのうち検証します)。

メッセージの送信方法

C++で書くところのObject->Function();[Obejct message];になるってのはすでに書いたとおり。これをメッセージ式と呼ぶ。
で、messageの部分はメッセージセレクタと呼ばれるが、やはり先に書いたとおり、「関数名(メソッドセレクタ)の途中途中に引数を突っ込んだもの」。

なので、C++でいうところの

int
SetValue(
    int inValue,
    int inX,
    int inY);

という関数の

Object->SetValue(value, x, y);

という呼び出しに対応するObjective-Cコードは

- (int)
    setValue    : (int) inValue
    atX         : (int) inX
    atY         : (int) inY;

というメソッドを

[Object setValue:value atX:x atY:y];

のようなメッセージ送信で起動するという格好になる。

またキーワード宣言子には、セレクタが付いてなくてもよい(最初の一個のはおそらく必要、定義だけ見ると不要な気がするが)ので、

- (int)
    setValue    : (int) inValue
                : (int) inX
                : (int) inY;

で宣言し、

[Object setValue:value :x :y];

でメッセージ送信しても良い。

クラスオブジェクト

newの代わりに、[MyClass alloc];でアロケートするっていうのも書いたけれど、これもメッセージ式。
メッセージを受け取るのは誰かというと、MyClass。こいつもオブジェクトなのであった。

誰がどのタイミングで作っているのは今の段階では不明。

で、MyClassにメッセージを送信すると、クラスメソッドが起動される(そのメソッドがなければnilが返る)。

ちなみに、alloc自体もNSObjectのクラスメソッドなので、NSObjectから派生してないやつは、ゆうたらnew/deleteできない(ような気がする)。C++でいうとグローバルのnew/deleteがない感じ。


idとNSObject

idとは?

早い話、クラスのインスタンスのポインタ。

id someObject;

とあったら、このsomeObjectにはどんなメッセージでも送ることができる(受け取って処理できるかどうかは別)。

メソッド(クラスの関数みたいなもん)のデフォルト返値型(プロトタイプから省略したときの返値型)は、ANSI Cの「関数」のデフォルト返値がintであるっていうのとは違って、id型である。
(返値型を指定しないことなんてここ10年以上ないので関係ないが)

NSObjectとは?

こいつがObjective-Cとどこまで切り離されているか、検証するのが今後の課題。「言語仕様的にNS~に依存している部分も少なからずある?」というのが、今のところのイメージ。
前出のNSObjectを継承していないalloc/deallocとか。new/deleteがない(alloc/deallocがNSObjectのクラスメソッド)というのと、コンストラクタ・デストラクタがない(init/release?)もない(やはりNSObjectのクラスメソッド)というのが、どの程度言語仕様なのかは見極めが必要。

あと、@"テキスト"という記法があるが、これは"テキスト"がconst char*になるというANSI Cの仕様に対し、@"テキスト"はNSStringのオブジェクトになるという「それってライブラリ?言語仕様?」という、C/C++あるいはJava(Javaもこの辺はかなり微妙か)プログラマから見ると、かなりVB的(要は1社で仕様決めしてる言語的)な扱いになっている。

個人的には、よっぽどプリミティブな仕様のクラスでないかぎり(つまり日付・時刻型とか複素数型とかBDC型とかでない限り)、NSObjectから派生させるのが正しい、と理解した。

C++がいろんな機能を言語自体に盛り込んでいるのに対し、Objective-CはNSObjectという、どこまでが何の仕様?(笑)な部分に機能を押し込むことにより、言語仕様自体を軽くしている。C++のワナの多さに比べると好感が持てるという人がいてもおかしくない(私はワナになれちゃったので、どっちでもよいけど)。


その他

演算子のオーバーロード

正直、C++に備わっている演算子のオーバーロードは諸刃の剣で、だいたい自分側に向いている刃の方が鋭くよく切れるので、例えば自分のチームの若手にはあまり使って欲しくない。どんなケースでもうまく動作する、というところまでブラッシュアップされるのにアホほど時間がかかるからだ。

Objective-Cは演算子のオーバーロード機能はない。

で、気を付けなければならないのが、NSStringのオブジェクトを比較するケース。

C++の標準ライブラリ(std::string)やMFC(CString)、PowerPlant(LString)などは演算子がオーバーロードされてて、==で文字列の比較ができるが、Objective-Cではそれはポインタの比較になってしまうので、中身が同じならOK、ということにするにはisEqualToString:やcompare:というメッセージを送って比較する必要がある。慣れればいんだけど、最初は大変かも。

更新日: