今天在与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。
AnyObject
不出所料,在创建的实例之后TestClass,将其强制转换为AnyObject并设置另一个变量,xyz由于AnyObject没有这样的属性,因此无法编译该属性。
TestClass
xyz
但令人惊讶的name是,编译器接受了一个名为的属性,因为on上有该名称的属性NSException。看起来,只要它在运行时的某个地方存在,Swift很乐意接受任何属性名称。
name
NSException
下一个意外行为以及引起所有这些后果的是,尝试访问该name属性将返回nil。观察调试器中的各种变量,我可以看到它typeAnyObject指向原始TestClass实例,并且它的name属性值为“ name”。
typeAnyObject
Swift在访问时不会引发错误,typeAnyObject.name因此我希望它能够找到并返回“名称”。但是相反,我得到零。
typeAnyObject.name
如果有人可以阐明这里发生的事情,我将很感兴趣。
我主要担心的是,我希望Swift在访问上不存在的属性时抛出错误AnyObject,或者找到并返回正确的值。目前都没有发生。
与Objective-C中类似,您可以在其中发送任意消息id,可以AnyObject 在Swift 的实例上调用任意属性和方法。但是,细节有所不同,并且 在“将Swift与Cocoa和Objective-C一起使用”一书中记录了 与Objective-C API的交互。
id
Swift包含AnyObject代表某种对象的类型。这类似于Objective- C的id类型。Swift导入id为AnyObject,这使您可以在保持无类型对象灵活性的同时编写类型安全的Swift代码。 … 您可以调用任何Objective-C方法并访问AnyObject值上的任何属性,而无需转换为更特定的类类型。这包括与Objective- C兼容的方法和标有@objc属性的属性。 … 当您在AnyObject类型的值上调用方法时,该方法调用的行为类似于隐式展开的可选。您可以使用与协议中的可选方法相同的可选链接语法,以选择性地调用上的方法AnyObject。
Swift包含AnyObject代表某种对象的类型。这类似于Objective- C的id类型。Swift导入id为AnyObject,这使您可以在保持无类型对象灵活性的同时编写类型安全的Swift代码。 … 您可以调用任何Objective-C方法并访问AnyObject值上的任何属性,而无需转换为更特定的类类型。这包括与Objective- C兼容的方法和标有@objc属性的属性。 …
@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对象,如果对象不具有该属性。
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是一个隐式展开的可选闭包。可以使用可选链接简化该代码:
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
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`
不再编译。其原因是,这两个TestClass和NSException 具有name属性,但具有不同类型(String?VS String),使表达式的类型是不明确的。这种歧义只能通过(可选)将其强制转换AnyObject为TestClass:
String?
String
if let name = (typeAnyObject as? TestClass)?.name { print(name) }
结论:
特别是由于最后一点,如果可能的话,我将尽量避免使用此机制,而是可选地将其强制转换为已知的类(如上例所示)。