2015年5月2日土曜日

[Swift] NSApp で学ぶ AnyObject

発端


とあるコードを書いていた時、以下の現象に遭遇した。
NSApp.active // 呼べない(error)
NSApp.activationPolicy // 呼べる(success)
NSApp.activationPolicy() // 呼べない(error)
はて。

NSApplication クラスに active プロパティは存在するのに呼び出しエラーが発生。
activationPolicy() メソッドに至っては呼び出し可能なものの関数呼び出しの () を付けられない始末。


宣言を調べてみる


想定どおり active はプロパティ(computed-property)として宣言されているし、activationPolicy() はメソッドとして宣言されている。
var NSApp: AnyObject!

// … 中略 …

class NSApplication : NSResponder, … {
 // … 中略 …
 var active: Bool { get }
 // … 中略 …
 func activationPolicy() -> NSApplicationActivationPolicy
 // … 中略 …
}
当然、以下のように明示的な NSApplication 型であれば定義どおりの動作となる。
let app: NSApplication = NSApplication.sharedApplication()
app.active // OK
app.activationPolicy // NG … というか Function 型が返る
app.activationPolicy() // OK
ということは NSApplication 型ではなく AnyObject! 型になっている NSApp 変数を使用したために、この現象が起きたのは明白だ。


疑問


気になるのは2つ。
  • なぜ AnyObject! に対する activationPolicy の呼び出しは成功するのに active 呼び出しは失敗するのか
  • なぜ AnyObject! に対する activationPolicy の呼び出しはメソッドであるにもかかわらず () をつける事ができないのか


Swift の AnyObject と Objective-C の id は少し違う


NSApp は Objective-C の場合 id 型で宣言されている。その NSApp が Swift で対応するクラスなんでも型の AnyObject が使われているのは納得がいく。

Objective-C の id 型なら、中身が何であろうと(宣言さえ存在していれば)どんなメッセージでも送る事ができる。

しかし Swift の AnyObject は、中身のインスタンスの正しいメンバ呼び出しでも失敗する。
class Foo {
 var myProperty = "Foo's property"
 func myFunction() { println("Foo's function") }
}
var foo: AnyObject = Foo()
foo.myProperty // error: 'AnyObject' does not have a member named 'myProperty'
foo.myFunction() // error: 'AnyObject' does not have a member named 'myFunction'
Swift の AnyObject は ObjC の id のような汎用インスタンス参照として用意されたものだが、完全に型安全だ。本来の型にキャストすることなしにメンバを呼び出すことはできない。

ObjC で id 型を扱った時に発生しやすいランタイムエラーを避けることができるものの、動的呼び出しのポテンシャルも損なっている。


AnyObject に対するメソッド呼び出しが成功したワケ


では NSApp が AnyObject であるのに NSApplication メンバを呼び出せるのはどういうワケかというと、Swift には AnyObject 型を ObjC の id 型と互換性を取るための仕組みがある。

You can also can any Objective-C method and access any property without casting to more specific class type. This includes Objective-C compatible methods marked with the @objc attribute.

Using Swift with Cocoa and Objective-C

Objective-C 互換クラスを作成するための属性 @objc を付けたクラスならば、一旦 Objective-C クラスにブリッジされるため id 型と同等の働きとなる。 ObjC 由来のクラス(NSObject)を継承しているクラスならば、すでに @objc 属性が付いたものとして扱われる。

つまり NSApp(AnyObject)の実体である NSApplication は ObjC 由来のクラスなので @objc 属性付きであるため、メンバ呼び出しが ObjC のそれと同等となった。
AnyObject である NSApp に対して activationPolicy が呼び出せたのは、この機能のおかげだ。


AnyObject に対するプロパティ呼び出しが失敗したワケ


次の例を見れば分かる通り、本来なら @objc 属性を付けたクラスならば AnyObject 型に対してプロパティ呼び出しも成功するはず。
@objc class Foo {
 var myProperty: String = "property"
}
var foo: AnyObject = Foo()
foo.myProperty // success
にも関わらず、NSApp.active 呼び出しは失敗した。なぜか。

実は active の他にも NSApp という AnyObject に対して、呼び出しが失敗するプロパティは幾つかあったが、逆に成功するプロパティもあった。
どうやら Swift と ObjC での宣言の違いが原因のようだ。
Swift 宣言ObjC 宣言AnyObject での呼び出し可否
unowned(unsafe) var mainWindow: NSWindow? { get }@property (readonly, assign) NSWindow *mainWindow;OK
unowned(unsafe) var keyWindow: NSWindow? { get }@property (readonly, assign) NSWindow *keyWindow;OK
var active: Bool { get }@property (getter=isActive, readonly) BOOL active;NG
var hidden: Bool { get }@property (getter=isHidden, readonly) BOOL hidden;NG
成功する呼び出し(mainWindow, keyWindow)は Swift / ObjC ともに宣言が一致している。逆に失敗する呼び出し(active, hidden)は、ObjC で getter に別名が付けられている。

要するに Swift 側の宣言と ObjC 側の宣言でプロパティ名が一致していれば AnyObject に対して呼び出し可能となる。逆に active と isActive のように宣言が一致していなければ Swift 側の宣言で呼び出す事ができない。

@objc 属性がついた AnyObjectは ObjC ブリッジのため ObjC API でメンバを探すらしい。なので NSApp で active や hidden を呼び出したいのなら
NSApp.isActive // success
NSApp.isHidden // success
上記のように ObjC の定義を使えば成功するのであった。


AnyObject に対するメソッド呼び出しに () が付けられなかったワケ


activationPolicty 呼び出しに () が付けられなかったのも同様の問題だろうと思われる。

Objective-C 側では
- (NSApplicationActivationPolicy) activationPolicy NS_AVAILABLE_MAC(10_6);
という宣言で、メソッドではあるけども KVC 準拠なので ObjC 側でも呼び出しはプロパティ扱いになる。 この ObjC 宣言にマッチして呼び出しが発生するので、「プロパティに () を付けるな」というエラーになっていたのだろう。

@objc な AnyObject ではこういう事が起きてしまうらしい。


結論

  • 中身が @objc なクラスのインスタンスであれば AnyObject の実体に対するメンバ呼び出しはアリ
  • @objc な AnyObject の中身が Swift / ObjC 両方に定義があるクラスのインスタンスでは、ObjC 側の定義で呼び出す

参考

0 件のコメント:

コメントを投稿