Swift AnyObject 的奇怪行为

Swift AnyObject 的奇怪行为

问题描述:

今天在和 Swift 打交道时,我遇到了一件奇怪的事情.这是我开发的单元测试,它显示了使用 Swift 的 AnyObject 时的一些意外行为.

In messing around with Swift today I came across a strange thing. Here's the unit test I developed which shows some unexpected behaviours when using Swift's AnyObject.

class SwiftLanguageTests: XCTestCase {

    class TestClass {
        var name:String?
        var xyz:String?
    }

    func testAccessingPropertiesOfAnyObjectInstancesReturnsNils() {
        let instance = TestClass()
        instance.xyz = "xyz"
        instance.name = "name"

        let typeAnyObject = instance as AnyObject

        // Correct: Won't compile because 'xyz' is an unknown property in any class.
        XCTAssertEqual("xyz", typeAnyObject.xyz)

        // Unexpected: Will compile because 'name' is a property of NSException
        // Strange: But returns a nil when accessed.
        XCTAssertEqual("name", typeAnyObject.name)

    }
}

此代码是其他一些代码的简化,其中有一个只能返回 AnyObject 的 Swift 函数.

This code is a simplification of some other code where there is a Swift function that can only return a AnyObject.

正如预期的那样,在创建了 TestClass 的实例后,将其转换为 AnyObject 并设置另一个变量,访问属性 xyz 不会编译,因为 AnyObject 没有这样的属性.

As expected, after creating an instance of TestClass, casting it to AnyObject and setting another variable, accessing the property xyz won't compile because AnyObject does not have such a property.

但令人惊讶的是,编译器接受了一个名为 name 的属性,因为 NSException 上有一个该名称的属性.Swift 似乎很乐意接受任何属性名称,只要它存在于运行时的某处即可.

But surprisingly a property called name is accepted by the compiler because there is a property by that name on NSException. It appears that Swift is quite happy to accept any property name as long as it exists somewhere in the runtime.

下一个意想不到的行为和让这一切开始的事情是尝试访问 name 属性返回 nil.观察调试器中的各种变量,我可以看到 typeAnyObject 指向原始的 TestClass 实例,它的 name 属性的值为名称".

The next unexpected behaviour and the thing that got all this started is that attempting to access the name property returns a nil. Watching the various variables in the debugger, I can see that typeAnyObject is pointing at the original TestClass instance and it's name property has a value of "name".

Swift 在访问 typeAnyObject.name 时不会抛出错误,所以我希望它找到并返回name".但相反,我得到了零.

Swift doesn't throw an error when accessing typeAnyObject.name so I would expect it to find and return "name". But instead I get nil.

如果有人能对这里发生的事情有所了解,我会很感兴趣?

I would be interested if anyone can shed some light on what is going on here?

我主要担心的是,我希望 Swift 在访问 AnyObject 上不存在的属性时抛出错误,或者找到并返回正确的值.目前两者都没有发生.

My main concern is that I would expect Swift to either throw an error when accessing a property that does not exist on AnyObject, or find and return the correct value. Currently neither is happening.

类似于 Objective-C,可以向 id 发送任意消息,可以在 AnyObject 的实例上调用任意属性和方法在斯威夫特.然而,细节有所不同,它记录在与 Objective-C API 交互在在 Cocoa 和 Objective-C 中使用 Swift"一书中.

Similar as in Objective-C, where you can send arbitrary messages to id, arbitrary properties and methods can be called on an instance of AnyObject in Swift. The details are different however, and it is documented in Interacting with Objective-C APIs in the "Using Swift with Cocoa and Objective-C" book.

Swift 包含一个表示某种对象的 AnyObject 类型.这类似于 Objective-C 的 id 类型.Swift 将 id 导入为 AnyObject,这允许您编写类型安全的 Swift 代码,同时保持无类型对象的灵活性.
...
您可以调用任何 Objective-C 方法并访问 AnyObject 值上的任何属性,而无需强制转换为更具体的类类型.这包括使用 @objc 属性标记的 Objective-C 兼容方法和属性.
...
当您对 AnyObject 类型的值调用方法时,该方法调用的行为就像一个隐式解包的可选项.您可以使用与用于协议中的可选方法相同的可选链语法来可选地调用 AnyObject 上的方法.

Swift includes an AnyObject type that represents some kind of object. This is similar to Objective-C’s id type. Swift imports id as AnyObject, which allows you to write type-safe Swift code while maintaining the flexibility of an untyped object.
...
You can call any Objective-C method and access any property on an AnyObject value without casting to a more specific class type. This includes Objective-C compatible methods and properties marked with the @objc attribute.
...
When you call a method on a value of AnyObject type, that method call behaves like an implicitly unwrapped optional. You can use the same optional chaining syntax you would use for optional methods in protocols to optionally invoke a method on AnyObject.

这是一个例子:

func tryToGetTimeInterval(obj : AnyObject) {
    let ti = obj.timeIntervalSinceReferenceDate // NSTimeInterval!
    if let theTi = ti {
        print(theTi)
    } else {
        print("does not respond to `timeIntervalSinceReferenceDate`")
    }
}

tryToGetTimeInterval(NSDate(timeIntervalSinceReferenceDate: 1234))
// 1234.0

tryToGetTimeInterval(NSString(string: "abc"))
// does not respond to `timeIntervalSinceReferenceDate`

obj.timeIntervalSinceReferenceDate 是一个隐式展开的可选并且 nil 如果对象没有那个属性.

obj.timeIntervalSinceReferenceDate is an implicitly unwrapped optional and nil if the object does not have that property.

这里有一个检查和调用方法的例子:

Here an example for checking and calling a method:

func tryToGetFirstCharacter(obj : AnyObject) {
    let fc = obj.characterAtIndex // ((Int) -> unichar)!
    if let theFc = fc {
        print(theFc(0))
    } else {
        print("does not respond to `characterAtIndex`")
    }
}

tryToGetFirstCharacter(NSDate(timeIntervalSinceReferenceDate: 1234))
// does not respond to `characterAtIndex`

tryToGetFirstCharacter(NSString(string: "abc"))
// 97

obj.characterAtIndex 是一个隐式展开的可选闭包.那个代码可以使用可选链接进行简化:

obj.characterAtIndex is an implicitly unwrapped optional closure. That code can be simplified using optional chaining:

func tryToGetFirstCharacter(obj : AnyObject) {
    if let c = obj.characterAtIndex?(0) {
        print(c)
    } else {
        print("does not respond to `characterAtIndex`")
    }
}

就您而言,TestClass 没有任何 @objc 属性.

In your case, TestClass does not have any @objc properties.

let xyz = typeAnyObject.xyz // error: value of type 'AnyObject' has no member 'xyz'

不编译,因为编译器不知道 xyz 属性.

does not compile because the xyz property is unknown to the compiler.

let name = typeAnyObject.name // String!

编译是因为——正如你注意到的——NSException 有一个 name 属性.然而,该值是 nil 因为 TestClass 没有Objective-C 兼容 name 方法.如上所述,您应该使用可选绑定以安全地解包值(或针对 nil 进行测试).

does compile because – as you noticed – NSException has a name property. The value however is nil because TestClass does not have an Objective-C compatible name method. As above, you should use optional binding to safely unwrap the value (or test against nil).

如果你的类是从 NSObject

class TestClass : NSObject {
    var name : String?
    var xyz : String?
}

然后

let xyz = typeAnyObject.xyz // String?!

编译.(或者,使用 @objc 标记类或属性.)但是现在

does compile. (Alternatively, mark the class or the properties with @objc.) But now

let name = typeAnyObject.name // error: Ambigous use of `name`

不再编译.原因是 TestClassNSException有一个 name 属性,但具有不同的类型(String? vs String),所以那个表达式的类型是不明确的.这种歧义只能是通过(可选)将 AnyObject 转换回 TestClass 来解决:

does not compile anymore. The reason is that both TestClass and NSException have a name property, but with different types (String? vs String), so the type of that expression is ambiguous. This ambiguity can only be resolved by (optionally) casting the AnyObject back to TestClass:

if let name = (typeAnyObject as? TestClass)?.name {
    print(name)
}

结论:

  • 您可以在 AnyObject 的实例上调用任何方法/属性,如果方法/属性与 Objective-C 兼容.
  • 您必须针对 nil 或使用可选绑定来检查实例是否确实具有方法/属性.
  • 如果多个类具有(Objective-C)兼容,则会出现歧义名称相同但类型不同的方法.
  • You can call any method/property on an instance of AnyObject if that method/property is Objective-C compatible.
  • You have to test the implicitly unwrapped optional against nil or use optional binding to check that the instance actually has that method/property.
  • Ambiguities arise if more than one class has (Objective-C) compatible methods with the same name but different types.

特别是因为最后一点,我会尽量避免这种情况如果可能,机制,并可选择强制转换为已知类(如上一个例子).

In particular because of the last point, I would try to avoid this mechanism if possible, and optionally cast to a known class instead (as in the last example).