こんばんは、クリスマスまであと3日ですね。
…。
すみません、大遅刻しました。Swift愛好会 Advent Calendar 201622日目の投稿です。
前置き: SE-0116(id-as-any)とは
Swift2→Swift3に移行して、特にFoundationを利用する際に大きな変更を感じるのが、引数の型が AnyObject
から Any
に変わった点ではないでしょうか。
- この変更が何故必要だったのか
- どういう仕様なのか
については、以下のSwift Evolutionに書かれています。
swift-evolution/0116-id-as-any.md at master · apple/swift-evolution
https://github.com/apple/swift-evolution/blob/master/proposals/0116-id-as-any.md
以前、自分はここらへんを読み解いて日本語スライドにしました。
5分でわかるSE-0116(id-as-any) // Speaker Deck
https://speakerdeck.com/takasek/5fen-dewakaruse-0116-id-as-any
なお、このスライドはSwift愛好会 vol12でも飛び入りで発表させていただきました。話の流れ次第で、飛び入りでそういうこともできちゃうのが、この勉強会の魅力ですね!
とAdvent Calendar主催にちなんだダイマをしつつ。
この記事では、 as AnyObject
の時に何が起こるのかを、具体的なコードとともに確認してみます。
as AnyObject
は値型をboxingする
まず基本のbridgeの機能。
値型変数は、 as AnyObject
の時点でclassインスタンスに変換されます。これをboxingといいます。
do {
let value = 1
let a = value as AnyObject
let b = a
a === b //true
//as AnyObjectすると、classインスタンスにboxingされるので、 === による同一性比較が可能
}
なお、 as AnyObject
のタイミングが異なれば、boxingされるclassインスタンスは別物になります。
do {
let value = 1
let a = value as AnyObject
let b = value as AnyObject
a === b //false
}
もうちょい細かい as AnyObject
の詳細設計
as AnyObject
は、Swift Evolutionによれば、
- クラス型 の場合、何もしない。
-
Bridged value types (String, Array, Dictionary, Set, etcのこと)の場合、
- 既存の
_ObjectiveCBridgeable
プロトコルを流用 - SE-0116ではノータッチ
- (けど、将来的にはもっと多くの型をbridgeableにしたい)
- 既存の
-
その他の値型の場合、immutableなclassでboxingする
- 元の値型に戻せるなら、内部実装を外に見せる必要はない
という挙動になっているそうです。確認してみましょう。
クラス型 の場合、何もしない。
do {
class C {}
let c = C() // (C #1)
let bridgedC = c as AnyObject // (C #1)
c === bridgedC //true
}
元の c
と、それを as AnyObject
にした bridgedC
が同一インスタンスなのが分かりますね。
Bridged value typesのブリッジは、既存の _ObjectiveCBridgeable
プロトコルを流用
_ObjectiveCBridgeable
プロトコルについては、@es_kumagaiさんによる以下のQiita記事が詳しいです。
Objective-C Bridge の仕組とそこから感じたこと - Qiita
http://qiita.com/es_kumagai/items/bc154ee0a8a842e3b3ca
記事中に紹介されているのはSwift2時代のインタフェースで、最新のインタフェースはこんな感じですね。
https://github.com/apple/swift/blob/master/stdlib/public/core/BridgeObjectiveC.swift
public protocol _ObjectiveCBridgeable {
associatedtype _ObjectiveCType : AnyObject
func _bridgeToObjectiveC() -> _ObjectiveCType
static func _forceBridgeFromObjectiveC(
_ source: _ObjectiveCType,
result: inout Self?
)
@discardableResult
static func _conditionallyBridgeFromObjectiveC(
_ source: _ObjectiveCType,
result: inout Self?
) -> Bool
static func _unconditionallyBridgeFromObjectiveC(_ source: _ObjectiveCType?)
-> Self
}
これらのプロトコルに適合している場合、 as AnyObject
した場合に、適切な型が選択されます。
do {
let anyNum = 1 as AnyObject
type(of: anyNum) //_SwiftTypePreservingNSNumber.Type
}
注意点として、一度値型へとキャストされた場合は、boxingは解除されてしまいます。
do {
let value = 1 as AnyObject
let a = value
let b = value as Any as AnyObject // AnyならNSNumberのままなんだけど、
a === b //true
}
do {
let value = 1 as AnyObject
let a = value
let b = value as Int as AnyObject // as Intだとboxingが解除されてInt型になっているので…
a === b //false
}
ところで、 _SwiftTypePreservingNSNumber
って?
NSNumberのサブクラスだそうです。
NSNumberだと実際の数値型を覚えていないので、ちゃんとブリッジできるようオリジナルの型を保持してdynamic castできるようにしたもの …らしい。
proposal:
https://github.com/apple/swift-evolution/blob/master/proposals/0139-bridge-nsnumber-and-nsvalue.md
implementation:
https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/TypePreservingNSNumber.mm
do {
let anyNum = 1 as AnyObject
let nsNumFromLiteral = 1 as NSNumber
type(of: anyNum) //_SwiftTypePreservingNSNumber.Type
type(of: nsNumFromLiteral) //__NSCFNumber.Type
}
さらに脱線。
ちなみに、NSNumberへの変換時に、それがInt型か、リテラルか、イニシャライザを通しているか、などで型が微妙に違ったりもします。
do {
let nsNumFromLiteral = 1 as NSNumber
let nsNumFromIntLiteralInitializer = NSNumber(integerLiteral: 1)
let intOne: Int = 1
let nsNumFromInt = intOne as NSNumber
let anyNum = 1 as AnyObject
type(of: nsNumFromLiteral) //__NSCFNumber.Type
type(of: nsNumFromIntLiteralInitializer) //__NSCFNumber.Type
type(of: nsNumFromInt) //_SwiftTypePreservingNSNumber.Type
type(of: anyNum) //_SwiftTypePreservingNSNumber.Type
}
__NSCFNumber
は数値リテラルからのブリッジ、あるいはNSNumber.initを通した場合に使われる型。
あと、 __NSCFBoolean
は真偽値の表現で、 Swift.Bool
を as AnyObject
したときには _SwiftTypePreservingNSNumber
ではなくてこっちが使われたりする…みたいな話もあるんだけど、そこらへんの深掘りは、まあまた別の機会がありましたら。
…脱線しすぎました。そろそろ本筋に戻りましょう。
その他の値型の場合、immutableなclassでboxingする
ここまで、クラス型 と Bridged value types について、 as AnyObject
したときに何が起こるかを確認したところでしたね。最後に、 その他の値型について確認します。
do {
struct S {}
let s = S() //(S #1)
let bridgesS = s as AnyObject //(S #1)()
type(of: s) //(S #1).Type
type(of: bridgesS) //_SwiftValue.Type
}
_SwiftValue
とかいう奴が出てきました。
インタフェースはこんな感じ。
https://github.com/apple/swift/blob/master/stdlib/public/runtime/SwiftValue.h
// _SwiftValue is an Objective-C class, but we shouldn't interface with it
// directly as such. Keep the type opaque.
#if __OBJC__
@class _SwiftValue;
#else
typedef struct _SwiftValue _SwiftValue;
#endif
namespace swift {
/// Bridge a Swift value to an Objective-C object by boxing it as a _SwiftValue.
_SwiftValue *bridgeAnythingToSwiftValueObject(OpaqueValue *src,
const Metadata *srcType,
bool consume);
/// Get the type metadata for a value in a Swift box.
const Metadata *getSwiftValueTypeMetadata(_SwiftValue *v);
/// Get the value out of a Swift box along with its type metadata. The value
/// inside the box is immutable and must not be modified or taken from the box.
std::pair<const Metadata *, const OpaqueValue *>
getValueFromSwiftValue(_SwiftValue *v);
/// Return the object reference as a _SwiftValue* if it is a _SwiftValue instance,
/// or nil if it is not.
_SwiftValue *getAsSwiftValue(id object);
/// Find conformances for SwiftValue to the given list of protocols.
///
/// Returns true if SwiftValue does conform to all the protocols.
bool findSwiftValueConformances(const ProtocolDescriptorList &protocols,
const WitnessTable **tablesBuffer);
} // namespace swift
コメントにめっちゃ書かれている通り、 _SwiftValue
は直接取り扱うべきものではないようです。
まあ、Swiftの世界だけで完結できるならObj-Cでの実装の詳細なんて気にする必要ないし、もし意識する必要があるなら _ObjectiveCBridgeable
に適合させればいい。
となれば、 _SwiftValue
は本当にただのboxingの仕組みさえ提供していればいいってわけですね。
まとめ
以上、 as AnyObject
でどのようにboxingが起こるかについて見てきました。
Xcode8 Release Noteを見ると、 AnyObject周りの実装はアップデートのたびにちょくちょく調整されているようです。
今後の動向にも注目です。