前置き
こちらの記事には2014/06/09現在、公式にはリリースされていないiOS8プレリリースドキュメントへのリンクが含まれます。iOS8にて新しく追加された内容には一切触れておらずAppleとのNDA規約にも違反するものではないという認識ですが、場合により予告なく削除する可能性があります。予めご了承ください。本題
iOS8プレリリースドキュメントを眺めていて気になったのですが、ほとんどのCocoaのメソッドの引数に!がついています。例えばNSKeyValueObservingプロトコルのaddObserver:forKeyPath:options:context:メソッドのシグネチャは以下のようになっています。func addObserver(_
anObserver
: NSObject!,forKeyPath
keyPath
: String!,options
options
: NSKeyValueObservingOptions,context
context
: CMutableVoidPointer)第1引数と第2引数に!がついているのがわかると思います。
更にもうひとつ気になったのが、このメソッドの第3・第4引数です。これらは元々0やNULLポインタを渡すことができる引数だったのですが、見ての通り引数が?で宣言されておらず、そのままnilを渡してしまうとエラーになってしまうように見えます。
直感的に考えると、
- !がついている引数は呼び出し時に強制的にunwrapされるので、nilを渡してはいけない、必須引数なのではないか?
- !がついていない第3第4引数はnilが渡せないのではないか?
という風に思うのですが、実際には
- !がついている引数にnilを渡すことができる。さらにnilを渡してもランタイムでクラッシュしない。
- 今回の例のaddObserver:forKeyPath:の場合はクラッシュしますが、これは元々のAPIがそうだったからで、例えば他のCocoaのAPIで!がついているものでnilを渡しても問題ないものが多数存在します。
- !がついていないNSKeyValueObservingOptions, CMutableVoidPointerにnilを渡してもコンパイルエラーにすらならず、何の問題もなくそのまま動作する。
このような私の理解とは正反対の挙動をします。大変気になったので調べてみることにしました。
!がついている引数の謎
これについてはそもそも私のSwiftに対する理解が完全に間違っていました。正しい理解は以下のとおりです。- 無印型 - nilを渡すことができない。nilになる瞬間が一瞬もない。これこそがnilを渡せない必須引数である。
- ?型 - Optional型であり、nilを扱うことができる。これについては問題ない。
- !型 - ImplicitlyUnwrappedOptionalという特殊なOptional型であり、Optional型なのでnilを扱うことができる。すなわち!が付いている引数については任意引数でありnilを渡すことができる。
- !も?も演算子でも命令でもなく型であり、!や?をつけることで対象を型に包んでいる、と考えれば良いと思います。
- !と?の違いはメンバにアクセスした際に暗黙的かつ強制的に元の型にアンラップされるか、そのままOptionalとして扱われるかの違いだけです。
ではなぜCocoaのAPIは引数で?ではなく!を使ってnilを受け取れるようにしているのでしょうか?これは引数を受け取った後に、その引数にアクセスした場合の挙動がおそらく影響しているのではないかと思われます。以下の例を見てください。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ExampleShark { | |
var name:NSString! | |
var howSwim:NSString! | |
init(name:NSString!, howSwim:NSString!) { | |
self.name = name | |
self.howSwim = howSwim; | |
} | |
func swim() -> String { | |
return "\(self.name) swims like '\(self.howSwim)'" | |
} | |
func dangerousSwim() -> String { | |
return "\(self.name.description) swims like '\(self.howSwim.description)'" | |
} | |
func safeSwim() -> String { | |
return "\(self.name?.description) swims like '\(self.howSwim?.description)'" | |
} | |
} | |
let shark1 = ExampleShark(name: "Shark", howSwim: nil) | |
shark1.swim() // OK, "Shark swims like '{nil}'" | |
shark1.dangerousSwim() // CRASH | |
shark1.safeSwim() // OK, "Shark swims like '{nil}'" | |
let shark2 = ExampleShark(name: nil, howSwim: nil) | |
shark2.swim() // OK, ""{nil} swims like '{nil}'" | |
shark2.dangerousSwim() // CRASH | |
shark2.safeSwim() // OK, ""{nil} swims like '{nil}'" | |
let shark3 = ExampleShark(name: "The Average Shark", howSwim: "Swim Swim Swim Lurk") | |
shark3.swim() // OK, "The Average Shark swims like 'Swim Swim Swim Lurk'" | |
shark3.dangerousSwim() // OK, "The Average Shark swims like 'Swim Swim Swim Lurk'" | |
shark3.safeSwim() // OK, "The Average Shark swims like 'Swim Swim Swim Lurk'" |
こちらのコードですが、safeSwim()メソッドが旧Objective-Cと完全に同じ挙動を示します。これは以下の様な理由によるものです。
- self.name(!型)が?型によってラップされる。
- self.name?の.descriptionにアクセスする。このとき?型によってラップされているので、self.nameがnilであれば何も起こらず、self.nameがnilでなければまず?型からアンラップされ、次に強制的に!型からアンラップされ、その結果通常通りdescriptionが呼び出される。
- 通常だと?型の返り値は?型にラップされてしまうが、self.nameが!型なので強制的にアンラップされ?型ではなく素のString型を返すことができる
- ここだけよく理解できてないです・・・
間違っていたらすみません(´・_・`)
とにかくこれこそが旧Objective-CのAPIについて!で型が表現されている理由ではないかと考えています。
!がついていないのにnilが渡せる引数の謎
こちらの謎はカラクリがわかってしまえば簡単です。前回の記事をご覧になった方はお気づきになったかもしれませんが、これらの型はAppleが__convertion()メソッドをNilTypeに対して追加しているため、nilを問題なく扱うことができます。まとめ
- !が付いている引数は?と同様にnilを渡しても良い引数である。nilを渡せない必須型は何もついていない引数のみである。
- !はImplicitlyUnwrappedOptionalという特殊なOptional型であり、アクセス時にOptionalではなく強制的に元の型を返す点がOptionalと異なる、と考えれば理解しやすい。
- 変数をすべて!で定義し、メンバアクセス時に常に?をつけるようにすると、どの変数にもnilを渡すことができ、実行時にnilがあればそこで実行がスキップされ、さらに返り値もOptionalではない通常の型にできるため、旧Objective-Cとほぼ同じ挙動になる。
- ただしSwiftで新しく用意されているAPIを見る限りnilを扱う必要がある箇所については可能な限り!ではなく?を引数や返り値に使っているように見えるため、新しくSwiftで書く箇所については!を乱用するべきではないと思われる。
- Cocoaの用意している型のうち、元々nilやNULLや0を渡すことができた型については、NilTypeに__conversion()メソッドが追加されているので、そのままnilを渡すことができる。