Search code examples
iosswiftcastinganyanyobject

Can't perform methods of objects stored in Array[Any]


I want to store objects of different types in an array. The program below is only a minimum demo. In the anyArray:[Any] an instance of Object1 is stored. The print statement prints out the expected object type. In the following line the test of the stored object's type returns true. This means, during run time the correct object type is known and every thing seems to be fine.

    class Object1 {
        var name = "Object1"
    }

    var anyArray:[Any] = [Object1()]
    print("\(type(of: anyArray[0]))")
    let testResult = anyArray[0] is Object1
    print("Test result:\(testResult)")
    //print("Name:\((anyArray[0]).name)")

Console output:
   Object1
   Test result:true

However, if I try to print out the name property of the object, I get an error message from the editor:

Value of type 'Any' has no member 'name'

Well, at compile time the object's type is unknown. That's why the compiler complains. How can I tell the compiler that it is OK to access the properties of the stored object?


Solution

  • The difference comes from the difference from Type Checking in:

    • runtime, or
    • compile time

    The is operator checks at runtime whether the expression can be cast to the specified type. type(of:) checks, at runtime, the exact type, without consideration for subclasses.

    anyArray[0].name doesn't compile since the Type Any doesn't have a name property.

    If you're sure anyArray[0] is an Object1, you could use the downcast operator as!:

    print("\((anyArray[0] as! Object1).name)")
    

    To check at runtime if an element from anyArray could be an Object1 use optional binding, using the conditional casting operator as?:

    • if let:

      if let object = anyArray[0] as? Object1 {
          print(object.name)
      }
      
    • Or use the guard statement, if you want to use that object in the rest of the scope:

      guard let object = anyArray[0] as? Object1 else {
          fatalError("The first element is not an Object1")
      }
      print(object.name)
      

    If all objects in your array have a name property, and you don't want to go through all the hoops of optional binding repeatedly, then use a protocol. Your code will look like this:

    protocol Named {
        var name: String {get set}
    }
    
    class Object1: Named {
        var name = "Object1"
    }
    
    var anyArray:[Named] = [Object1()]
    print("\(type(of: anyArray[0]))")
    let testResult = anyArray[0] is Object1
    print("Test result:\(testResult)")
    print("Name:\(anyArray[0].name)")
    

    Notice that anyArray is now an array of Named objects, and that Object1 conforms to the Named protocol.

    To learn more about protocols, have a look here.