好きな言語はC++
です.どうも @mizuhugu35 です.
いかがお過ごしでしょうか.私は今朝寝違えたため,首がとても痛いです.
初めに
Objcを触る機会があって,基本ができていない気がしてきたので,今年の4月頃に勉強しました.
それをこの2022年の最後として,Objective-Cを再確認しよう!という記事です.
(iOSアプリ開発は,Swiftから入ったクチです)
すべてを書くと長すぎるので端折るか,また違う記事として書こうと思います.
間違っているところがありましたら,ぜひコメントお願いします!
Objective-C
名前の通りオブジェクト指向の言語で,メッセージ式と呼ばれる記述方法が特徴的です.
// - (id) a; で自分自身を返すメソッド
[object a];
// ネストできる
[[[object a] a] a];
初めて見たときは, なんかLISPみたいな見た目してんな...と思いました.
コンパイラ指定子
@
から始める命令のことで,代表的なのは@intarface
,@implementation
, @property
,@synthesize
とかです.
Objcでは直接C言語を使用できるので,区別できるようになっています.
クラス
クラスを宣言するときには,実装ファイル (m/mm)
とインターフェイスファイル (h)
の二つに分けて記述します.
インターフェース
ヘッダファイルには外部に公開したいメンバ関数(メソッド)などを記述します.C++でいうところのpublicに指定したものに相当します.ここで宣言したメソッドは必ず実装しなければなりません.また継承するにはクラス名の隣に: 親クラス名
とします.
@interface ClassA: NSObject
@property (nonatomic) int a;
- (id) initWithValue: (int) value;
- (void) addAndSayA:(int) value;
@end
-
から始まるものはインスタンスメソッドということを示しています.+
から始まればクラスメソッドです.
id
型はいわばvoid *
型です.型が変わる動的に扱いたいときにはこのid
が使えるわけですね.
実装
実装ファイルにはプライベートなメソッドとインターフェイスで宣言されたメソッドの両方を記述していきます.
self.*
としてアクセスすることに注意です.この記述では気が付きませんが,実はこれゲッタとセッタを通してます.C/C++
の感覚でいると???ですので...
@implementation ClassA
@synthesize a;
- (id) initWithValue: (int) value{
if(self = [super init]){
self.a = value;
}
return self;
}
- (void) __addA:(int) value{
self.a += value;
}
- (void) addAndSayA:(int) value{
[self __addA: value];
NSLog(@"%d", self.a);
}
@end
property
先のコードではGetter/Setter
などのアクセサを宣言していなかったと思います.実装ファイルでドット演算子を使用し,
自身のインスタンス変数にアクセスすることができました.本来であればドット演算子は使用できず,[]
を使わなければなりません.またGetter/Setter
の宣言・定義も必要なはずです.
これは@property
というコンパイラ指定子を利用することで自動的にアクセサの生成 も してくれる指定子のおかげらしいです.
重要なものとして,この指定子はインスタンス変数の生成を行います.つまり,プロパティを使用しなければ以下のように記述する必要があるます.
@interface ClassA: NSObject{
int a;
}
// 省略
この場合,self->a
でアクセスします.実装ファイル内で「なんでselfはポインタぽいのにアロー演算子じゃないの」と思ったのですが,このプロパティ君がゲッタとセッタを「ドット演算子」で提供してるからというからくりだったわけです.
ちなみにself->a
を使うとプロパティで指定できる排他処理などを通さないため,スレッドセーフでなくなります.だめです.
プロパティでは属性の設定が詳しく行えます
オプション名 | 説明 |
---|---|
readonly | 読み取りのみ |
readwrite | 読み書き可能 |
assign | 代入によって後で設定されることを示す.プリミティブな型に指定 |
retain | 保持されながら設定されることを示す.参照カウントを増やす. |
strong | 強い参照で設定される.勝手に開放されないように指定 |
weak | 弱すぎ参照で設定される.勝手に開放されるかも.nilが設定されるらしい |
copy | コピーされて設定されることを示す.強い参照だがコピーを受け取る. |
getter | ゲッターの名前を指定 |
setter | セッターの名前を指定 |
atomic | スレッドセーフ.つまり排他的 |
nonatomic | スレッドセーフにしない |
相容れない設定でなければすべて設定することができます.
例えば
@property (nonatomic, readonly, strong) int a;
などです.
strong/weakなどはARC
が有効でないと使えません.(あたりまえ)
atomic / nonatomic
atomic
を指定した場合, セッターにはlinux
でいうところのptherad_mutex
で囲んで,ロックとアンロックをかけてくれます.
ゲッターには, セッターの排他制御に加え,retain
とautorelease
が追加されるそうです.これらはそれぞれ確保と自動的な開放(参照が切れたら解放) を行います.つまり,ゲッターで取得されたオブジェクトや値が勝手に開放されるのを防いでくれます.
synthesize
こいつは@property
で宣言されたインスタンス変数に対して,実装ファイル内で自動的に変数の生成を行ってインターフェースと統合してくれます.
@synthesize
宣言しないと_*
という名前で自動的に生成されますが,Xcodeで警告がたくさん出るので書いた方がよさそうです.
ちなみに@synthesize
のスコープはこのコンパイラ指定子より後ろなので,@implementaion
の直後に書くのが良いです.
クラス(Object)をメンバ変数にするとき
クラスを使用するときはポインタで扱います.NSArray
ならば
@property (atomic) NSArray* ary;
となります.C++の感覚で行くと自分が作ったクラスをほかのクラスで利用するときヘッダファイルに#include
すると思います.Objc も同様で#import
すればいいのですが,@class
を利用するとクラスのプロトタイプ宣言的なことが行えます.そうすることで,余計な情報を読み込まなくて済みます.
「余余計な情報」というのは例えば, Project-Swift.h
のような自動変換されたコードで,こういったものはトラブル回避のため実装ファイルでのみ読み込んだ方がいいからです.
ヘッダファイルは他のクラスで利用するときに, import
しますが,そこにProject-Swift.h
などが読み込まれていると自動生成されたコード等の影響範囲が大きくなってしまいます.
これの問題はC++
のコードでも同じで, C/C++
のヘッダが読み込まれるとそのファイルは純粋なObjective-Cではなくなってしまうため,他のファイルからimport
した際にC/C++
のコードとして認識されてしまいます.
従って,私はObjective-CのヘッダファイルではC/C++のヘッダファイルは読み込まず, プロトタイプ宣言に止めています.
まとめ
当たり前すぎることで,はずかしいですが
// ダメ✖ : インスタンス変数の宣言
@interface ClassA: NSObject{
int a;
}
@end
// すばらしい ◯ : インスタンス変数の宣言
@interface ClassA: NSObject
@property (nonatomic) int a;
@end
// ダメ✖ : アクセスするときは..
self->a;
((ClassA*)objc)->a;
// すばらしい ◯ : アクセスするときは.. ドット演算子でGetter/Setterを通す!
self.a;
((ClassA*)objc).a;
ということですね.
アドベントカレンダーには書かないと思いますが,そのほかのメモリ管理におけるARC
, リファレンスカウンタ
, それすら使わない手動管理,C++クラスをWrapしれ使用する方法などもそのうち記事に書きたいです.
P.S.
Objective-C
の情報や書き方のパターンがたくさんあってどれがいいのかとても悩みました.
書き方やスタイルについてはこちらのNYTimes
のガイドが参考になりました.