Objective-C
Swift

[Swift] @objcの話 [Objective-C]

@objc の話

Swift4になって@objcを付けなければならないプロパティやメソッドが増えて大変ですね。
@objcって付けないとだめなの?っていう疑問がある人も多いと思います。

この記事では @objc を付けると何が起こるのかを解説しています。

ただし、素人が雰囲気で書いています。

Objective-Cの話

@objcを付けると何が起こるかを知るにはObjective-Cの話が必要です。
しかも割と低レベルな話です。ちゃんと説明できるかよくわかりません。

メソッド

ご存知の通り、Objective-CはC言語にオブジェクト指向な記述を付け加えたものです。
メソッドは他のOOPLと同じように隠し引数を持った関数としてコンパイルされます。

@implementation Hoge
- (void)moveTo:(NSPoint) point {
    // do something
}
@end

これをコンパイルすると次のようになります。(疑似コードです。)

void moveTo(id self, SEL cmd, NSPoint point) {
    // do something
}

隠し引数としてselfcmdが追加されています。
selfはレシーバー自身、cmdはSELと呼ばれるセレクタを表す何かです。
Objective-Cではメソッドをコンパイルするとふたつの隠し引数を持った関数が生成されるということを覚えてもらえればOKです。

プロパティ

古いObjective-Cと最新のObjective-Cではプロパティの捉え方が完全に変わってしまっています。
今は最新のObjetive-Cの話をします。

@interface Hoge: NSObject

@property NSRect point;

@end

分かるようなわからないような宣言ですが、まあわかるでしょう。
一般的な言語では、このプロパティはオブジェクトを構造体のように連続した領域に収められてアドレスに直接アクセスしたりするのですが、(新しい)Objective-Cではsetter, getterでアクセスするようになっています。
通常setter, getterはコンパイラにより自動的に生成されます。
setter, getterはObjective-Cの文法では次のようになります。

- (NSPoint)point;
- (void)setPoint: (NSPoint) point;

コンパイルされると以下のような関数になります。(疑似コード)

NSPoint point(id self, SEL cmd);
void setPoint(id self, SEL cmd, NSPoint point);

こちらもメソッド同じくselfcmdのふたつの隠し引数を持ちます。

Swiftの話

ではSwiftの話に戻りましょう。
公式で公表されているようにSwiftは未だABI安定に至っていませんので、以下の話はSwift4.0までの話となります。変わらないと思いますが。

メソッドとプロパティ

Objective-Cと同様にメソッドとプロパティがどのようにコンパイルされるか見てみましょう。

class Hoge {
    var point: NSPoint = .zero

    func moveTo(point: NSPoint) {
        // do somethig
    }
}

以下疑似コード

// getter
NSPoint point(Hoge *obj);

// setter
void setPoint(Hoge *obj, NSPoint point);

// method
void moveTo(Hoge *obj, NSPoint point);

Swiftでは隠し引数はオブジェクト自身ただ一つであるということです。

@objcの話

ここまで、Objective-CとSwift4.0でメソッドやプロパティがどのようにコンパイルされているかを見てきました。
それらにはObjective-CとSwift4.0で決定的な違いがありました。
隠し引数の数です。
Objective-Cには2つの隠し引数がありましたが、Swift4.0には1つしかありません。
これはつまり相互に利用することができないということです。

ただし、Swift側からObjective-Cのそれらを利用することが可能なように細工がなされています。
Objective-C側に存在する第二の隠し引数cmdはメソッド名プロパティ名から簡単に生成できるためそれらを呼び出すためのSwift用の関数が(半)自動的に生成されているのです。
Swiftで作成したアプリケーションバンドル内にframeworksフォルダがありその中に多量のdylibがあるのを見たことがあるかもしれません。
あれです。
この仕組みによりSwift側からObjective-C側へのアクセスは表面上はシームレスに行えます。

ところがObjective-C側からはそうはいきません。
これまで、SwiftからObjective-Cへのフィードバック的な変更が各種ありましたが、基幹部分への変更は行われていません。
つまりObjective-CのランタイムはSwiftを知らない状態のままなのです。
この状態でSwiftのメソッドを呼び出そうとするとObjective-Cは余分な第二引数を付けて呼び出してしまいます。(詳細は省きますが正しくは関数を見つけられないです。)
当然動きません。

そこで登場するのが @objc です。
@objcを付けられたメソッド、プロパティにはSwiftの実装に加え、Objective-Cで用いられるふたつの隠し引数を持った関数を同時に作成するようになります。
このことで@objcを付けたメソッド、プロパティはObective-Cからも正しく利用することができるようになります。

まとめ?

先に書いたように、素人が雰囲気で書いたものなのでいろいろ突っ込みどころがあるとは思いますが、ご了承ください。