🕸️

Swiftのas Any / AnyObjectの暗黙的な型変換について

2022/03/18に公開

スライド版

遭遇した問題

  • Objective-Cで実装されたAny型を引数に持っている関数に対してSwiftのStructを渡したら、同一のインスタンスを渡しているのにObjective-C側では別のインスタンスが渡されているという判定になってしまった。

結論

  • 独自で定義したStructやenumに対して as AnyObject を行うと__SwiftValueというObjective-Cで実装されたクラスにラップされる。
struct StructModel {}

let structModel = StructModel()
print(type(of: structModel))              // StructModel
print(type(of: structModel as AnyObject)) // __SwiftValue

// 元のstructに戻すことも可能
print(type(of: (structModel as AnyObject) as! StructModel)) // StructModel
  • IntやStringに対して as AnyObject を行うと、それぞれに対応したObjective-Cの型に変換される。
let int: Int = 0
print(type(of: int))                  // Int
print(type(of: int as AnyObject))     // __NSCFNumber

let string = ""
print(type(of: string))               // String
print(type(of: string as AnyObject))  // __NSCFConstantString

var mString = ""
mString = "hi"
print(type(of: mString))              // String
print(type(of: mString as AnyObject)) // NSTaggedPointerString
  • そして本題のObjective-Cで実装されたAny型(Objective-Cだとid型)の引数を持っている関数にSwiftのStrcutやenumを渡すと、__SwiftValueにラップされる。つまり、as AnyObject と同じことが行われる。
/* Objective-C */
@interface ObjCModel : NSObject
- (NSString *)test:(id)any;
@end

@implementation ObjCModel
- (NSString *)test:(id)any {
  return NSStringFromClass([any class]);
}
@end

/* Swift */
let structModel = StructModel()
print(ObjCModel().test(StructModel())!) // __SwiftValue
  • class型では暗黙の型変換はない。
class ClassModel {}

let classModel = ClassModel()
print(type(of: classModel))              // ClassModel
print(type(of: classModel as Any))       // ClassModel
print(type(of: classModel as AnyObject)) // ClassModel

解説

SwiftとObjective-Cの型の相互変換

  • Objective-C and Swift interoperability(相互運用性)によって、SwiftとObjective-Cの型を相互に変換が行える仕様になっている。
  • 変換はfunc _bridgeAnythingToObjectiveC<T>(_ x: T) -> AnyObjectで行われている。
    • この関数の中でObjective-Cの型に変換や__SwiftValueへのラップが行われている。
  • Objective-Cでは、id型はAny型として扱われる。
    • Swift 2では id型はAnyObject型として扱われていた。 ただし、AnyObjectだとSwiftのstructやenumが渡せない不都合があったため、Swift 3からAny型に変わった。
    • つまり、元々Objective-CでAny型を受け取る場合にはAnyObject(参照型)しか想定していないので、Swiftの値型(struct, enum)が渡された場合には適宜適切なclass型に変換またはラップされることになる。

問題が起きるケース

  • structやenumをObjective-Cに渡す時は暗黙的にclassでラップされるため、値型で同一インスタンスを渡している場合に意図しない挙動になることがある。
    • 例えば、関数内でインスタンスの同値判定するロジックが実装されている場合に、呼び出し元は同一のstructを渡しているつもりでも、関数内では異なるインスタンスと判定されてしまうので意図した挙動にならなくなる。

インスタンスが異なることを検証

  • ObjectIdentifierを使って、生成されたインスタンスの一意のIDを出してみると、同一のstructのはずなのに as AnyObject の度に違うIDになることが確認できる。
    • 以下の例で同じIDが出力されるときがあるが、これはたまたま同じメモリ領域が使いまわされたと推察される。
    • またObjectIdentifierの仕様的にも生存中のインスタンスの比較(comparisons during the lifetime of the instance.)を想定しているIDなので、すでに解放されたインスタンスと同じIDになってしまう可能性はありそう。
struct StructModel {}

let structModel = StructModel()

Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in // 1秒ごとにクロージャー内が実行される
  print("struct", ObjectIdentifier(structModel as AnyObject))
}

/*
出力されるログ

struct ObjectIdentifier(0x00006000005749c0)
struct ObjectIdentifier(0x0000600000568ce0)
struct ObjectIdentifier(0x00006000005565c0)
struct ObjectIdentifier(0x000060000055ef80)
struct ObjectIdentifier(0x00006000005557c0) // 同じID
struct ObjectIdentifier(0x000060000055dba0)
struct ObjectIdentifier(0x000060000057e300)
struct ObjectIdentifier(0x00006000005566c0)
struct ObjectIdentifier(0x00006000005789a0)
struct ObjectIdentifier(0x00006000005557c0) // 同じID
struct ObjectIdentifier(0x00006000005566c0)
struct ObjectIdentifier(0x000060000055ef80)
struct ObjectIdentifier(0x00006000005557c0) // 同じID
*/

未解決な疑問

Discussion