drekka
drekka

Reputation: 21883

The strange behaviour of Swift's 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)

    }
}

This code is a simplification of some other code where there is a Swift function that can only return a 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.

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.

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 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?

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.

Upvotes: 2

Views: 578

Answers (2)

Martin R
Martin R

Reputation: 539815

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 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.

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 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 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`")
    }
}

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 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).

If your class is derived from NSObject

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

then

let xyz = typeAnyObject.xyz // String?!

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

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

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)
}

Conclusion:

  • 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).

Upvotes: 3

user3441734
user3441734

Reputation: 17544

it has nothing with NSException!

from Apple documentation:

protocol AnyObject { ... }

The protocol to which all classes implicitly conform.

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

name is @objc property, xyz is not.

try this :-)

let typeAnyObject = instance as Any

or

@objc class 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

Upvotes: 0

Related Questions