Weird Swift AnyObject behavior

Today, messing around with Swift, I came across a strange thing. Here was developed a unit test that shows some unexpected actions when using 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) } } 

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

As expected, after creating the TestClass instance, dropping it to AnyObject and setting another variable, access to the xyz property will not compile, because AnyObject does not have this property.

But it is surprising that a property called name is accepted by the compiler because this property is on NSException . It seems that Swift is very happy to accept any property name if it exists somewhere at runtime.

The following unexpected behavior and all that was started is that trying to access the name property returns zero. Watching various variables in the debugger, I see that typeAnyObject points to the original instance of TestClass , and the name property has the value "name".

Swift does not typeAnyObject.name error when accessing typeAnyObject.name , so I expect it to find and return a "name". But instead, I get zero.

I would be wondering if anyone can shed light on what is happening here?

My main problem is that I would expect Swift to either throw an error when accessing a property that does not exist on AnyObject , or it will find and return the correct value. Nothing is happening right now.

+2
swift runtime
source share
2 answers

As in Objective-C, where you can send arbitrary messages in id , arbitrary properties and methods can be called on the AnyObject instance in Swift. However, the data is different, and this is documented in Interacting with the Objective-C API in the "Using Swift with Cocoa and Objective-C" section.

Swift includes an AnyObject type that represents an object. This is similar to the Objective-C s id . Swift imports id as AnyObject , which allows you to write secure Swift code while maintaining the flexibility of an untyped object.
...
You can call any Objective-C method and access any property in the value of AnyObject without using a more specific class type. This includes Objective-C compatible methods and properties marked with the @objc attribute.
...
When you call a method with a value of type AnyObject , this method call behaves as an implicitly expanded optional. You can use the same optional chain syntax that you would use for additional methods in the protocols to call the method on AnyObject if necessary.

Here is an example:

 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 - implicitly expanded optional and nil if the object does not have this property.

Here is an example of 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 is an implicitly deployed optional closure. This code can be simplified with an additional chain:

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

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

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

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

 let name = typeAnyObject.name // String! 

does compilation because - as you have 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 an optional binding to safely expand the value (or check for nil ).

If your class is derived from NSObject

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

then

 let xyz = typeAnyObject.xyz // String?! 

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

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

no longer compiles. The reason is that both TestClass and NSException have a name property, but with different types ( String? Vs String ), so the type of this expression is ambiguous. This ambiguity can be resolved (optional) by dropping AnyObject back to TestClass :

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

Output:

  • You can call any method / property on an AnyObject instance if that method / property of Objective-C is compatible.
  • You need to test the implicitly deployed option against nil or use an optional binding to check if the instance is a Method / property.
  • Ambiguity arises 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, perhaps, apply it to a well-known class instead (as in the last example).

+2
source share

it has nothing with NSException!

from the Apple documentation:

protocol AnyObject {...}

A protocol to which all classes implicitly match.

When used as a specific type, all known methods and properties of @objc are available, as implicitly unwrapped optional methods and properties, respectively, for each instance of AnyObject

name - property @objc, xyz - no.

try the following :-)

let typeAnyObject = instance as Any

or

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

let instance = TestClass () instance.xyz = "xyz" instance.name = "name"

let typeAnyObject = instance as AnyObject

typeAnyObject.name// will not compile now

0
source share

All Articles