小编典典

Swift的AnyObject的奇怪行为

swift

今天在与Swift交往时,我遇到了一件奇怪的事情。这是我开发的单元测试,显示了使用Swift的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)

    }
}

这段代码简化了一些其他代码,其中只有一个Swift函数只能返回AnyObject

不出所料,在创建的实例之后TestClass,将其强制转换为AnyObject并设置另一个变量,xyz由于AnyObject没有这样的属性,因此无法编译该属性。

但令人惊讶的name是,编译器接受了一个名为的属性,因为on上有该名称的属性NSException。看起来,只要它在运行时的某个地方存在,Swift很乐意接受任何属性名称。

下一个意外行为以及引起所有这些后果的是,尝试访问该name属性将返回nil。观察调试器中的各种变量,我可以看到它typeAnyObject指向原始TestClass实例,并且它的name属性值为“
name”。

Swift在访问时不会引发错误,typeAnyObject.name因此我希望它能够找到并返回“名称”。但是相反,我得到零。

如果有人可以阐明这里发生的事情,我将很感兴趣。

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


阅读 289

收藏
2020-07-07

共1个答案

小编典典

与Objective-C中类似,您可以在其中发送任意消息id,可以AnyObject 在Swift
的实例上调用任意属性和方法。但是,细节有所不同,并且 在“将Swift与Cocoa和Objective-C一起使用”一书中记录了 与Objective-C
API的交互

Swift包含AnyObject代表某种对象的类型。这类似于Objective-
C的id类型。Swift导入idAnyObject,这使您可以在保持无类型对象灵活性的同时编写类型安全的Swift代码。

您可以调用任何Objective-C方法并访问AnyObject值上的任何属性,而无需转换为更特定的类类型。这包括与Objective-
C兼容的方法和标有@objc属性的属性。

当您在AnyObject类型的值上调用方法时,该方法调用的行为类似于隐式展开的可选。您可以使用与协议中的可选方法相同的可选链接语法,以选择性地调用上的方法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对象,如果对象不具有该属性。

这是检查和调用 方法 的示例:

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是一个隐式展开的可选闭包。可以使用可选链接简化该代码:

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

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

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

无法编译,因为该xyz属性对于编译器是未知的。

let name = typeAnyObject.name // String!

进行编译是因为-如您所注意到- NSException具有name属性。但是,该值是nil因为TestClass没有与
Objective-C兼容的 name方法。如上所述,您应该使用可选绑定来安全地解包值(或针对进行测试nil)。

如果您的班级来自 NSObject

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

然后

let xyz = typeAnyObject.xyz // String?!

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

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

不再编译。其原因是,这两个TestClassNSException 具有name属性,但具有不同类型(String?VS
String),使表达式的类型是不明确的。这种歧义只能通过(可选)将其强制转换AnyObjectTestClass

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

结论:

  • 您可以在实例的任何方法/属性AnyObject是否与Objective-C兼容的情况下调用该方法/属性。
  • 您必须针对隐式展开的可选对象进行测试,nil或使用可选绑定来检查实例是否确实具有该方法/属性。
  • 如果不止一个类具有(Objective-C)兼容的名称相同但类型不同的方法,则会产生歧义。

特别是由于最后一点,如果可能的话,我将尽量避免使用此机制,而是可选地将其强制转换为已知的类(如上例所示)。

2020-07-07