LoginSignup
28
11

More than 5 years have passed since last update.

`as AnyObject` で何が起こるのか

Posted at

こんばんは、クリスマスまであと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.Boolas 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周りの実装はアップデートのたびにちょくちょく調整されているようです。
今後の動向にも注目です。

28
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
11