任何对象如何符合NSObjectProtocol?

10

这个问题的灵感来自于mz2在问题Check for object type fails with "is not a type" error上的回答。

考虑一个空的Swift类:

class MyClass { }
尝试在此类的实例上调用任何NSObjectProtocol方法都将导致编译时错误:
let obj = MyClass()
obj.isKindOfClass(MyClass.self) // Error: Value of type 'MyClass' has no member 'isKindOfClass'

然而,如果我将实例转换为AnyObject,那么我的对象现在符合NSObjectProtocol,并且我可以调用协议定义的实例方法:

let obj: AnyObject = MyClass()
obj.isKindOfClass(MyClass.self) // true
obj.conformsToProtocol(NSObjectProtocol) // true
obj.isKindOfClass(NSObject.self) // false

我的对象没有继承自NSObject,但仍符合NSObjectProtocol。那么AnyObject如何符合NSObjectProtocol

4个回答

6
在Cocoa/Objective-C的世界中,AnyObject就是id类型。将此对象转换为AnyObject后,您可以向其发送任何已知的Objective-C消息,例如isKindOfClass或conformsToProtocol。现在,当您说isKindOfClass或conformsToProtocol时,您不再处于Swift世界中;您正在使用Objective-C与Cocoa交流。因此,请考虑Objective-C如何看待此对象。Objective-C世界中的所有类都是从某个基类派生出来的;像MyClass这样的无基类类是不可能的。而且,Objective-C世界中的每个基类都符合NSObject协议(Swift称之为NSObjectProtocol);这就是成为(或从)基类下降的方式!因此,为了将其带入Objective-C世界,Swift将MyClass表示为从特殊的桥接基类SwiftObject下降,该基类确实符合NSObjectProtocol(如您可以在此处看到:https://github.com/apple/swift/blob/master/stdlib/public/runtime/SwiftObject.mm)。

1
但是 AnyObject 被定义为 @objc public protocol AnyObject { },这是否意味着所有的 @objc 协议都符合 NSObjectProtocol?那 Any 呢? - JAL
这并不意味着那样。我说的是我说的,不是别的什么。 - matt
1
你是在说“像 MyClass 这样没有基础的类是不可能的”。那么,你是在说 MyClass 继承自 SwiftObject 吗?为什么只有将其转换为 AnyObject 才能暴露出符合 NSObjectProtocol 的内容? - JAL
因为只有将其转换为任何对象才能使用“符合协议”的方法。 - matt
1
好的,我觉得我开始慢慢明白你在说什么了。“将此对象转换为 AnyObject 后,您可以向其发送任何已知的 Objective-C 消息,例如“isKindOfClass”或“conformsToProtocol”。 ” 那么这是如何完成的呢?协议一致性是在哪里指定的?Foundation 中是否有 AnyObject 的扩展程序,将其符合 NSObjectProtocol?还是全部都是通过 SwiftObject 完成的? - JAL
2
疯了。在这种情况下,即使该类本身不是Objective-C可见类型并且不使用基于消息的分派来执行自己的方法,isKindOfClass实际上也作为动态分派消息发送了吗? - mz2

6
如果我基于matt's的回答正确理解,这种情况只有在Swift/Objective-C互操作可用时才有效,因为实际上Swift类类型最终继承自SwiftObject,当Objective-C互操作被编译时,它实际上涉及Objective-C类(SwiftObject在SwiftObject.mm中实现,当使用Objective-C互操作时编译为Objective-C++)。因此,将Swift类类型对象强制转换为AnyObject类型会“泄漏”这些信息。
Swift源代码的实现中查看一些相关部分,文件swift/stdlib/public/runtime/SwiftObject.mm
#if SWIFT_OBJC_INTEROP

// …

@interface SwiftObject<NSObject> {
   SwiftObject_s header;
}

// …

@implementation SwiftObject

// …

- (BOOL)isKindOfClass:(Class)someClass {
  for (auto isa = _swift_getClassOfAllocated(self); isa != nullptr;
       isa = _swift_getSuperclass(isa))
    if (isa == (const ClassMetadata*) someClass)
      return YES;

  return NO;
}

// …

// #endif

正如预测的那样,在Linux中使用Swift 3(据我所知,Swift运行时和Foundation实现中没有Objective-C运行时可用),这个问题的示例代码和启发了这个问题的早期问题和答案会导致编译器错误:

ERROR […] value of type 'AnyObject' has no member 'isKindOfClass'

4

在这些优秀的答案中,我希望添加一些额外的信息。

我创建了三个程序,并查看了每个程序生成的汇编代码:

obj1.swift

import Foundation
class MyClass { }
let obj = MyClass()

obj2.swift

import Foundation
class MyClass { }
let obj: AnyObject = MyClass()

obj3.swift

import Foundation
class MyClass { }
let obj: AnyObject = MyClass()
obj.isKindOfClass(MyClass.self)

obj1和obj2之间的差异微不足道。任何涉及对象类型的指令都具有不同的值:

movq    %rax, __Tv3obj3objCS_7MyClass(%rip)

# ...

globl   __Tv3obj3objCS_7MyClass         .globl  __Tv3obj3objPs9AnyObject_
.zerofill __DATA,__common,__Tv3obj3objCS_7MyClass,8,3

# ...

.no_dead_strip  __Tv3obj3objCS_7MyClass

vs

movq    %rax, __Tv3obj3objPs9AnyObject_(%rip)

# ...

.globl  __Tv3obj3objPs9AnyObject_
.zerofill __DATA,__common,__Tv3obj3objPs9AnyObject_,8,3

# ...

.no_dead_strip  __Tv3obj3objPs9AnyObject_

完整的差异请点击这里.

我觉得这很有趣。如果两个文件之间唯一的区别是对象类型的名称,为什么声明为AnyObject的对象可以执行Objective-C选择器?

obj3展示了如何触发isKindOfClass:选择器:

LBB0_2:
    # ...
    movq    __Tv3obj3objPs9AnyObject_(%rip), %rax
    movq    %rax, -32(%rbp)
    callq   _swift_getObjectType
    movq    %rax, -8(%rbp)
    movq    -32(%rbp), %rdi
    callq   _swift_unknownRetain
    movq    -24(%rbp), %rax
    cmpq    $14, (%rax)
    movq    %rax, -40(%rbp)
    jne LBB0_4
    movq    -24(%rbp), %rax
    movq    8(%rax), %rcx
    movq    %rcx, -40(%rbp)
LBB0_4:
    movq    -40(%rbp), %rax
    movq    "L_selector(isKindOfClass:)"(%rip), %rsi
    movq    -32(%rbp), %rcx
    movq    %rcx, %rdi
    movq    %rax, %rdx
    callq   _objc_msgSend
    movzbl  %al, %edi
    callq   __TF10ObjectiveC22_convertObjCBoolToBoolFVS_8ObjCBoolSb
    movq    -32(%rbp), %rdi
    movb    %al, -41(%rbp)
    callq   _swift_unknownRelease
    xorl    %eax, %eax
    addq    $48, %rsp

# ...

LBB6_3:
    .section    __TEXT,__objc_methname,cstring_literals
"L_selector_data(isKindOfClass:)":
    .asciz  "isKindOfClass:"

    .section    __DATA,__objc_selrefs,literal_pointers,no_dead_strip
    .align  3
"L_selector(isKindOfClass:)":
    .quad   "L_selector_data(isKindOfClass:)"

obj2和obj3之间的差异请这里查看。

isKindOfClass被发送为动态调度方法,如_objc_msgSend所示。两个对象都以SwiftObject(.quad _OBJC_METACLASS_$_SwiftObject)的形式暴露给Objective-C,将对象类型声明为AnyObject完成了到NSObjectProtocol的桥接。


现在我开始思考的另一个完全独立的问题是,为什么import Foundation会产生影响:如果不导入Foundation,则将Swift类类型对象强制转换为AnyObject会引发编译器错误。在所有这些中,Objective-C互操作的使用是否基于是否使用Foundation进行调节? - mz2

1
除了matt's的答案,我认为是正确的:
在这种情况下,isKindOfClass实际上被发送为动态分派消息,即使该类本身不是Objective-C可见类型,并且不使用基于消息的分派来进行其自己的方法。不,isKindOfClass作为动态分派方法被发送,因为该类本身是Objective-C可见类型,并且使用基于消息的分派进行其自己的方法。
它之所以这样做,是因为@objc public protocol AnyObject {}中的@objc
如果您在XCode中cmd-click AnyObject,则会在生成的标头中看到此内容。
/// When used as a concrete type, all known `@objc` methods and 
/// properties are available, as implicitly-unwrapped-optional methods 
/// and properties respectively, on each instance of `AnyObject`.

https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html文档中提到:
为了让Swift类在Objective-C中可访问和可用,它必须是Objective-C类的后代或者被标记为@objc。
(我强调的部分)
采用带有@objc标签的协议意味着您的类是一个@objc类,并且通过mz2在上面的答案中指出的互操作机制进行ObjC桥接。

没错,但是令人惊讶的是,尽管有所有这些记录的信息,这仍然适用于未标记为@objc且未以(已记录的方式)从Objective-C类继承的Swift类。实际上,它们并不总是这样(AnyObject也存在于语言中,而无需Objective-C互操作)。 - mz2
每当您使用AnyObject时,都会调用ObjC交互,正如上面引用的文档所述。 - Steve Trewick
当没有ObjC互操作编译到您的Swift副本中时,AnyObject也存在。例如,在Linux上,class A {}; print(A() as AnyObject)在Swift 3中完全正常工作。我想我们只是在表达同样的观点:它在ObjC可互操作平台上这样做,因为未记录的基类是具有ObjC互操作性的NSObjectProtocol兼容的Objective-C类,并且该效果(方法和属性可用作隐式解包选项)已记录,正如您所指出的那样。 - mz2
这是一个很好的观点,我忽略了在Linux上运行的情况,因为那里没有ObjC运行时依赖(目前Swift 2.2在Apple平台上有这个依赖)。 - Steve Trewick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接