#TL;DR
-
Any型(Objective-Cで言うところのid型)にProtocolに準拠した値型オブジェクトを
as AnyObject
を行って格納し、さらにそのAny型のオブジェクトをProtocolにキャストしようとすると失敗する。 -
格納したAny型のオブジェクトを一度
as AnyObject
してから、Protocolへキャストすると成功する。 -
これはObjective-CとSwiftの間でのブリッジの挙動によって起こる。
-
Swift3の頃からIssueに上げられているが現在(Xcode10.2, Swift5)でも修正されていない。
#何が起こったか
Any型のオブジェクトからプロトコルにキャストしようとしたところ、プロトコルに準拠した値が入っているのにも関わらず、キャストに失敗してしまいました。
以下に簡単に再現できるソースを貼るのでPlayground等で試してみてください。
import Foundation
protocol Animal {}
struct Dog: Animal {}
let anyAnimal: Any = Dog() as AnyObject
print(anyAnimal as? Animal) // nil
anyAnimal
には、Animalに準拠しているDogの値が入っているにも関わらず、キャストに失敗してnilが出力されてしまいます。
#SR-3871
これはSwift側のバグなのかと思い、調べてみると以下のIssueを発見しました。
[SR-3871]Protocol passing via objective-c / Any can't be cast back to protocol type
このIssueは、Objective-CのAny型に格納されたProtocolに準拠した値オブジェクトをProtocolにキャストしてAny型から戻そうとすると失敗するという内容に関してのIssueです。
これは現在(Xcode10.2, Swift5)でも解決されていません。
コメントを見てみるとSwiftの値型オブジェクトをObjective-Cで扱うためにas AnyObject
でキャストをした状態でAny型プロパティに格納すると、Protocolへの準拠の判定ができなくなるために、キャストできるかどうかの判定で弾かれるということのようです。
#解決策
一度as AnyObject
を挟んでからキャストすると、Protocolへのキャストも成功します。
上記のコードであれば以下のような対応を行えば正しくProtocolにキャストされます。
print((anyAnimal as AnyObject) as! Animal) // __lldb_expr_11.Dog
もしくは、Dogをclassに変更することでも、正しくキャストする事ができるようになります。
import Foundation
protocol Animal {}
class Dog: Animal {} // struct → class
let dog = Dog()
let anyAnimal: Any = dog as AnyObject
print(anyAnimal as! Animal) // __lldb_expr_11.Dog
次はどうして、AnyObjectにキャストしたり、Dogをstructからclassに変更するとProtocolへのキャストが成功するのかを探っていきましょう。
#原因
原因にたどり着くためには、まずas AnyObject
の挙動について理解する必要があります。
##as AnyObject
の挙動
as AnyObject
は、Swiftの値型オブジェクトをObjective-Cで扱うために、クラスのインスタンスに変換する機能を持ちます。
クラスのインスタンスに変換するということを詳細に説明すると、
-
クラスの型に行った場合は何もしない
-
Bridged value typesに行った場合は、
_ObjectiveCBridgeable
プロトコルで指定された型に自動で変換される- Bridged value typesであるDictionary、Arrayなどの型は、
_ObjectiveCBridgeable
によってObjective-Cで扱う際の型を指定しているので、NSDictionaryやNSArrayなどの型として扱う事できるといった感じです。
- Bridged value typesであるDictionary、Arrayなどの型は、
-
それ以外の場合に行った場合は、Immutableのクラスとして変換する
- その際 _SwiftValue というクラスに変換される
as AnyObject
に関しては、以前に@takasekさんが「as AnyObject
で何が起こるのか」という素晴らしい記事を書かれているので、それを読めば、より詳細に理解できるかと思います。
##_SwiftValue in Any
as AnyObject
の挙動が理解できたところで、最初に挙げたコードを見ていきましょう。
import Foundation
protocol Animal {}
struct Dog: Animal {}
let anyAnimal: Any = Dog() as AnyObject
print(type(of: anyAnimal)) // _SwiftValue
Dog()は値型であり、_ObjectiveCBridgeable
に準拠しているわけでもないのでas AnyObject
した時点で、_SwiftValue
に変換されて、Objective-Cの領域であるAnyに格納されます。
もちろんprint(type(of: anyAnimal))
では、_SwiftValue
が出力されます。
// Any(_SwiftValue) → Protocol
print(anyAnimal as? Animal) // nil
// Any(_SwiftValue) → Struct
print(anyAnimal as! Dog) // __lldb_expr_11.Dog
// Any(_SwiftValue) → AnyObject(_SwiftValue) → Protocol
print((anyAnimal as AnyObject) as! Animal) // __lldb_expr_11.Dog
以上のキャストの結果を見ると、Objective-Cの領域のAny(id)型
に格納されている_SwiftValue
に対してProtocolに準拠しているかどうかを判定できないがゆえに、キャストもできないという事のようです。
よって、解決法で挙げたようにSwiftの領域にあるAnyObject型
に一度変換する事で、それを回避する事ができるようになります。
class
as AnyObject
はclassに対しては何も行わないというのは先ほど述べた通りです。
import Foundation
protocol Animal {}
class Dog: Animal {}
let dog = Dog()
let anyAnimal: Any = dog as AnyObject
print(type(of: anyAnimal)) // Dog
print(anyAnimal as! Animal) // __lldb_expr_11.Dog
as AnyObject
をDogに対して行っても、何も行われないため、_SwiftValue
に変換されず、Any型に格納されていてもProtocolへのキャストに失敗する事がなくなります。
#どういった場合に起こり得るか
今日では、Swiftが浸透しているため、Objective-Cとのブリッジを特に意識する必要はあまりなく、今回述べたような状況に陥ることはほとんどないのではないかと思います。
しかし、実際のところFoundationやOSSのライブラリがObjective-Cで作成されていることで、それを意識していないと、思わぬ落とし穴にはまる事があります。
Notification がその最たる例でしょう。
##例:Notification
以下が、サンプルです。
import Foundation
protocol Animal {}
struct Dog: Animal {}
final class HogeClass {
init() {
// Notificationを登録する
NotificationCenter.default.addObserver(self,
selector: #selector(self.didReceiveNotification(_:)),
name: .init("Hoge"),
object: nil)
}
// Notificationを受け取る
@objc
private func didReceiveNotification(_ sender: Notification) {
print(type(of: sender.object!)) // _SwiftValue
print(sender.object as? Animal) // nil
}
}
let hoge = HogeClass()
// Notificationを投げる
NotificationCenter.default.post(name: .init("Hoge"), object: Dog())
NotificationCenterがpostする際の引数のobject(Any型)
にDogのインスタンスを指定します。
実際に受け取ったHogeClassのprivate func didReceiveNotification(_ sender: Notification)
のsender.object
は、内部でas AnyObject
が行われているためか、_SwiftValue
として受け取らざるを得なくなり、Animalにキャストしようとすると失敗し、先ほどの解決法を使用することになります。
#個人的感想
こういったことから、開発中のアプリのソースコードにSwiftとObjective-Cが共存していないとしても、フレームワークやライブラリにObjective-Cが存在している限りは、それらとSwiftとのブリッジの意識は頭の片隅に置いておいたほうが良いでしょう。