☆Objective-C勉強室
検索していた見つけたんだけど、
こんなページがある。
ここって、私のここの内容をちょこちょこっと変えただけのコピーページである。
いや、別に元にして作ったというのを報告してくれてれば全面的にだめという気はないのだけど、
無断で、ここまであからさまにコピーされたらいくらなんてもなぁ、という感じ。
ソース見てて確信した。まあ、念のため。
基本
まずは用語と基本を覚える。Objective-Cおよびオブジェクト指向言語特有の名称が多いが、
覚えやすくするためにCの名称に置き換えることもする。その対応表の作成は後日に行うかもしれない。
要するにここはC言語は習得しているが、C++は勉強したけどあきらめた人(=用語ぐらいは少しは解る)向けで、
「普通」とは異なるので注意(自分以外の誰が読んでんねん、という突っ込みは無視^_^;)。
まずは、絶対覚えなければならない基本用語。
オブジェクト | メモリ上に配置されるワークエリアの一般名称 |
クラス | オブジェクトのひな形(構造定義書)
Cの構造体に近く、その中に制御関数;メソッドも含む |
メソッド | クラスの専用制御関数 |
インスタンス | クラスを実際にメモリ上に割り付けた「オブジェクト」実体 |
インスタンス化 | クラスを定義しただけでは構造体宣言と同じく実体は存在しないので、
それを元に変数を宣言、実体をメモリ上に確保すること |
継承 | あるクラスの機能を受け継いでで新規のクラスを定義すること
親クラス(スーパークラス)の全ての機能を受け継いだ上で、新機能または置き換え分のみ実装すればいい |
オーバーライド | 既存メソッドを同名メソッドで再実装すること。
基本的には、子クラスで親クラスに存在するメソッドを同名メソッドで再実装する。 |
レシーバー | メソッドを実行するインスタンスのこと。 |
Cの構造体だって関数をメンバーに出来るので(関数だって所詮はアドレスに過ぎないから)、
よく言われる「構造体とクラスはメソッドの有無が違い」というのは実は間違い。
Cの構造体のメンバー関数は「呼び出す」というし、アセンブラレベルでもCallするが、
Objective-Cのメソッドは「メッセージを送る」という。
実はObjective-Cの基本にして重要な思想がここに現れている。Cのメンバー関数は(基本的に)構造体に固定されているが、
メソッドは受信するオブジェクトを選択することが出来る。
異なるクラスに同じメソッド(名)を実装した場合、それは「同じメッセージを受信できるクラス群」として扱われる。
そして、あるメッセージを送信した場合、それを実際に受信するクラスによって動作を切り替えることが出来る。
Cでなら、同じメンバー名関数を持つ構造体群があったときに、場合によって構造体側を切り替える場合には
switch caseか関数の実行アドレスを持つ配列に入れて切り替えるしかないが、
Objective-Cでは受信するクラス名を変数に出来るため、このあたりが実に簡潔に記述できる。
この両者の「基本概念の向きの違い」を覚えておかなければならない。それが「オブジェクト指向」である
(この場合「思考」の方が合ってそうだけど)。
Objective-CはCの完全な拡張であるので、一部に制約が付いたC++とは異なり、Cで使えた書式は
全て使える。
変数の型も同じだが、typedefにより、見かけ上いくつか追加がある。
id | オブジェクトを示す汎用変数(正確にはインスタンスのアドレス=ポインタ) |
Class | クラスオブジェクト(クラスの定義内容そのものを持つオブジェクト)へのポインタ |
SEL | メソッドのセレクタ値 |
IMP | メソッドの実行アドレス(ポインタ) |
BOOL | YES/NOのどちらかの値を持つ型 |
また、以下の定数もobj.hをimportすることで使える(標準定数)。
定数名 | 用途・意味 | 定義 |
nil | CにおけるNULLをidに拡張したものと思えば正解。終端やエラーを示すのに多く用いられる | (id)NULL |
YES/true | BOOLの肯定 | (BOOL)1 |
NO/false | BOOLの否定 | (BOOL)0 |
NSNotFound | 要素を探したが見つからないとき | 開発環境によって異なる |
もちろんCの標準定数(NULLなど)も使える。
BOOLはCでも定義して使っている人が多いけど、私は使わない主義であった。
関数の改良時によく、それ以外のリターン値を返すようにしてたからである。
余談。実はCocoa Touch上には真偽値を示す型が複数ある。
BOOL/Boolean/boolである。
NS/UIクラスで使われているのはBOOL、しかし、plistという物で使われているのはBooleanであったりする。
Boolean/boolではtrue/falseが値として使われるが、YES=true/NO=falseである。
boolがISO Cの標準真偽値型のようなので、正確にはifで判定されるのはbool型だと言えるが、
混在してもコンパイラのチェック上でも動作でも特に問題は起こさない。
まあ、NS/UIクラスと同じくBOOLで統一しておくのが無難かも知れない。
BOOLはObjective-Cのソース、すなわち.m内でのみ使える。
BooleanはCのソース、すなわち.Cでも使える。C内ではBOOLはエラーになる。Booleanはどちらでも使えると思う。
実際の値は同じだが、相互に使える使えないがあるので、注意が必要である。
Cの関数も全くの変更なしに呼び出すことが出来る(C++はextern "C"でヘッダーの書き換えが必要)が、
一部機能は強化されている。
CとObjective-Cの細かい違いとしては、
- ヘッダーは「基本的に」#includeではなく#importで読み込む
プロトタイプ宣言が必須(前方参照不可)Xcode4.3では同一ファイル内では自動的に前方参照が解決されるようになった。
- 一部の例外を除き、変数宣言をスコープの先頭で行う必要がない
と言うのがある。
#importはCのヘッダーで問題になっていた2重読み込みを自動的に排除してくれる。
ただし、#defineで定数の定義内容を書き換える(切り替える)タイプのヘッダーを#includeする場合は、
#importに変更するとバグになる可能性がある。
Cのヘッダーは#includeで、Objective-Cのヘッダーは#importで読み込むのがいいかもしれない。
メソッドは、基本的に宣言と実体実装を別ファイルにするため、宣言ファイル(ヘッダー)を#importしておけばいいが、
通常のC関数も、それがプロトタイプ宣言されているヘッダーを読み込んでおく必要がある。
(本当はクラスの独立性を高めるためらしいが、)
ヘッダーを#importするのが面倒な場合で、クラス名であることを宣言するだけで良いなら、
@class クラス名1,クラス名2...;
というコンパイラディレクティブを使って宣言しておける。Cにおけるexternに近い。
「コンパイラディレクティブ」とは、Objective-Cで拡張されたプリプロセッサ命令である。
プリプロセッサ命令であるため、コンパイル時には実内容に展開される=静的に決定される。
古いCの場合、ローカルに新規の変数を使う場合は必ず新しいスコープの先頭(宣言文以外が出てくるまで)に宣言しなければならなかったが、
Objective-Cではこれがかなり緩くなる。本当は最近のCの規格ではそういうものらしい。
ただし、一部例外がある。
Cの場合 Objective-Cの場合
{ {
int x; int x;
x=1; x=1;
{ int y; // 必要時に宣言すればよい
int y; y=x+1;
y=x+1;
}
} }
Cの場合 Objective-Cの場合
switch (x) { 左に同じ
case 1: switch内だけはスコープを新規に作る必要がある
{ (厳密にはcase文の直後に宣言するときのみ、新規スコープの宣言が必要。)
int y;
y=x+1;
}
}
という具合である。ただし、正確には上の例は変数yのスコープが異なる。Cの場合の記述は{}の中だけで有効だが、
Objective-Cの記述では宣言以降全てである。従って、厳密にスコープを規定する場合は、Objective-Cでも
Cと同じにする。単に「使うときに宣言したい」という意味の時のみ書き換えることが出来る。
各種名称の命名規則は基本的にCと同じであるが、「習慣的」には推奨がある。
クラス | 大文字から始める |
メソッド | 小文字から始める。_(アンダーバー)から始まる名称は禁止(アップル予約) |
カテゴリ | 大文字から始める |
プロトコル | 大文字から始める |
ラベル | 小文字から始める |
型名 | クラス名に準じる(だから見分けが付かなくてややこしい) |
変数 | 小文字から始める |
定数 | Cの場合大文字だけど、Objective-Cはクラス名に準じる(だから見分けが付かなくてややこしい)
ただし、Google推奨記法ではkで始めると書いてある。アップル標準もそういうのが多い。
|
また、「同じ名前を付けたときにどうなんねん」については、以下の規則がある。
- メンバー変数名とメソッドの引数が同名の場合は、メンバー変数名が隠蔽される(要注意)。この場合、self->で回避できる(メンバーアドレスへのアクセスであり、プロパティ式ではない)
- メソッド名および引数ラベル名は予約語名と同じでもかまわない(int:とか)
- メソッド名および引数ラベル名は#define定数名と同じではいけない。プリプロセッサの方が先にソースを通すため
- クラス同じ名前のグローバル変数はあってはいけない(コンパイルエラー)
- 異なるクラス間では同じメソッド名を使うことが出来る
- 継承したクラスが親と同じメソッド名を持つ場合は、親のメソッドの代わりに実行される=オーバーライド
親のメソッドを実行する場合は、superを使う
- 継承したクラスに親と同じメンバー変数名を持ってはいけない(コンパイルエラー)
- 異なるクラス間では同じメンバー変数名があってもかまわない
- メンバー変数とメソッド名が同じでもかまわない。というか積極的にそうしてメンバー変数への直アクセスをさせないようにする=プロパティ
- インスタンスメソッドには、クラスメソッドと同じ名前を付けてもいい(対象によって自動的に切り替わる)
- プロトコルには、クラス、カテゴリ、その他のものと同じ名前を付けてもいい
- 1つのクラスのカテゴリには、他のクラスのカテゴリと同じ名前を付けてもいい
この他はCと同じ。詳細はその都度。
Objective-Cでは同じ名前でも、機能さえ異なればちゃんと識別される場合が多いということだ。
同一機能であれば同じ名前にしておくとわかりやすいかもしれないが、
だからといって全然異なる機能に同じ名前を付けるとバグの元なので避けるべきである。
メソッドの宣言
メソッドは以下の書式で宣言する。
関数のプロトタイプ宣言に近い。Objective-Cでは関数名から引数のラベル名までを含め全体を「メッセージ」と呼ぶ。
-(リターン型)メソッド名:(引数型)引数; // 1引数の場合
-(リターン型)メソッド名:(引数型)引数1 label2:(引数型)引数2; // 2引数の場合
-(リターン型)メソッド名:(引数型)引数,...; // 可変引数の場合
リターン型は必ず()で括る。なぜかというと、Objective-Cの基本型はintではなくid型というものになっているからである。
要するに、この()はid型からの型キャストを示しているのである。事実id型なら()も要らないし、省略も出来る。
しない方が良いけど。
id型はクラスに対し、動的に(=実行時に確定される)型や内容を変更することが出来る万能型。int等基本型の代わりにはできないけど。
idの本性は、オブジェクトのアドレスを示すポインターである。typedefで*を含めて名称定義してあるようなので、
*は省略する。
引数の区切りは「:」であり「,」ではない。「,」は可変引数を示すからであるが、可変引数はあまり使われない。
Apple提供のクラスでも、それほど多くは出てこない。
Objective-Cでは、同名メソッドを引数別に定義出来るため、必要性が少ないのだろう。
可変引数の取り出し型は、Cと同じくva_listとva_start()を使う。
2引数以上の場合、引数名にラベルを付けるのが「習慣」だそうな。ラベルはその直後の変数の物。
すなわち、Objective-Cではメソッド名は第1引数のラベル、その後各引数にラベルを付けるということだ。
ラベルなのでラベル名と:に間にスペースを入れてはいけない。
Objective-CはCからの拡張実装であるから、すでにそれが持っているラベル認識機能をうまく使った実装であると言えよう。
(実はラベルは省略可能だが「推奨されていない」。)
ラベル名を変えると、同じメソッド名でも異なるメッセージとして扱われる。
これはオーバーライドとはちょっと違い、メッセージの受信オブジェクトの変更になる。
メソッドはメンバー変数名と同じであってもかまわない。通常メンバー変数は外部から直アクセスさせることはないかららしい。
ただ、初心者は分けた方が迷わなくて良いかもしれない。
ラベル名はCの予約語名とは同じでも大丈夫だが、#define定数名と同じではいけない。
プリプロセッサの方が先にソースを通すためである。
1つのメソッドで複数の引数が同じラベル名を持っていても良いようである。
NSObjectには-(id)performSelector: withObject: withObject:というメソッドが存在したりする。
が、あんまり使うべきじゃないのでは、と思ったりする。
先頭の「-」はインスタンスへのメソッドであることを示す。
他に「+」というのもあって、これはクラスに対するメソッドであることを示す。
インスタンスメソッドは実体を持ったオブジェクトに対して実行される関数であり、
クラスメソッドはクラスに実体を持たせる時に実行させる関数、もしくはクラスの特性をして持っておくべくメソッド
と理解すればいい。
インスタンスメソッドはselfおよびメンバー変数にアクセスできるが、
クラスメソッドはそれがアクセスできない。
当然コンパイルするとエラーが出る。
クラスメソッドは
クラスを実体化=インスタンス化するときなど、
インスタンスがない状態でも呼び出されるからである。
init=初期化されていない/またはalloc=確保されていないクラス内のメソッドを呼び出すとどうなるか。
実は何も起こらない。エラーも何も発生せずに素通りしてしまう。
Objective-Cでは初期化してしていないクラスはnilであり、nilへの呼び出しは全て無視するようになっているからである。
もし実装したはずのメソッドが実行されないときは、そのインスタンスが正しく初期化されているか確認すべきである。
クラスの宣言
クラスはObjective-Cの肝であり、Cで言う所の「構造体」を大幅に機能/思想拡張した物である。
クラスは通常何らかの親クラスからの継承で作られる。
表面上、何も継承することがない場合でも、クラスの基本的機能を内包させるため、
NSObject(一部NSProxy)を親にする。
親クラスを書かない場合は、それらのクラスが持つべき全てのメソッドを自前で実装する必要があるので、
よほど腕に自信がない限り行うべきではない。
メーカー提供のクラスは必ずNSObject(相当)を親にしている。
クラスの宣言には@interfaceコンパイラディレクティブを使う。
クラス名は習慣的に大文字から始めるらしい。
@interface クラス名:親クラス名
{
変数型 メンバー変数宣言;
}
メソッド宣言;
@end
「例」
@interface AClass : NSObject
{
id var;
}
-(void)setVar: (id)v;
@end
メンバー変数を持たないクラスの場合、{~}の部分は省略できる。
変数の型はCと同じ基本の型(int等)、もしくはid型を含む後述のクラス型となる。
クラス内で実装されるメソッドにとってメンバー変数は、プログラムの記述上はグローバル変数とほぼ同じ扱いができる。
これは楽そうに思えるが実は、「ローカル変数を使うつもりだったが実は未宣言で、同名メンバーがクラス内にある場合、
コンパイルエラーにならない」ので要注意である。このあたりは命名規則を作って避けるようにしなければならない。
NSObjectから継承した場合、メンバーにはid self;というものも自動的に定義される。
self->メンバー名でメンバーに確実にアクセスできる。
メソッドの引数がメンバー名と重複する場合に有効であるが、これはCと同じで引数がグローバル変数を
隠蔽してしまう「バグの元」なので、別の名前にするのが本筋であろう。慣れるまでは。
メソッド実体実装
メソッドの実体実装は、@implementationで行う。
@implementation クラス名
+(リターン値)関数名:(引数型)引数 // クラスメソッド
{
return(リターン値); // 当然(void)なら省略
}
-(リターン型)関数名:(引数型)引数 // インスタンスメソッド
{
return(リターン値);
}
命名規則はCと同じだが、習慣的にメソッド名は小文字から始めるらしい。
(Cとしての通常変数名とメソッド名は同じになっても良い。でも分けた方がわかりやすいのは当然。)
クラス宣言と実体記述は、ファイルを分離するのが「原則」である。
ファイル名は同じで、拡張子はヘッダーは.hでCと同じだけど、実体は.mと異なる。
実はXcodeでは.cと.mでコンパイル時の挙動がかなり変わる。
自動的にimport/includeするファイルが異なり、また.c内では、ごく一部を除きObjective-Cによる拡張部分は使えない。
<UIKit.h>のimportも許されないし、@"~"もエラーになる。と言うことは、それを使うNSLog()などの関数も
使えないと言うことを意味する(代わりにprintf()を使う)。
先に書いたとおり、BOOLも.c内では使えない。
例外は、私の知る限りでは#importくらいである。これはプリプロセッサ命令だからだと思される。
逆に、.m内ではCの全てが使える。従って、基本的には.mで統一しておいた方が面倒がなくて良い。
Objective-Cではインスタンスもしくはクラスにメッセージを送ることでメソッドを実行させる。
この「メッセージを送る」式はCに対して拡張された部分なので、識別できるように[]で括る。これをメッセージ式という。
あるメソッド中で同じクラス内の別のメソッドを呼び出す場合は[self 関数]と記述する。
Objective-Cにおいて、メソッドの呼び出しは基本「オブジェクトへのメッセージ送信」の形を取るからである。
自分自身クラス中である場合にはselfが必要となる(メソッド名(引数)という形で呼び出すことは出来ない)。
「基本的に」Cで記述された部分とObjective-Cで記述された部分は切り離されているからである。
その垣根を越える方法はあるが、難しいので省略。
// _____引数型
-(int) CheckDropPoint:(int)x arg2:(int)y
// ~~|~~ ~|~~
// 引数型 第2引数ラベル
{
~
return(1);
}
- (int) Cascades
{
while ([self CheckDropPoint:x arg2:y]==0) { // ←ここ要注意
r=[super getScrn:x+1 arg2:y]; // ←親クラスのメソッドに対してはメッセージで送る
~
}
return(0);
}
クラスインスタンスを保存しておく必要がない場合は、その確保宣言そのものを省略して、
[[Class alloc] method]
と記述することも出来る。
プログラムの実行がmain()からなのはCと同じ。
iOSアプリケーションの場合はもう1段上のクラスからの呼び出し処理が入るのだけど、
その辺りは
「アプリケーション実装の勉強」で。
int main()
{
id obj = [Class alloc];
[obj method];
return (0); // iOS上ではexit()は使えない
}
メソッドは、クラスを@implementationで実装すれば使えるようになる。
処理実体がメモリ上に存在するということである。
しかし、@implementationしてもメンバー変数は確保されない。
クラスインスタンスの実確保は別途行う必要があるということである。
メンバー変数にアクセスするメソッド=インスタンスメソッドはクラスの実体が確保されるまで実行できないが、
クラスの構造だけに依存するメソッド=クラスメソッドはこの時点で実行できる。
この「実体の存在」の認識はObjective-Cでは非常に重要となる。
式の記述上は似ているメンバー変数とグローバル変数には、実は根本的な違いがある。
「メンバー変数はアドレスが固定でない」ということである。
グローバル変数は、コンパイラがコンパイル時にそのアドレスを確定する。
(正確にはリンカーやOSがプログラムをメモリ上に読み込み際に確定される)。
だから、プログラム内どこからアクセスしても同じアドレスにアクセスする。
しかし、クラスメンバはインスタンスによってアドレスが変わる。
だから、記述上は同じに見えても実行時のアクセスという意味では全く異なるわけである。
Cで構造体の制御関数を作るときは、必ず、引数に構造体(多くの場合そのアドレス)を渡すことが必須となる。
制御関数内でのメンバーへのアクセスは、その引数のアドレスからの相対アドレスによる。
ところがObjective-Cのクラス定義では@implementationにクラスのアドレスを渡す記述はない。
ないが、内部的には、クラス内のメソッドが実行されるときには、自動的にその時使うクラスインスタンスのアドレスが渡される。
逆に言えば、クラスインスタンスのアドレス確定=領域確保は絶対必須であるということである。
クラス実装はあくまでひな形であって、クラスそのものの領域確定宣言そのものではない。
このあたり、慣れるまではクラス実装を書いただけで、インスタンスがあるような錯覚を起こすことがあるので要注意である。
ここからは少し、高度なお話。
iOSは実はかなり高度にマルチタスクを行う。
少なくとも、メソッド単位ではマルチタスクが基本になっているので、
「このメソッドを呼び出して、返ってきてからこの処理が走って・・・」と考えてはいけない。
メソッドを呼び出したら、帰りを待たずに呼び出し側は次の処理を実行してしまう、こともある。
どこが待ってどこが待たないかは実のところはっきり理解してないのだが、
たぶん「リターン値を持たないメソッドは並行動作する」で間違いないと思う。
少なくともUIKitは呼び出したら即並行動作するので、
「表示されたはずだから」と想定してその後の処理を書いてはいけない。
表示が完了したらデリゲートが呼び出されるので、そこで後の処理を書く必要がある。
あまりに高度な並行動作なので、頭の中でなかなか追い切れなかったりする。
自分でマルチスレッドなどを書いたらさらに複雑になるわけで、
μITRONのようなイベントドリブン型マルチタスクに慣れているなら、考え方を根本的に変える必要があるかもしれない。
(私がそうだった)。
絶対に処理終了を待つ必要がある場合は、別途フラグなどで判定して待つ必要がある。
しかし、ループで単純に待ったら他のスレッドが走らなくなるので、いろいろと対策を入れる必要がある。
X-BASIC'(X-BASIC for iOS)はマルチタスクでないプログラム動作を実現するために、このあたりが
かなり複雑になっている。実はBASICのコア自体がバックグラウンドで動いてたりするのだが、
その辺の話は、そうさなぁ、1万本くらい売れたら書くと言うことで(^_^;)。
super
クラスを継承した子クラスでは、親クラスを同じ名前のメソッドを作り、
処理を置き換えることが出来る。これをオーバーライドという。
オーバーライドした時でも、親のメソッドを呼び出す必要がある場合は「super」を使う。
(親クラスは英語で「SuperClass」と書く。子クラスは「SubClass」。)
@interface SuperClass : NSObject
// 変数がないので{}は省略
- (void)method;
@end
@interface SubClass : SuperClass
// 変数がないので{}は省略
-(void)method; // 同一名関数を作りオーバーライドする
@end
@implementation SuperClass
- (void)method
{
printf("SuperClass.method\n");
}
@end
@implementation SubClass
-(void)method
{
printf("SubClass.method\n");
[super method]; // [SuperClass method]を呼び出す
}
@end
void CallMethod(id obj)
{
[obj method];
}
int main()
{
CallMethod([SuperClass alloc]); // [SuperClass method]が実行される
CallMethod([ SubClass alloc]); // [SubClass method]が実行される→その中で[SuperClass method]も呼び出される
return 0;
}
実行結果(未確認)
SuperClass.method
SubClass.method
SuperClass.method
こうした時、CallMethod()は引数によって呼び出し関数を変更することになる。
Cではswitch caseか関数のアドレスを持つ配列を使った呼び出しUByte (*fnc)()[]を使ったが、
その辺りの記述が大幅に簡潔になる。
superはselfとは違いアドレスを示すものではなく予約語なので、super->という使い方は出来ない。
インスタンスの確保/初期化/解放とAutorelease Pool
クラスのメンバー変数やインスタンスメソッドは、クラスを宣言するだけでは使えない。
実体が存在しないからである。利用時には必ず実体の確保が必要となる。
これは、基本的にはCの構造体と変わらない。
構造体の場合、その確保はグローバルまたはローカルの変数として宣言することで行う。
typedef struct {
int a;
int b;
} SINT2; // 構造体の宣言
SINT2 x; // 実体の確保
Objective-Cでも宣言に関しては(記述方法はともかく))基本的に同じであるが、実体の確保はかなり異なる。
クラスを示す変数はアドレスで宣言し、実体確保もメソッドで行うである。
クラスの実体はローカルでもグローバルでも直接確保は出来ない。
なので、
// CINT2がクラスの場合
CINT2 x; // エラー
はエラーとなる。宣言時はクラス位置を示すアドレスを宣言、実体確保は別途allocメソッドで行わなければならない。
CINT2 *x;
x=[INT2 alloc];
~
[x release];
C風に書くなら、
x=malloc(sizeof(CINT2));
~
free(x);
となるだろう。
なぜこんな面倒なことをするのかであるが、
Objective-Cのクラスは、実行アドレスの確定を初め動的な部分が多いので、
ある特定のメモリ領域に一括して格納し、管理するためではないかと思われる。
Cのヒープ領域よりもっと高度に管理されたメモリ領域を使うということだ。
そのため、位置不定のローカルや、逆に位置が完全に固定されるグローバルな確保をさせないのだと思う。
クラスの利用を宣言するとき、多くの場合2つの確保し方/され方がある。
alloc/copyなどで明確に領域の確保を指定する場合と、クラスインスタンスを使って確保する場合である。
たとえば、NSStringでは、
(1)NSString *str1=[[NSString alloc]initWithString:@"文字列"];
(2)NSString *str2=[ NSString stringWithString:@"文字列"];
の2つは、どちらも領域を確保して、その実体として@"文字列"をコピーする。
もっと正確に言えば、str1/str2には確保された領域の先頭アドレスが入り、
そのポインタが指し示す領域に"文字列"が格納される(ここではどのように文字列が格納されているかは言及しない)。
ではこの2つは同じなのかというと、「メソッド内で使うだけなら同じ」であるが「メソッドを抜けた後の動作が全く違う」。
(1)がmalloc相当による確保、(2)はAutorelease Poolという自動開放領域への確保となる。
(1)はメソッドを抜けてもその領域は確保されたまま=有効であるが、
(2)はメソッドを抜けると、あるタイミングで自動解放されてしまい内容が不定となる。
メソッドだけでなく、関数で使う場合も同じ。
(1)は必ず対応するreleaseが必要となる。
それは、同一関数内である必要はない、と言うか、無理な場合も多い。
その場合は呼び出し側でreleaseすることを忘れてはいけない。
ただし同一関数内でreleaseしないと、Analyzeを掛けたとき警告が出る。
基本的にallocで確保したものをそのまま関数のリターン値に使うことは避けた方が良い、という考えなのであろう。
ちなみに、自分で翻訳しておいてなんだが、アップルの
メモリ管理の規約に従えば、メソッドや関数内でallocしたままリターンし、
呼び出し側でreleaseするのは御法度とされている。クラスメソッドを使って返すか、
allocする場合はautoreleaseしておけと書いてある。
今度からそうしよう(^_^;)
なおreleaseは厳密にはfree()とは異なる。詳しくは次の
オブジェクトの所有権に譲るが、
即時解放ではなく解放要求である。
というのも、Objective-Cではオブジェクトは複数から保持要求されることがあり、
その場合、全てのrelease要求が揃った時点で初めてメモリが解放されるからである。
Cocoa Touch上では、NSArray等のように要素を自動的にretainするようになっているクラスあり、
その場合、プログラムの記述上では要素に代入したら即時relaseするような、
Cで考えたら「おかしいんじゃねぇ?」と思えるような書き方も当然のように必要だったりするが、
それもreleaseが「即時解放」ではなく「解放要求」だからこそ成立する仕組みである。
(2)のようなクラスメソッドによる確保は、いちいちallocする必要もないし、終了時にreleaseも必要ない(してはいけない)。
なので、ローカルで使うには便利であるが、逆にローカルから外へは持ち出す=リターン値として返す場合は、
受け側でcopyするか、retainして明示的に解放を遅らせる必要がある。
それをしなかったらどうなるか。それは、
str2の記憶しているポインタアドレスそのものは変化しないのに、
その指し示す領域の内容が変化するということになり、デバッグ時に非常にやっかいな現象を引き起こす。
Cでローカルワークに格納したものをリターン値にして、呼び出し側で受けようとしたら
内容が化けてた、というのと同じである。
(1)(2)の違いを正しく把握することは、Cocoa Touch上でプログラムを組む上で、きわめて重要である。
にもかかわらず、アップルのドキュメントにおいてもそれに関する記述が見つけられなかったし、
インターネットで調べても書いてあるサイトが1つも見つけられなかったという始末である。
ほっとして、誰も正しく理解してなかった?それとも理解している人は隠していたか。
Autorelease Pool(自動解放プール)とは、登録されたオブジェクトを、自動解放プールを解放したときに一括して解放する仕組みである。
自動開放プールの宣言からその開放までの間に登録されたオブジェクトに対し、自動的に release メッセージを送ってくれる。
NSAutoreleasePool
メソッド名 | 動作 |
-(void)drain | 全強制解放する(ガベージコレクション付き) |
-(void)release | 解放する(ガベージコレクションなし) |
+(void)addObject:(id)object
-(void)addObject:(id)object | オブジェクトを追加する |
-(id)autorelease | 解放要求+1 |
-(id)retain | 保存要求+1 |
// プール作成
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// たくさんのオブジェクトを登録
id *anObject = [[NSObject alloc] init];
[pool addObject:anObject];
~
//プール解放時に、登録したオブジェクトにもreleaseが送信される
[pool drain]; // プールの解放 / iOSではreleaseでも同じ
このように、自動解放プールに登録したオブジェクトは、自動解放プールを解放したときと同時に解放される。
iOSではガベージコレクションを行わないので、drainとreleaseは同じである
(Xcodeが自動的に作るmain.mでもreleaseを使っている)。
私は、iOSアプリを作り始めてしばらくは「クラスメソッドによる確保はローカルスタックに作られる」と理解していた。
そのように(古い)ここにも書いてた。
実際動作だけから見ると、その理解は「ほぼ」間違いではない。
が、もっと詳細に動作を追跡する必要が出て、それをやっている内に「Autorelease Pool領域内に確保されている」と
理解した次第である。
なぜか。Instrumentという解析ツールの中にメモリの確保状況を見る機能がある。
これで追跡していると、クラスを抜けた後にもかかわらず、クラスメソッドで確保したワークが解放されていないことが分かった。
ローカルスタックなら、リターン時に絶対に解放されるはずである(内容が破壊されるかどうかは別にして)。
試しに呼び出し前後でAutorelease poolを確保~解放したら,そのワークも解放された。
ここで「Autorelease pool内に確保される」と理解した次第である。
Autorelease poolは、iOSアプリ起動時にmain()内で自動的に確保される。
だから、ユーザープログラムの中でそれを宣言しないでもクラスインスタンスによる確保が使えるわけだが、
これはメインスレッド用なので、
ユーザーが別スレッドを動かす場合は、その中で明示的にAutorelease poolを確保~解放する必要がある。
これをしないと、メイン用のAutorelease PoolがあふれてiOSが異常動作を起こすこともある。
経験から言えば、アプリが落ちるのではなく、画面表示がされなくなることが多い。
非常にわかりにくいバグになるので要注意。
スレッド内でクラスインスタンスによる確保をしていないと思っても、一応AutoreleasePoolは確保しておいた方が良い。
なぜなら、アップル純正や、ちまたに流れているクラスライブラリの用意しているメソッドや関数の中には、
autoreleaseオブジェクトを返してくるものがあるからである。
確認している範囲では、
- becomeFirstResponder
- UIGraphicsGetImageFromCurrentImageContext()
- UTF8String
等がある。特にbecomeFirstResponderやUTF8Stringは見逃しがちである。
逆に、クラスメソッドでもシステム内のアドレスを返すだけのものは例外的に自動開放プールを使わない。
まあ、なんにせよ転ばぬ先の杖。
また、for等ループの中も同様で、ループを抜けないとAutorelease Poolが解放されないため、
ループ内でクラスインスタンスによる確保を行うとあふれてしまうことがある。
ここでも、ループ内で別途Autorelease Poolの宣言が必要となる。
NSMutableArray *array = [NSMutableArray array];
for ( int i = 0; i < 10000; i++ ) {
NSAutoreleasePool *p = [[NSAutoreleasePool alloc] init];
[array addObject:[NSNumber numberWithInt:i]];
[p drain];
}
for ( id n in array ) NSLog( @"%@", n );
for 内の[NSNumber numberWithInt:i] は、クラスメソッドによる確保であり、
自動開放プールなしでループを回すと、NSNumberインスタンスが一時的に 10000個作成されることになる。
このようなところでは局所的に自動開放プールを作成・解放しておく。
allocした結果をpropertyで宣言したインスタンスに代入する場合、たとえば
@interface MyClass : NSObject
{
NSString *string;
}
proterty (nonatomic,retain) NSString *string;
@end
の場合は、確保は同じだが、解放は
self.string=nil; // [string release]; string=nil;相当
でもいいらしい。プロパティーとして作用させる必要があるので、「self.」は外してはいけない。
でも、allocしたのにreleaseがないというのはやはり美しくない。
それはCでmallocしたのにfreeがないのと同様で
メモリの確保と解放は、見た目上も対で存在すべきなのだ。
世の中にはreleaseを書かないことを美しいように言っているホームページもあるようだが、
メモリの確保と解放を意識しないことがメモリ漏れバグの温床であるから、
むしろ「releaseは意識的に書く」方が美しく、理にかなっていると思う。
まあ、個人が「ちゃんと」理解して使っている分にはどちらでもいいのだが。
Cocoa Touchで提供されているクラスは、allocに対しては単純にreleaseを発行すれば解放される。
しかし、自分でクラスを設計するとき、その中で別の領域を確保するかもしれない。
このような場合は、単純にreleaseすると中の領域が開放されないまま残ってしまう。
このようなクラスの場合、中にdeallocメソッドを作って、独自の解放処理を記述しておく。
deallocはクラスをreleaseするときに内部的に呼び出されるメソッドである。
C++でいうところの「デストラクタ」に相当する、のだと思う。
@interface MyClass : NSObject
{
NSString *string;
}
@end
@implementation MyClass
-(NSString *)initString
{
string=[[NSString alloc]initWithString:@"初期化"];
}
-(void)dealloc
{
[string release];
}
@end
~
MyClass *my;
my=[[MyClass alloc]init];
~
[my release]; // ここでdeallocが呼び出される
なお、deallocをユーザーが呼び出してはいけない。
実行エラーとなる。
それはシステムがオブジェクトを解放するときに自動的に呼び出すものだからである。
ユーザーが呼び出すのはあくまでreleaseのみである。
Google Objective-Cスタイルガイド日本語訳
では「確保する変数はautoreleaseしてから代入せよ」と書いてあるけど、これは「すべての確保系をautoreleaseで行うときのみ」にのみ
使える方法であって、上記のようにallocを使っている場合にはしてはいけない。確実に落ちる。
私個人的な感想としては、Googleのスタイルガイドには従いづらい部分が多いので、基本的には無視している。
クラスメンバー名の付け方とか、TABとか。
オブジェクトは多くの場合、初期化を必要とする
C++ではコンストラクタと呼んでオブジェクト生成時に自動的に呼び出しがかかるが、
Objective-Cでは初期化をイニシャライザまたはコンビニエンス コンストラクタと言い、明示的に呼び出す必要がある。
イニシャライザ(initializer)/コンビニエンス コンストラクタ(convenience constructor)は
どちらも初期化を行うメソッドであり、よく似た機能を持つが、
前者がインスタンスメソッド用、後者はクラスメソッド用で、動作も微妙に違う。
実はクラスの「内容=メンバー変数」はインスタンス化されるときに0で初期化されることが決まっている。
Cはauto変数は内容不定だったのとは違う。これは、実はObjective-Cのクラスはスタック上に作られるauto変数ではなく、
メモリ上に直に確保されるグローバル変数だからである。Cもグルーバル変数は0で初期化された。
Objective-Cで特別に変更された仕様ではない。
(ただし、通常はイニシャライザで必要な初期値を与えておくべきである)。
しかし、インスタンスのアドレス自体はnil(=0)ではない(そのアドレスがselfである)。
プログラム上、時にインスタンスのアドレスをnilで初期化して呼び出すことを要求しているクラスがあるので注意が必要である。
initはNSObject(NSObject)が用意しているインスタンスの初期化用の標準的にイニシャライザー(メソッド)である。
多くのクラスはこれをオーバーライドして独自の初期化処理を追加している。
initは多くの場合、初期化された自身のオブジェクトのアドレス=selfを返すが、
オーバーライドの実装によっては他の値を返したり、nilでエラーを返す場合もある。
従って、そのリターン値を確認することは重要である。
@interface Point : NSObject
{
int x;
int y;
}
- (id)init; // 引数なしだから変数名のように見えるけど、関数
- (id)ObjFree; // 〃
- (int)getX; // 〃
- (int)getY; // 〃
@end
@implementation Point
- (id)init // initは引数を持たせることが出来ない
{
[super init]; // 親クラスのinitを呼び出しておく;子クラスを作る時は必須
self = [super init];
if (self != nil) {
x = y = 0;
printf("init method\n");
} // self==nilの時は何らかの原因で初期化に失敗している
return(self);
}
- (id)ObjFree
{
printf("ObjFree method\n");
return [super ObjFree]; // 親クラスのObjFreeを呼び出しておく;子クラスを作る時は必須
}
- (int)getX
{
return(x);
}
- (int)getY
{
return(y);
}
@end
int main()
{
id pt = [[Point alloc]init]; // 確保して初期化
// [Point new]; と書き換えることも出来る。new=alloc+init
printf("x=%d,y=%d\n", [pt getX] , [pt getY]); // なんか変数を並べているようで違和感あるなぁ
[pt ObjFree];
return(0);
}
initはオーバーライド時にも引数を持たせることが出来ない。無理に持たせようと定義してもコンパイラがエラーを出す。
これはnewというクラスメソッドが=alloc+initと定義されているので、その互換性を維持するためだろう。
そういうこともあって、Point.init()は引数を取れないので=0という固定値で初期化しているが、
引数付きとして任意の値で初期化する場合は別途そういうイニシャライザを定義する。
それを「指定イニシャライザ」といい、慣習的に名称はinitWith~とする。
initと同じく、子クラスのイニシャライザでは、superで親クラスの初期化も行う必要がある。
@interface Point : NSObject
{
~
}
- (id)initWithPoint:(int)x arg2:(int)y;
~
@implementation Point
- (id)init // initは引数を持たせることが出来ない
{
[super init]; // 親クラスのinitを呼び出しておく;子クラスを作る時は必須
return [self initWithPoint:0 arg2:0];
}
- (id)initWithPoint:(int)x arg2:(int)y
{
self->x = x;
self->y = y;
return(self);
}
int main()
{
id pt1 = [Point new]; // alloc+init=new
id pt2 = [[Point alloc] initWithPoint:400 arg2:300]; // こちらはnewにはできない
//
printf("pt1.x=%d,pt1.y=%d\n" , [pt1 getX] , [pt1 getY]);
printf("pt2.x=%d,pt2.y=%d\n" , [pt2 getX] , [pt2 getY]);
[pt1 ObjFree]
[pt2 ObjFree]
return(0);
実行結果(未確認)
pt1.x=0,pt1.y=0
pt2.x=400,pt2.y=300
ObjFree method
ObjFree method
「メモリ管理」も参照のこと。
オブジェクトの所有権
Objective-Cでは、前述の通り
クラスはローカルやグローバルな領域に実体を確保できないため、
必ずオブジェクトの生成と破棄を必要とする。
アクセスしたメソッドが、それを解放して良いか、解放するならそれはいつかは、わかりにくいこともある。
特にメソッドのリターン値になる場合などはそうである。
誰が解放するかということについて、一応の規約が存在する。
オブジェクトを作成する | alloc / allocWithZone: |
オブジェクトをコピーする | copy / copyWithZone: / mutableCopy / mutableCopyWithZone: |
オブジェクトを保持する | retain |
以上を自身で行った場合はreleaseでオブジェクトを解放する。
行ってない場合は「そのオブジェクトの所有権はない」として解放処理は行わない。
別のまとめ方をすると、以下の通りとなる。
ローカルでallocした | そのスコープ内でrelease |
クラスメソッド(+で始まるメソッド)で確保された | 基本的には何もしない(OSが解放する) または明示的にAutorelease Poolを確保~解放する |
プロパティでcopyまたはretain | deallocメソッド内でrelease |
allocとcopyは実メモリの確保を伴い、retainはアドレスが示す内容の解放を遅らせるだけ、と意味が異なる。
にもかかわらず、同じreleaseで解放指示を行うところが注意である。
allocとcopyは特にわかりにくいことはないと思うが、retainは少しわかりにくい概念かもしれない。
Objective-Cのクラスはアドレスでやりとりされる。
従って、代入したもしくはメッセージに引数として渡したと思って元のオブジェクトを
解放してしまうと、代入先が参照できなくなってしまう。
これを渡された側で「解放に待ったをかける」方法が「retain」である。
Objective-Cではマルチタスクを前提としているので、
呼び出されたメソッドと呼び出したメソッドは並行動作する可能性が高い。
そのためこのような指示が必要なのである。
呼び出し側でその後の処理でreleaseをかけても、そこでは解放せず、呼び出された側もreleaseをした
時に初めて実解放がかかるようにする。
しかしこれもあくまで解放の遅延だけであって、内容の書き換え禁止ではないことには注意すべきである。
元の内容を書き換えると、
代入先の内容まで書き換わってしまう。これが顕著になるのが配列オブジェクトを使う場合である。
Objective-CのクラスはCの構造体と見た目似ているが決定的に違う部分があって、
それは「実体を代入することが出来ない」である。
このことは配列クラスであるNS(Mutable)Arrayの要素にて特に問題になる。
NSArrayの要素はid型であるが、これはすなわち要素として実際に格納されるのはアドレスであることを示している。
addObject:メッセージで要素を追加しても、その要素のオブジェクトのアドレスが格納されるだけであって、
要素実体のコピーが格納されるわけではい。Cで構造体の配列を作った場合は、その実体がコピーされる。これが最大の違いである。
// 構造体の場合
typedef struct {
int x;
int y;
} XY;
XY xy[10]; // 構造体XYの実内容を持つワークが10個分用意される。
~
xy[0]=xy[1]; // xy[1]の内容全てがxy[0]の領域にコピーされる
// クラスの場合
@interface XY : NSObject
{
int x;
int y;
}
~
{
XY *xy0=[[XY alloc]init];
NSMutableArray *ary=[[NSMurableArray alloc]init];
//
[ary addObject:xy0]; // ここではxyのアドレスだけが格納され、xyの実体は格納されない
XY *xy1=[ary objectAtIndex:0];
(構造体とクラスのプログラムは同じ意味ではない。)
ここで、構造体の場合はxy[1]の内容をこの後いじってもxy[0]の内容は変化しない。
実内容がコピーされているからである。
ところが、クラスの場合、xy0の内容をいじるとxy1から読み出す内容も同じになる。
配列に格納されているのはあくまで実体アドレスであって、内容ではないからだ。
このことを理解しておかないと、「配列に要素を保存したはずなのに内容が変わってる!」
という現象に悩まされる。クラスではこれはバグではなく仕様なのである。
仮にretainを指定しても、それはあくまでアドレスの示す領域の解放を遅らせるだけであって、
内容の書き換えを禁止するわけではない(その方法はない)ので要注意である。
MacのCocoaではメモリの有効活用のためにワークを移動させてまとめたり
不要になった領域をメモリ上から消すガベージコレクション機能を持つらしい。
古くはBASICで文字列を扱うと発生していたのが「ガベージコレクション」だし、
X68のSX-Window上でもワークの移動はOSの大事な機能だったが、
それとほぼ同じである(こんな事書いても解る人の方が少ないだろうが)。
確かにそれがあれば解放を気にしなくて済むようになるからプログラムは楽に書けるようになるかもしれない。
が、本来メモリ管理は明確かつ厳密に行われるべきであって、
ガベージコレクションなどに頼るべきではないと思う。
アセンブラやCの時代からプログラムを組んでいると、ガベージコレクションどころか、
クラスインスタンス以外のAutorelease Poolだって甘やかしだと思うのだが。
「メモリ管理」も参照のこと。
id型よりクラス名 *の方が安全
クラスの型としてはid型が汎用的で使いやすいが、逆に言えば型(クラス)が特定出来ないので、
持っていないはずのメソッドを呼び出してしまったりする可能性がある。
それを動的に調べる方法もあるのだが、コンパイラレベルでもある程度チェックできる。
素直に元のクラス名のアドレス型で宣言すればよいだけである。
これはコンパイルレベルのチェックを強化させるだけなので、実行時には全く影響を与えない。
@interface A : NSObject
- (void) Write;
@end
@implementation A
- (void) Write
{
printf("Write\n");
}
@end
int main()
{
id obj1 = [A new]; // クラスAにはWriteというメソッドが存在するが、
id obj2 = [NSObject new]; // 親クラスであるNSObjectにはない(場合)
[obj1 Write];
[obj2 Write]; // 実行時にエラーとなる
[obj1 ObjFree]
[obj2 ObjFree]
return(0);
}
↓これを明示的なクラス名で記述する。
int main()
{
A * obja = [A new]; // idは元々ポインタを示す型なので、クラス名で明示する時は"*"が必須
[obja Write]; // もしクラスAがWriteメソッドを持たない場合は、コンパイル時にエラーになる
[obja ObjFree];
return(0);
}
この場合は呼び出す元となるインスタンスが固定されているので、メソッドの存在があらかじめ解るが、
Objective-Cでは逆に、メソッド名自体=メッセージを固定して呼び出し対象とするインスタンスを変更していく;
送るメッセージは同じだが、それを解釈するオブジェクトが違う、という記述をよく行う。
その場合はこの方法は使えない。
その「動的なメソッドの存在チェック方法」については後述。
メンバーの公開範囲
通常、クラスメンバー変数はそのメソッドによってのみアクセスされるべきである。
が、メソッドの呼び出しはCの関数呼び出しに比べ(出来ることが多い分)遅いので、
高速性を求められるプログラムではメンバー変数に直アクセスアクセスする必要があるかもしれない。
しかし、全メンバーを公開してしまうと、いじられたくない部分までいじられる可能性がある(特に派生クラスを作る時)ので、
公開範囲を特定すできる。
@public | 全公開(外部、このクラス内、子クラス内) |
@private | このクラス内のみ公開 |
@protected | このクラスおよび子クラス内のみ公開;デフォルト |
これを記述した後のメンバー変数は全てその公開範囲となる。クラス定義内で何回、どの順序で書いても良い。
クラスの属性値は@publicに、内部変数は@privateまたは@protectedにするといいのだろう。
C言語で言うなら、グローバル変数、ファイル内static変数、auto変数と考えるとわかりやすい。
Apple純正のObjective-C解説
によると
@publicの利用は避けるべき
と書かれている。
デバッグ時に内容読むのには使えると思うけどね(デバッグ中だけ@publicにしておく)。
ついでに書くと、クラスをidで受けてしまうとメンバー変数がコンパイル時には不定でエラーになるので、
「クラス名 *」で定義する必要がある。
@interface A : NSObject
{
@public
int a;
@protected
int b;
@private
int c;
}
- (id)initWithA:(int)a arg2:(int)b arg3:(int)c;
- (void)WriteA;
@end
@interface B : A
// 変数がないので{}は省略
- (void)WriteB;
@end
@implementation A
- (id)initWithA:(int)a arg2:(int)b arg3:(int)c
{
self->a = a; // @publicメンバーへの代入はポインタを経由して行う
self->b = b;
self->c = c;
return(self);
}
- (void)WriteA
{
printf("[A Write a=%d, b=%d, c=%d]\n", a , b , c);
}
@end
@implementation B
- (void)WriteB
{
printf("[B Write a=%d, b=%d]\n", a , b); // cにはアクセスできない
}
@end
int main()
{
B * objb = [[B new] initWithA:1000 arg2:100 arg3:10];
printf("[main() a=%d]\n" , objb->a); // @publicなのでメンバー変数に直接アクセスできる
objb->a=200; // 当然代入も出来る
[objb WriteB];
[objb WriteA];
[objb ObjFree];
return(0);
}
このあたりはCで書いてた構造体を持つライブラリを移植していくと理解が深まるのであろう。
Objective-C V2.0で導入された
プロパティを使うと
自動的にアクセスメソッドを生成させることが出来るので、この公開範囲をいじるというのは
「高速化を除き」今後余り行わない方が良いのかもしれない。
世間に公開するようなライブラリを作るならいざ知らず、個人の開発においてはほぼ無用であろう。
私は一度も使ったことがない。
Class型
クラス変数を「インスタンス化した」時、そのオブジェクトの中には
「自分自身のクラス定義内容」も入っている。これを「クラスオブジェクト」という
(実はこれこそがNSObjectクラスの主な機能である)。
これはClass型で定義され、変数にも代入できる。
代入するメソッドはclassである。
万が一クラスオブジェクトが存在しない時は「Nil」という値が返ってくる。ここではNULLは使わんようだ。
定義内容そのものを動的に取得可能というわけだ。しかも、その定義内容を元にインスタンスを作成することも出来る。
型(定義)そのものを変数に代入し、その変数に代入された型で変数を宣言できるということだ。
うぉっ、ややこしい。でもなんかうまく使えば強力そう。
@interface Test : NSObject
+ (void)Write; // クラスメソッド
- (id)init; // インスタンスメソッド
@end
@inprementation Test
省略
@end
int main()
{
Class testClass = [Test class]; // Class型変数testClassを宣言し、Testというクラスのクラス情報を代入する
// testClassはクラス宣言に相当する
[testClass Write]; // testClass->Write();相当 Writeがクラスメソッド(+)だから呼び出せる
[[testClass new] ObjFree]; // testClassの保持するクラス型のオブジェクトを作成→即解放している
[testClass ObjFree]; // クラスオブジェクトを解放
return(0);
}
セレクタ
Objective-Cはオブジェクトが動的に確保されるため、メソッドの実行アドレスもコンパイル時には決定されない。
だからといって、毎回名称を検索して実アドレスを決定してたのでは遅いので、中間コードを割り付けているらしい。
古のBASICの中間コードのようだ。Objective-CがC+インタープリターと言われるのは、この動作から見ても間違いではない。
ゆえに、中間コードから実アドレスに変換するアドレスをどこかに持っている(たぶんClass型の情報の中)。
この中間コードのことを「セレクタ」と言い、SEL型で取得できる。
中間コードまで取得できるとは、Objective-Cはマニアックである
・・・ではなくて、ちゃんと使い道がある。
このセレクタを取得するのが@selector(メソッド名)である。
記述は関数のように行うが、@で始まっているので実際にはコンパイラディレクティブである。
(メソッド名)と書いているが、メソッド名は同じで引数が違う場合もあるので、その場合は(メソッド:ラベル:)まで書く。
perform(NSObject)またはperformSelector(NSObject)というメソッドを使うと、指定したセレクタを実行できる。
第1引数はSEL型で固定、それ以降引数を持つ場合は場合によって変わる。
-(void)performSelector:(SEL)aSel;
-(void)performSelector:(SEL)aSel withObject:(id)anObject; // 1引数
-(void)performSelector:(SEL)aSel withObject:(id)anargment1 withObject:(id)anargment2 // 2引数
例によってwithはラベル。
@interface Test : NSObject
- (void)Write;
@end
@implementation Test
- (void)Write
{
printf("Write\n");
}
@end
int main()
{
id obj;
SEL method;
obj = [Test new];
method = @selector(Write); // obj.Write()の中間コードを得る
[obj performSelector:method]; // obj.Write()を実行する
return(0);
}
ここで1つ疑問が浮かぶ。@selector()の引数はメソッド名であるが、複数のクラスが同じメソッド名を持っている場合はどうなるのか。
これを理解するには、もう一度Objective-Cの基本思想を思い出す必要がある。
Objective-Cではメソッド名とはメッセージそのものであり、そのメッセージを受信できるオブジェクトは1つとは限らない。
言い換えれば、同じメソッドを持つクラスは同じメッセージを受信可能と言える(そのようにプログラムを設計する)。
ならば、メソッド名毎に受信可能なオブジェクトをまとめて管理して置いた方が都合が良い。
受信オブジェクトの決定の流れは
実行時のメソッド
実行アドレス解決法を理解しなければならないが、詳細は省略するとして、
メソッド名はハッシュ表のような物で管理されており、同一名称は同じエントリにまとめられ、
そこから実際のオブジェクトに相当する物を呼び出すようになっているのである。
@selector()が返すのはメッセージ別受信可能クラステーブルを示すアドレスである。だから、同じ名前ならそれはそれで良いとなる。
「selector」とは「選択する物」であるが、クラスの持つメソッドから1つを選択するコードと理解すれば、
その名前も納得いくだろう。
インターネットでこのあたりを正確に解説したものはApple純正サイトも含め「なかった」。
AppleのObjectibe-Cの解説書をよく読むと、ちらっとだけ書いてあったが、
@selector()の返す値に疑問を持った者は他にいないのだろうか。
メソッドのポインタ
クラスのメソッドは動的に決定されるとはいえ、もちろん実呼び出し時にはどこかのアドレスに固定(配置)され呼び出される。
Objective-CはCの純粋な拡張なので、メソッドの実行アドレスを得ることが可能になっている。
メソッドの実行アドレスを示す型はIMP型で、メソッドは引数を含め
(*IMP)(id,SEL,...)
と定義される。引数として必ずidとSELを持っている。
idにはインスタンスのアドレス=self、SELにはこのメソッドのセレクタ(変数_cmdの値)が入る。
Cで記述したプログラム部分からメソッドを関数として呼び出したい場合、
もしくはObjective-Cでも高速化をしたい場合(メッセージを送るのは重い処理である)は、
そのアドレスを取得して直呼び出しすればよい。
+(IMP)instanceMethodFor:(SEL)aSel; | クラスメソッドの実行アドレスを得る |
-(IMP)methodFor:(SEL)aSel; | インスタンスメソッドの実行アドレスを得る |
共に引数はメソッドのセレクタである。
@interface Test : NSObject
- (void)Write;
@end
@implementation Test
- (void)Write
{
printf("~\n");
}
@end
int main()
{
id obj;
SEL method;
IMP func;
obj = [Test new];
method = @selector(Write);
func = [Test instanceMethodForSelector:method]; // メソッドの実行アドレスを得る
func(obj , method); // メソッドを直接関数として呼び出している
[obj release];
return(0);
}
補足)performSelectorはNSObjectが規定しているプロトコルである。
従って、その実装はそれを継承した各クラスに任されている。
NSObject自体もそのメソッドを持つが、プロトコルとは若干異なる(拡張された)仕様を持つ。
カテゴリ
カテゴリは分割コンパイルならぬ、分割ソース記述の方法である。
1つのクラスを記述するのに、1つのファイルだけでなく、複数のファイルに書いた分を実行時に全部まとめてくれる。
これの良さは、大本のクラスの記述されたソースを書き換えずに機能追加が出来ることにある。
#importする必要もない。
カテゴリは、クラス名の後ろに()内に名称を付けて指定する。
@interface クラス名 (カテゴリ名)
@implementation クラス名 (カテゴリ名)
命名規則はCの変数名と同じ。
なお、カテゴリで追加できるのは(+クラス、-インスタンス)メソッドのみであって、変数は追加できない。
必要なら、大本のクラスに対して追加する必要がある。これがカテゴリの唯一の欠点であると思う。
@interface Test : NSObject
- (void)WriteA;
@end
@interface Test (add1)
- (void)WriteB;
@end
//
@implementation Test
- (void)WriteA
{
printf("~A\n");
}
@end
//
@implementation Test (add1)
- (void)WriteB
{
printf("~B\n");
}
@end
//
int main()
{
id obj = [Test new];
[obj WriteA];
[obj WriteB];
[obj ObjFree];
return(0);
}
例外なのが()の中に何も書かない無名カテゴリで、これだけは変数の追加が可能になっている。
最近の記法では、.h内の@interfaceには公開される@propertyのみを、
.mの先頭にも@interface 無名のカテゴリ(プライベートカテゴリとか言うらしい)を書いて、そこに内部だけで使う@privateのメンバーを書くのが
ヘッダーと本体の独立性を向上させるために「お勧め」らしい。
じゃあ、どうしても変数を追加したいときは、無名カテゴリで本体そのものを拡張したように見せかければいいんじゃないかと思ってテストしてみたが、
どうやら無名のカテゴリは1つしかダメみたいで、他のファイル中に()で無名を書くと、確かにコンパイルは通るがリンカーがエラーを出す。
@propertyで記述するとリンカーもエラーを出さなくなるが、実行時にエラーが出る。
方法はないかと思って探していると、@propertyでいいなら、比較的楽な方法が見つかった。それは後述。
.h内
@interface NewView : UIView // 継承はこちらに書く
// プロトコルもこちらに書く
// ここに書くのは@public(相当)のみ
@property (nonatomic,strong) IBOutlet UIView*view; // _viewは@privateなので注意。@publicにしたいなら、メンバーにUIView *_view;と@synthesize view=_view;が必要
@end
.m先頭
#import "NewView.h"
@interface NewView()
{
@private
〜
}
@end
カテゴリで追加した部分は、通常"元クラス名+カテゴリ名.h/.m"とする(命名規則)。
たとえば、上記の例なら"NSObject+Test.h"と"NSObject+Test.m"となる。
あるクラスにカテゴリ拡張をし、さらにその拡張があることを前提として別のカテゴリを追加するときはどうすれば良いのだろうか。
答えは、「大本に対するカテゴリとして記述すれば良い」だけである。
カテゴリによる拡張は、拡張部分全体があたかもオリジナルに最初からあるように振る舞うので、
他の拡張の存在を前提としていても、大本に対する拡張として記述すればちゃんと動く。
ただし、別のカテゴリで拡張されたメソッドを「コンパイル時の警告なし」に使うには、
その拡張分のヘッダーを読み込んでおく必要がある。
@interface Test (add2)
- (void)WriteC;
@end
//
@implementation Test (add2)
- (void)WriteC
{
WriteB();
printf("+C\n");
}
@end
//
int main()
{
id obj = [Test new];
[obj WriteA];
[obj WriteB];
[obj WriteC];
[obj ObjFree];
return(0);
}
カテゴリは大規模なクラスの記述を複数のファイルに分割するだけのように見えるが、
実行時の動作は「各カテゴリが動的に結合される」。
すなわち、各カテゴリは別々にコンパイルされたのち、実行時に結合される。
カテゴリのクラス継承との違いは、継承は新しいクラス名を作らなければならないが、
カテゴリは元のクラスの機能そのものを拡張できることにある。
それは、メーカー提供のクラスに対しても有効である。実行時結合ならではの拡張され方である。
たとえば、NSStringに数値の進数指定による数値文字列変換機能を追加するなら
以下のようになる(大枠のみ)。
// NSStringの新たなカテゴリの宣言
@interface NSString (MakeBase)
+ (NSString*)makeBase:(int)code Base:(int)base
@end
@implementation NSString (MakeBase)
+ (NSString*)makeBase:(int)code Base:(int)base
{
return (~);
}
@end
これでNSStringにmakeBaseのクラスメソッドが追加された。
この拡張メソッドは、すべてのNSStringクラスのインスタンスに対して有効となる。
自分で作成したインスタンスだけでなく、システム側(他人が作った部分)で作られたものも含めて、である。
もっとも、自分以外はメソッドが追加されたことを知らないわけだから、
他から呼ばれることはないわけだが、今後のプログラムはその機能があることを前提に組める。
iOS自体もこれを使って拡張されてきたそうである。
がしかし、カテゴリによって、NSStringのようなメーカー提供のクラス(ファクトリクラス)に
機能を追加するには限界がある。それは「クラスメンバー変数が判らない」からである。
カテゴリではメンバー変数は追加できない。
ファクトリクラスではメソッドは公開されてもメンバー変数については公開されていない(ヘッダーを見ても書いてない)。
クラスメソッドの追加は簡単と言うか理解しやすいが、インスタンスメソッドを追加する場合は、どうするかというと、
selfで自身を参照する。
例えば、NSStringに 文字列先頭から連続する半角スペースを取り除くcutHeadSpacesというメソッドを追加する場合、以下のようになる。
なにげに、自身がnilの場合の判定が重要だったりする。
-(NSString *)cutHeadSpaces
// 文字列先頭から連続する半角スペースを取り除く
{
NSString *s=self;
if (s!=nil) {
NSInteger spLength=0; // スペースの数
for (NSInteger i=0;i<[s length];i++) {
if ([s characterAtIndex:i]==' ') {
spLength++;
} else {
// スペース以外が出た
if (spLength>0) {
// 先頭に連続するスペースが有った時
// それを除いた後ろを返す
return [s substringFromIndex:spLength];
}
break;
}
}
if (spLength>0) {
// 全部スペースだった
return @"";
}
} // nilの時は処理してはいけない
return s;
}
Cにおいては、機能的固まりは必ず関数という形で分離したが、
Objective-Cでは関数が良いのか、独自クラスを作るのか、カテゴリで既存クラスを拡張した方が良いのかは
それが使われる場所も考慮して決める必要がある。
このあたりは、プログラムを作りながら決定していくしかない。
補足:カテゴリ内で@propertyを宣言して使えるようにする方法
以下の方法で、カテゴリ内で@propertyを宣言して使えるようになる。
ほぼテンプレート的に書く。
カテゴリの.h内
@interface View2 (extention)
@property (nonatomic,strong) NSString *name; // これを追加したいとき
@end
カテゴリの.m内
#import "View+extention.h"
#import <objc/runtime.h>
@implementation View (extention)
static char _selfAddress_; // 一意に決まって変更されないアドレスを定義(ここのアドレスを決定するため)
@dynamic name; // アクセサは動的に実装する宣言;@synthesize(=自動生成)の代わり
// setter
- (void)setName:(NSString *)name
{
// intとかの場合は、NSNumberでオブジェクト化して受け渡しする
objc_setAssociatedObject(
self, // インスタンス(=self)にプロパティを持たせる
&_selfAddress_, // 保持するオブジェクトのアドレス
name, // 引数
OBJC_ASSOCIATION_RETAIN_NONATOMIC // オブジェクトはretain指定
);
/* 4つ目の引数の属性は以下の中から選ぶ
OBJC_ASSOCIATION_ASSIGN
OBJC_ASSOCIATION_RETAIN_NONATOMIC
OBJC_ASSOCIATION_COPY_NONATOMIC
OBJC_ASSOCIATION_RETAIN
OBJC_ASSOCIATION_COPY
*/
}
// プロパティのgetter
- (NSString *)name
{
return objc_getAssociatedObject(
self, // インスタンス(=self)が保持するデータを取り出す
&_selfAddress_ // 保持されたオブジェクトのアドレス
);
}
これだけでよい。ただし、プロパティ式でしかアクセス出来ないので、
KVOとかの設定はしないこと。
プロトコル
プロトコルは、簡単に書けば「クラスに対し、特定名称のメソッドの実装を保証させる」機構である。
プロトコルも「継承する」という。
@protocol プロトコル名 <親プロトコル1 , ...>
実装するメソッドの宣言;
@end
@interface クラス : 親クラス <プロトコル1名 , ...>
{
}
とすると、プロトコルを継承するクラスでは、それが要求するメソッドを@implementationで実装する必要が出てくる。
これをこのクラスは「プロトコルに準拠している」という。
逆に、@interface部分ではプロトコルで宣言されているメソッド名は省略する。
プロトコルで実装要求されるのはメソッドのみなので、
メンバー変数の追加は出来ない。
その辺り、先のカテゴリに近い物がある。
@protocol ClassNameToString // これはプロトコル名
- (id) ToString; // このプロトコルを継承するクラスでは、このメソッドを実装しなければならない
@end
@interface A : NSObject <ClassNameToString>
{
char *name;
}
- (id) init;
- (id) ObjFree;
// プロトコルで宣言されているメソッドの宣言は省略する
@end
@interface B : NSObject <ClassNameToString>
@end
@implementation A
- (id) init
{
[super init];
name = (char *)malloc(255);
sprintf(name , "%s . A@%d" , __FILE__ , self); // __FILE__はプリプロセッサが展開する定数(Cの機能)
return(self);
}
- (id) ObjFree
{
free(name); // これはCの標準関数
return [super ObjFree];
}
- (id) ToString // 実装必須
{
return (id) name;
}
@end
@implementation B
- (id) ToString // 実装必須
{
return (id)"This is NSObject of B Class"; // 文字列のアドレスをid型に変換して返している(文字列を返しているのではないぞ)
}
@end
int main()
{
id objA = [A new];
id objB = [B new];
printf("objA = %s\n" , [objA ToString]);
printf("objB = %s\n" , [objB ToString]);
[objA ObjFree];
[objB ObjFree];
return (0);
}
プロトコルはメソッドだけでなく、クラス変数に対しても指定でき、この場合、そのクラス変数がそのメソッドを
実装していることを保証させる。とはいえ、確定しているクラスに対してメソッドの実装保証しても仕方ないので、
汎用型であるid型に代入されるクラスに対して保証することにしか実質使えないだろう。
@protocol InstanceListener // これはプロトコル名
// このプロトコルを継承するクラスでは、このメソッドを実装しなければならない
- (void) InstanceFree:(id)object; // 引数はid型のobjectという名前の変数(クラス)
@end
@interface Test : NSObject
{
id <InstanceListener> listener; // このメンバー変数(クラス)はプロトコルInstanceListenerの持つメソッドを実装保証されている
}
- (id) init;
- (id) ObjFree;
- (void)SetInstanceListener:(id <InstanceListener>); // 同上
- (id <InstanceListener>)GetInstanceListener; // 同上
@end
@implementation Test
- (id) init
{
[super init];
listener = NULL;
return self;
}
- (id) ObjFree
{
if (listener) {
[listener InstanceFree:self];
}
return [super ObjFree];
}
- (void)SetInstanceListener:(id <InstanceListener>)
{
listener = l;
}
- (id <InstanceListener>)GetInstanceListener
{
return listener;
}
@end
@interface WriteInstanceFree : NSObject <InstanceListener>
@end
@implementation WriteInstanceFree
- (void) InstanceFree:(id)object
{
printf("%X:インスタンスが解放されました\n" , object);
}
@end
int main()
{
id obj1 = [Test new];
id obj2 = [Test new];
id <InstanceListener> listener = [WriteInstanceFree new];
[obj1 SetInstanceListener:listener];
[obj2 SetInstanceListener:listener];
[obj1 ObjFree];
[obj2 ObjFree];
return(0);
}
あるクラスがそのプロトコルに準拠しているかどうかは、
-(BOOL)conformsToProtocol:@protocol(プロトコル名)
で調べることが出来る。これはNSObjectの持つメソッドなので、全てのクラスにおいて利用可能である。
@protocol()はプロトコル名の識別子を得るためのコンパイルディレクティブである。
クラスの継承と異なり、こちらはソースコードレベル=コンパイラレベルのチェックなので意味は全く異なる。
にもかかわらずプロトコルも「継承」と呼ぶからややこしい。
プロトコルは「継承」と呼ばず「実装保証」にすればいいのに。
今更ながらのことではあるが、Objective-Cのクラス継承は単一のみ、プロトコルは複数継承できる。
プロトコルを継承したとき、それを「実装必須」にするのか「なくても構わない」にするのかを指定することもできる。
@protocol ClassNameToString
@required
- (id) message1; // このプロトコルを継承するクラスでは、このメソッドを実装しなければならない
@optional
- (id) message2; // このプロトコルを継承するクラスでは、このメソッドを実装してもよい
@end
「@required」は実装必須なプロトコルなことを宣言する。
これ以下(@optionalまたはプロトコル宣言最後まで)に書かれたメソッドは全て実装しなければならない。
一方「@optional」は実装してもよいプロトコルなことを宣言する。
@requiredはデータ供給部など、@optionalは実装すれば表示が変えられる等の目的で
指定されていることが多いようである。
全くの余談であるが、requiredされたメソッドは、ソース内に存在さえしていればコンパイラは通る。
自分でプロトコルを作る場合、まれにrequiredで宣言したけど、内部からその呼出をしてない場合があったりする。
本来それは実装ミスだが、ソース上そのメソッドが存在しているとコンパイラはエラーを出さないので気が付きにくい。
要注意・・・かな?
--- 以下未検証 ---
実は、プロトコルの「真の力」は実装保証ではなく、名前の由来にもなっている
「通信規約」にある。Cocoaには異なるスレッドまたはプロセス(CPU)間でメッセージ(=メソッドの実行要求)を通信する
手段が用意されている。
この機能のためにプロキシとコネクションと呼ばれる機能が用意されている。
メッセージを送る際は、相手のオブジェクトの「代理人」となるプロキシを作り、
メッセージをエンコードして、コネクションを通して、相手に届ける。
プロセス間通信は、完了するまでの時間が特定出来ない。一般的には、時間がかかる処理を別スレッドなどに任せる
ので余計である。パフォーマンス向上のためには、可能な限り無駄な通信は控えたい。
だが、通信時のメッセージのエンコード方法を知っているのは、通信先の相手である。
相手先にメッセージの型を問い合わせていたのでは、通信が二度手間になってしまう。
そこで、メッセージのエンコードに関する情報をあらかじめ与えるために、
通信規約としてプロトコルが使われるのである。
実際に使用するときは、プロキシに対して、setProtocolForProxy:というメソッドを使い、
プロトコルを当てはめる。
こうすることで、プロキシはメッセージのエンコード方法が分かることになり、無駄なプロセス間通信を減らすことができる。
NSDistantObject
メソッド名 | 動作 |
+proxyWithLocal: connection: | オブジェクトとコネクションでローカルプロキシを返す |
-initWithLocal: connection: | オブジェクトとコネクションでローカルプロキシを初期化して返す |
+proxyWithTarget: connection: | リモートオブジェクトとコネクションでリモートプロキシを返す |
-initWithTarget: connection: | リモートオブジェクトとコネクションでリモートプロキシを初期化して返す |
-connectionForProxy | コネクションを返す |
-setProtocolForProxy: | プロキシーのプロトコルをセットする |
-(void) setProtocolForProxy:(Protocol *)aProtocol
<例>
[(NSDistantObject *)DOcontrol setProtocolForProxy: @protocol (ServerProtocol)];
デリゲート
「デリゲート」=「delegate」とは「代行者」という意味で、
Objective-Cでは「本来メッセージを受け取るべきインスタンスから、別のインスタンスに処理を代行してもらう」
という意味で使われる。
ある特定の処理をまとめる(サブルーチン的に処理をまとめる)という役目の他、
Apple提供のクラスにおいては、「一部処理をユーザに用意させる」という役目も果たす。
多くの処理はメーカー提供のそれで実行可能だが、一部の動作や表示をカスタマイズしたいときや、
メーカーでは作っておけない部分(データの供給部など)の実装などがそれにあたる。
UIKitを使ってプログラムを構築する場合に限っては、後者しかないと思って間違いない。
実はこのデリゲートはObjective-Cが言語機能として持っているわけではなく、
クラスの実装仕方によって実現されている。
まずは「サブルーチン的な使い方」から。
特にユーザーインターフェースにおいては画面の複数の場所にボタンとかスライドバーとかが
存在することがある。
本来なら、メッセージはクラスを継承した個々のインスタンスで処理すべきであるが、
これらの処理はその内容が似ていることが多い。
であれば、1つのインスタンスにて処理を集約した方が良い。
まず代行してもらう側(デリゲートしてもらう側)の記述はこう。
@interface MyObject : NSObject
{
id delegate;
}
@property (assign) id delegate; // これで.delegateでアクセスできる
@end
@imprement MyObject
@synthesize delegate;
- (id)delegateMethod:(id)anObject
{
if ( [delegate respondsToSelector:@selector(delegateMethod:)] ) {
// 指定されたインスタンスがデリゲート処理メソッド(delegateMethod:)を持っている場合
return [delegate delegateMethod:anObject]; // そこに処理を投げる
}
// デフォルトの動作
}
@end
MyObject* obj;
obj.delegate=代行してもらう先のインスタンス;
ここで注意すべきは、delegateの属性を「assign」にすることである。
デリゲートを保持してしまうと、ほとんどの場合循環参照が起こるので、 retainを使ってはいけない。
デリゲートさせる実体が-(id)delegateMethod:(id)anObjectである。
ここではまず、delegate先インスタンスが、delegateMethod:メッセージに応答可能かどうかを判定するため、
respondsToSelectorメソッドで調べている。
引数は
@selectorで得られるdelegate先インスタンスのdelegateMethod:のSEL値である。
デリゲート先インスタンスがdelegateMethodメソッドを実装している場合は、そのメソッドを呼び出して実行させる。
実装していなかった場合は、自身で代わりの動作を行うようにコードを書く。
デリゲートを必須にするなら、エラーを発生させるのも一手だろう。
受信側も基本的に同じである。他にデリゲートする必要がなければ以下のように記述するだけでよい。
- (id)delegateMethod:(id)anObject
{
~
}
上記例ではデリゲートしているメッセージは1つだけ(delegateMethod:)だけだが、
UIKitで提供されるユーザーインターフェース群クラスは複数のメッセージを
デリゲートする。
このような場合はデリゲートするべきメッセージを
プロトコルで
定義するのが一般的である。
複数のデリゲートを1箇所で行うことも出来るため、継承するプロトコルは1つでなくても良い。
プロトコルの定義はそれぞれ異なるので、受信側に実装すべきメソッドの内容も異なってくる。
UIKitのデリゲートは多くが非形式プロトコルなので、全ての実装が必須ではないが、
「必須」になっている物もあるので、それは確実に実装しておく。
ターゲット-アクション、アウトレット
「ターゲット-アクション」は、基本的には、ボタンなどの操作に対応して処理を呼び出すことである。
操作がアクションであり、処理がアクションである
(だから、発生順で言えば「アクション&ターゲット」が正しい)。
1つの操作に対して1つの処理を書けば良さそうに思うが、それでは同じ処理を何度も書かなければならないことがある。
処理実体をサブルーチン化する方法もあるが、通常は、アクション側に識別子を用意し、
1つのターゲットで集中して受け付け、識別子で実処理を分ける。
アクション側を「ビュー」と呼び、ターゲット側を「コントロール」と呼ぶ。
また、アクションには、それを制御するクラスインスタンスが接続される場合がある。
これを「アウトレット」という。
ターゲットとアクションの接続については、インターフェースビルダー(以下IB)を使う場合は、線を引っ張るだけで完了する。
ターゲットとなるメソッドやアウトレットとするクラスインスタンスには、
ソース上であらかじめIBで認識出来るようヘッダー内で宣言だけ(は)しておく必要がある
逆に、その修飾子が付いたインスタンスのみIB上で認識される。
名称 | 識別子 | IBに認識させるもの |
アクション | IBAction | メソッド |
アウトレット | IBOutlet | インスタンス変数 |
@interface AppController :NSObject
{
IBOutlet id textField;
}
- (IBAction)sayHello:(id)sender;
修飾子の実定義は
#define IBAction void
#define IBOutlet
なので、コンパイラには何の影響も与えない。
従って、とりあえず宣言しておいて、完成したときに接続するなんてことしても何の問題もない。
ターゲットとアクション、およびアウトレットとの接続は、IBが作るnibファイルを読み込むたびに確立される。
通常それはプログラム起動のデリゲート処理内でinitWithNibName:で実行される。
nibファイルを使うことで、ユーザーインターフェースのレイアウトや、
各接続をプログラムで記述する必要がなくなる。
これはプログラム内から固定値となるデータ=リソースを出来るだけ排除することに役立つ。
初代のMacOSでメモリ節約のために作り出されたリソ-ス分離の考え方が、今においても徹底され、
また活用されているのである。
IBを使わずともターゲット-アクションは記述出来る。
IB上では、アクションからターゲットへ線を引っ張るだけで接続できるようになっているが、
内部的には
[target performSelector:action withObject:]を使ってターゲット側のメソッドを呼び出している。
(id)target | 処理を含むクラスインスタンス |
(SEL)action | @selector(メソッド名) |
これはNSObjectで定義されているので、全てのクラスがターゲットとなることが出来る。
クラスによっては、別のメソッドで設定する場合もある。
前に書いたとおりメソッド名から実行アドレスの決定は実行時に行われる。
存在しないメソッドを指定しておくことすら出来る。
実行時に実在するかどうかは
respondToSelector:で調べるので問題ない。
存在する場合のみ
performSelector: withObject:で呼び出すし、
アクション側がメニューの場合には、存在しない部分は自動的に選択不可になる機能まである。
ターゲットには引数としてアクション側の
selfが与えられる。
この部分は、プログラムで記述すると以下のようになる(実際にそう言うコードが出力されるわけではない)。
// ターゲットのための変数
id target;
// アクションのための変数
SEL action;
action=@selector(メソッド名);
...
// ターゲットがアクションを実装しているかどうか確認
if ([target respondsToSelector:actioin]) {
// アクションを呼び出す
[target performSelector:action withObject:self];
}
これは、見た目上、コンパイルせずにアクション側の追加が出来るということを意味している。
もちろんターゲット側に実装がなければ動かないが、見た目だけでも先に作り上げられることは、
例えばプログラムのプレゼンをするときなどには有用である。
アクション側クラスは、IBではとりあえず一意に決定するが、書き換えてもかまわない。
そのためのには
NSInvocationクラスを使う。
メソッド名 | 動作 |
-(void)setTarget:(id)anObject | 起動オブジェクトにターゲットを設定する |
-(void)setSelector:(SEL)selector | 起動オブジェクトにセレクタを設定する |
setSelectorは通常書き換えない(同じメソッド名のものに受信させる=送信するメッセージは同じ)が、
setTargetは変更する。
これにより、例えば同じcopy:というメッセージに対して、
アクションがテキストに対してならテキストのコピー処理を書いたターゲットを選び、
画像ならそれ用の処理を書いたターゲットを選ぶことで実処理を変更できるわけである。
Cで書くならswicth caseで処理を分けるような部分を「綺麗に書ける」ようである。
なんか、実際のコピーコマンドでは、さらに複雑な状況に対応するために、
直接setTarget:するのではなく、
レスポンダ・チェインという手法を使うらしいが、それはまた必要になったら見るということで。
KVO : Key-Value observe
KVOとはKey-Value observeの略で、簡単に言えば、プロパティーの内容が書き換えられたときに
登録されたメソッドを呼び出す機能である。
まずは宣言仕方。
@interface tConsole : NSObject
CGFloat fontWx_;
CGFloat fontWy_;
}
@property (nonatomic) CGFloat fontWx;
@property (nonatomic) CGFloat fontWy;
#define OBSERVE_FONTWX @"fontWx"
#define OBSERVE_FONTWY @"fontWy"
@end
@imprementation tConsole
@synthesize fontWx=fontWx_;
@synthesize fontWy=fontWy_;
~
@end
クラスを作るとき、対象としたいメンバーをプロパティにできるよう宣言しておく。
次の#defineは通知名の定義である。そこで定義する文字列はKVO対象としたいプロパティ名と一致させておかなければならない。
(なぜ文字列で指定するかというと、「Objective-Cは内部でプロパティー名を文字列としての保持しており、
それを使って対象かどうか比較するから」なのであるが、そういう細かいことはどうでもよい。)
別に#defineしなくても良いが、私のプログラミング作法ではプログラム中での「即値記述は基本的に御法度」なのと、
「このプロパティーはKVOで書き換えられる可能性がある」ということも意味させている。
KVOの真価は、「メンバーの値が変更されたタイミングで何か処理を追加したいとき、
クラスの実装をいじらずにそれを追加出来ることにある。
クラスの拡張をカテゴリを使って、元のソースを全くいじらず追加も可能になるし、
外部のクラスで、その値の変更と同期して何かを行うことも簡単になる。
tConsole *tconsoleとして、
~
[tconsole addObserver:self forKeyPath:OBSERVE_FONTWY options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
[tconsole addObserver:self forKeyPath:OBSERVE_FONTWX options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
これがKVOの受信宣言。tconsoleに対し、forKeyPath:で指定したメンバーが変更されたとき、addObserver:で指定されたクラス、
ここではself=自分自身に対して通知せよと宣言している。optionsは、通知タイミングの指定。
詳細はAppleのドキュメントに譲るが、「初期値代入時は除く」とかが指定出来る。
最後に、その通知を実際に受けるデリゲートを実装する。
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
// KVOによる変更通知受信
{
//NSLog(@"observeValueForKeyPath:%@ from=%@",keyPath,object);
tConsole *tc=(tConsole *)object;
if ([keyPath isEqual:OBSERVE_FONTWY]) {
NSLog(@"KVO.fontWy");
kconsole.fontWy=tc.fontWy;
}
if ([keyPath isEqual:OBSERVE_FONTWX]) {
NSLog(@"KVO.fontWx");
kconsole.fontWx=tc.fontWx;
}
}
1つのクラスだけでなく、複数のクラスに対してKVO通知指定をしている場合も、受信は全てこの1つにまとめられる。
通知は全てこの1つのデリゲートで受けるので、複数の通知を受けるように設定しているときは引数を調べて処理を分ける必要がある。
ここでは、変更通知があったとき、それをkconsoleという別のクラスのメンバーにも代入している。
ここまで準備をしておいたら、あとは指定したクラスのメンバーをプロパティー式で書き換えれば、その書き換え直後にKVO通知が発生する。
tconsole.fontWx=tconsole.fontWx;
tconsole.fontWy=tconsole.fontWy;
ここで1つ注意しなければならないのは、通知は、変更した処理の動いているスレッド内で来るということである。
書き換えがサブスレッド上で行われれば、通知もサブスレッド内で行われる。
UIKitの中には、メインスレッド内での呼び出ししか認めていないクラスもあるので、
「値が更新されたら表示も更新する」ためにKVOをしようとして、「できない!」と言うことになったりするので
要注意である。まあ、これはKVOに限ったことではないが。
このように、KVOはうまく使えばかなり強力なクラス間通信機能にもなり得る。
X-BASIC'(X-BASIC for iOS)ではタスク間の同期にも使ってたりする。
より詳しくは、
KVOプログラミングガイドも参照のこと。
プロパティ
プロパティといえば、C++(というかVisualBasic?)では、オブジェクトの属性のことであり、
それはすなわちメンバー変数そのものであった。
Objective-C V2.0で追加されたこれは、「見た目の式」をそれらと同じにして簡潔に記述する方法である。
実際には、Cの構造体メンバーへのアクセスとは全く異なり、自動的に作られたメソッドを使ってアクセスするコードがコンパイルされ、
それを隠蔽する手段だと言える。
UIKitではプロパティの使用が前提になっている。従って、その習熟はiOSアプリ開発において必須と言える。
プロパティにはメンバー変数の保持やコピーの属性も設定できる。
後述のKVOという仕組みも利用出来るようになる。
これは、一見代入式に見えるプロパティが実際にはメソッドの生成であり、
メンバーの属性によって、生成させるべき式も変わるからである
(読み込み専用変数に書き込みよう処理は不要とか)。
いつまでだったかは定かではないが、古いXcodeでは
実装は、@interface部と@implementの両方に行っていた。
@interface MyObject : NSObject
{
NSString* name; // こちらはアクセスされるメンバー変数
}
@property (retain) NSString* name; // これはアクセサメソッド名
@end
@implementation MyObject
@synthesize name; // 自動アクセスメソッド生成(synthesize=合成)宣言;アクセサメソッド名で宣言
// コンパイラレベルで静的にsetter/setterを自動作成する
//@dynamic name; // こう書くと、実行時にリンクされる事を宣言する(ゆえにコンパイラ時のエラーチェックがなくなる)
@end
最近(少なくともXcode5)では、多くの場合大幅に簡略化され、@interface部にだけ、しかも1行で記述すれば良くなった。
@interface MyObject : NSObject
@property (retain) NSString* name;
@end
この場合、アクセスはself.nameであるが、KVOを発生させずまたsetterを経由せずインスタンスに直接アクセスしたい場合は、
名前の前に_=アンダーバーをつけてアクセスする。
ということは、内部では以下の様な宣言が自動的に作られているように思えたりするが、実はちょっと違う。
@interface MyObject : NSObject
{
NSString* _name; // こちらはアクセスされるメンバー変数
}
@property (retain) NSString* name; // これはアクセサメソッド名
@end
@implementation MyObject
@synthesize name=_name ;
@end
この相当のコードが自動的に生成されている、と思いがちだが実はちょっと違っていて、
@private
NSString* _name;
である。この違いは、_nameはそのクラスのメソッド内では参照できるが、これを継承した子クラスからは参照できないということである。
古い書き方の場合は@publicになるので、子クラスからも参照できる。
なので、無闇矢鱈と新しい書き方に変更できるわけではない。もちろん、.プロパティ式に書き換えれば子クラスからもアクセスはできるが、
何度も書くとおり、プロパティ式とメンバー変数への直アクセスは意味が違うので、単純には書き換えられない。
新しいXcodeでは.m側にも@interfaceが書けるようになったので、
.hには@private相当=子クラスにも公開分をメンバー変数及び@propertで記述し、.mには@synthesizeも記述、
.mには@private相当を@property分だけで記述、と区分けすべきなのだろう。
プロパティーを使った呼び出し方は以下のとおり。
MyObject *obj;
obj.name=@"文字列";
//または _name=@"文字列";であるが、この2文は必ずしも同じではない
NSLog(@"aString:%@",obj.name);
プロパティ名は必ずしもメンバ変数名と同じにする必要はない。
と言うか、分けた方が良い場合もある。その場合は、
@interface MyObject : NSObject
{
NSString* name_;
}
@property (retain) NSString* name; // これはアクセサメソッド名
@end
@implementation MyObject
@synthesize name=name_; // これが重要
-(void)makeString
{
name_=@"文字列"; // メソッド内ではメンバ名のままアクセスできる
// self.name=@"文字列"と結果は同じ(ただし発生するコードは異なる)
~
}
@end
とする。この場合は新しい書き方は使えない。
呼び出し方は上記と同じ。
このように、プロパティを宣言しておけば、「見かけ上」Cの構造体と同じ記述でアクセス出来る様になる。
ここで、.nameはメンバー名ではなくプロパティ名である。
.もメンバー名を示すセパレーターではないので、objがアドレスだからといってobj->とは書かない。
obj->は、そのクラスをCから参照するときには有効な式であり、意味が全く異なる。
メンバーの型がCの標準型やそれに準拠するBooleanなどでない場合、
@propertyの後ろの()内には属性を指定する。これにより、自動生成式が変わる。
Cの標準型では指定してはいけない。そうしても付けたければassignのみである。
複数の属性を指定するときは","で並べて書く。
属性名 | 意味 |
readwrite | 読み書き可能;getter&setterが生成される |
readonly | 読み込み専用;getterのみ生成される |
getter=メソッド名 | getterメソッド名を変更する |
setter=メソッド名: | setterメソッド名を変更する
1引数を持つので名前の後ろに':'が必要となる(メソッド名は:も含めてラベル名となるから)。
|
nonatomic | 並行動作時にも再入可能(当たり前だと思うのだけど?理解できず) |
assign | 参照のみ。NSIntegerなどC標準型に相当する物は全てこれを指定する |
retain | 必要な時期まで保持する(オブジェクトに対してのみ設定可能) |
copy | オブジェクトを「深くコピー」する。
この属性を持たせておけば、代入式だけで内容のコピーまで自動的に行う式が生成される。 |
ARCになってから、strongとweakが新設されたが、基本的にassign=weak,retain=strongと書き換えればいい。
厳密には違うようだが。
getter/setterで関数名を指定しないとき、@synthesizeで自動生成されるメソッド名は、
getter | プロパティ名そのもの |
setter | setプロパティ名:(ただしプロパティ名の先頭を大文字化する) |
となる。通常@property宣言時は.構文を使うべきなので、余り意識することはないが、
オブジェクトをid型で受ける時など、idではプロパティー式は使えないので必要になる。
getter側はプロパティ名そのものなので、
var.name
[var name]
は等価となる。すなわち、「プロパティ名はそのままメソッドとして使える」ことだけは覚えて必要がある(使われることがあるから)。
最後3つは
「オブジェクトの所有権」である。
プロパティで指定される変数がクラスの場合、その所有権(保持状態)を指示できる。
これにより、getter/setterの生成式が変わる。
copyを使った場合、OSにより自動的にメモリが確保されるので、
不要になったらユーザーがreleaseする必要がある。
retainの場合は、基本的にはその参照される元の領域を確保した側でもreleaseを発行しているが、
参照が終わるまで保持され続けるので、参照終了を知らせるため、やはりユーザー(というか呼び出し側)がreleaseする必要がある。
もし@synthesizeを使わず自前でgetter/setterを実装するときは、
// getter側
- (NSString*)name
// 読み出しだけなので引数はなく。アクセスする変数と同じ型のリターン値を持つ。
{
~
}
// setter側
- (void)setName:(NSString*)name
// アクセスする変数と同じ型の引数を1つ持つ。返値はない。
{
~
}
とする。@propertyがあるのに@synthesizeもこの実装もないと警告が出る(実装漏れを防げる)。
なお、setterと同じメソッドを作ったからといって、プロパティー式での代入が有効になるわけではない。
それが出来るのは、あくまで@propertyで宣言されたもののみである。
古い書き方の場合、プロパティ名はメンバー変数名と同じにすることも多かったが、
プロパティはその「見かけ」とは異なり、実際にはメソッドの自動生成式であることを忘れてはならない。
なまじCの式と同じに見えるため、同じに理解してしまうと大きな間違いを犯す。
「Cと似て非なる動作をする」わけである。
そのため、新しい書き方に変更してプロパティ式と内部インスタンスを分離するようにしていくのが、
バグを減らすためにも良い。
列挙子/高速列挙
配列などの要素にループでアクセスするとき、Cでは要素数を自分で管理する必要があったが、
Cocoa Touchのクラスでは要素数を得ることが出来るようになっているので、安全に=要素個数を超えることなくループを回すことが出来る。
要素番号が必要な場合は、Cと同様のループを使う。
// 配列
NSUInteger i; // 要素番号
NSArray *array = [NSArray arrayWithObjects:@"abc", @"def", @"ghi", @"jkl", nil];
for (i = 0; i < [array count]; i++) {
NSLog(@"index: %d, value: %@\n", i, [array objectAtIndex:i]);
}
// 辞書
NSDictionary *dict =
[NSDictionary dictionaryWithObjectsAndKeys:
@"futaba" , @"name",
[NSNumber numberWithInt:4], @"age",
@"white" , @"color",
nil];
NSArray *keys = [dict allKeys];
for (i = 0; i < [keys count]; i++) {
NSLog(@"key: %@, value: %@\n",
[keys objectAtIndex:i],
[dict objectForKey:[keys objectAtIndex:i]]);
}
Objective-Cでは「列挙子」というものを使ってループを構築することも出来る。
配列(NSArray,NSSet)/辞書(NSDictionary)などがそれをサポートするメソッドを持っている。
これは要素の終端を判定してループを構築するもので、要素番号を保持しておく必要がなくなる。
この場合はwhileとNSEnumeratorというクラスを使う。
NSEnumerator
メソッド名 | 動作 | 持っているクラス |
-(NSEnumerator *)objectEnumerator | 配列や辞書の内容を先頭から、呼び出し毎に返す | NSArray/NSDictionary |
-(NSEnumerator *)reverseObjectEnumerator | 配列の内容を最後から、呼び出し毎に返す | NSArray/NSMutableArray |
-(NSEnumerator *)keyEnumerator | 辞書のキーの列挙子を返す | NSDictionary |
-(id)nextObject | 次の要素を得る | NSEnumerator |
-(NSArray *)allObject | 残っている要素を配列に得る(これまでにnextObjectで取り出した分は除かれる) | NSEnumerator |
// 配列
NSArray *array = [NSArray arrayWithObjects:@"abc", @"def", @"ghi", @"jkl", nil];
NSEnumerator *enumerator = [array objectEnumerator];
id obj;
while (obj = [enumerator nextObject]) { // nilで終了
NSLog(@"value: %@\n", obj);
}
// enumeratorは解放の必要がない(というかしたら駄目)
// 辞書
NSDictionary *dict =
[NSDictionary dictionaryWithObjectsAndKeys:
@"futaba" , @"name",
[NSNumber numberWithInt:4], @"age",
@"white" , @"color",
nil];
NSEnumerator *objEnumerator = [dict objectEnumerator]; // 辞書の場合は内容の列挙子を得る
id obj1;
while (obj1 = [objEnumerator nextObject]) {
NSLog(@"value: %@\n", obj1);
}
NSEnumerator *keyEnumerator = [dict keyEnumerator];
id key;
while (key = [keyEnumerator nextObject]) {
NSLog(@"key: %@, value: %@\n", key, [dict objectForKey:key]);
}
Objective-C 2.0では、さらにfor文が拡張され、配列(NSArray)/辞書(NSDictionary)の内容が高速かつ安全に「参照」出来る様になっている。
正確には、「NSFastEnumerationプロトコルに準拠したオブジェクト」に対して使える。
「高速」はコンパイラが最適化したコードを発生するという意味であり、「安全」は終了判定を記述する必要がないことを意味する。
なお「参照」のみなので、変更は出来ない(変更しようとすると例外が発生する)。
for ( 型 新変数 in 参照クラス ) {
~
}
NSArrayとNSSetの場合は列挙されるのは要素内容、NSDictionaryではキーが対象となる(Mutableも同様)。
それ以外のクラスは、何が対象となるか確認する必要がある。
たとえばを、Core DataクラスのNSManagedObjectModelはそのエンティティを列挙する。
// 配列
NSArray *array = [NSArray arrayWithObjects:@"abc", @"def", @"ghi", @"jkl", nil];
for (id obj in array) { // 新しいクラス変数を作り、上記arrayの内容を参照する(最後まで)
NSLog(@"value: %@\n", obj);
}
// 辞書の場合
NSDictionary *dict =
[NSDictionary dictionaryWithObjectsAndKeys:
@"futaba" , @"name",
[NSNumber numberWithInt:4], @"age",
@"white" , @"color",
nil];
for (id key in dict) {
NSLog(@"key: %@, value: %@\n", key, [dict objectForKey:key]);
}
// NSEnumeratorの場合
NSEnumerator *enumerator = [dict objectEnumerator];
for (id obj in enumerator) {
NSLog(@"value: %@\n", obj);
}
// 特定の要素が出てきたときにループを終了したい場合
for (NSString *element in enumerator) {
if ([element isEqualToString:@"Three") {
break;
}
}
いずれも、通常ループや列挙子と異なり、終了判定式をループ内に記述していないのが特徴である。
ファイル所有者オブジェクト
ファイル所有者オブジェクト(File's Owner object)は実はObjective-Cの機能ではない。
nibファイル内のオブジェクトとのやり取りをするために使われるオブジェクトである。
アクション-ターゲットで要素とその処理をするメソッドを結びつけることは、
その処理を全てソースで書くことは当然出来るが、IB上では線を引っ張るだけで指定することも出来る。
(ただし、ターゲットとなるメソッド名のソース上への記述も必要)。
各要素(アクション)から引っ張る先になるのがFile's Ownerである。
File's Ownerは、ソース上でIBAction指定されたメソッドを認識し、
そこへ線が引っ張られた時に、接続するアクションを選択させる。
逆に、File's Ownerで見ると、
「Received Action」にてそこにつながっている全ての接続を見ることが出来る。
1つの要素からは複数のターゲットに接続することも出来る。
不要な接続はそこで切ることが出来る。
同様に、メンバー(ターゲット)を各要素に引っ張る元になるのがFile's Ownerである。
File's Ownerは、ソース上でIBOutlet指定されたメンバーを認識し、
そこからに線が引っ張られた時に、接続するターゲットを選択させる。
nibファイルを読み込むときにファイルの所有者(File's Owner)となるオブジェクトを指定すると、
ファイルの所有者への接続は、その外部のオブジェクトへの接続となる。
つまり、ファイルの所有者代理オブジェクトは実行時に決定される、逆に言えば実行時まで分からない。
nibファイルは実行時に読み込まれ展開されてインスタンスになる。
そのときに、デリゲートを含む、ターゲットとアクションのインスタンス間の接続も確立(復元)される。
ファイルの所有者オブジェクトが実際にどのクラスなのかは、IBでxibファイルを見ればよい。
通常ここはUIApplicationである。
すなわちファイル所有者オブジェクトがアプリケーションが生成するUIApplicationオブジェクトの
代理として使われることになる。