Reputation: 21883
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
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’sid
type. Swift importsid
asAnyObject
, 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 ofAnyObject
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 onAnyObject
.
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:
AnyObject
if that
method/property is Objective-C compatible.nil
or
use optional binding to check that the instance actually has that
method/property.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
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