0

Why must I downcast an array item in Swift, if the sub-class of the array item is known?

 > class B {let t: String = "-B-"}
 > class B1:B {let t1: String = "-B1-"}
 > class B2:B {let t2: String = "-B2-"}
 > let bunch = [B(), B1(), B2()]

As expected:

 > print(type(of:bunch))
 Array<B>

The sub-class of each element of the array is known:

 > for item in bunch {print(type(of:item))}
 B
 B1
 B2

Yet (as documented) we cannot access the item's sub-class member, and we have to down-class:

> if bunch[1] is B1 {let b1 = bunch[1] as! B1; print(b1.t1)} 
-B1-

because this does not work:

> bunch[1].t1
error: repl.swift:17:6: error: value of type 'B' has no member 't1'; did you mean 't'?

Why can Swift determine the sub-class using type(of:) but it cannot infer it when accessing a member in that sub-class? Is it a bug, a historic hang-over, or am I missing something?

P2000
  • 1,042
  • 8
  • 14
  • 4
    How should the *compiler* know that `bunch[1]` is a `B1` (but `bunch[2]` is not)? – Martin R Jan 21 '20 at 20:11
  • @MartinR yes now that makes sense. Within a loop `if item is B1 {let b1 = item as?B1; print(b1.t1)}` is valid because b1 is cast as a B1 and so it has b1.t1, while item, as compiled but not run, may or may not have a t1. Great insight, thanks! – P2000 Jan 21 '20 at 20:23

2 Answers2

1

The simple answer is that once you declare the array of type B then the compiler treats all the elements in the array to be of type B and will not automatically infer methods based on any subclasses present. It seems you are wondering why the compiler/array is not smart enough to determine the class of a let declared array, when in theory it could be. I'm not sure many languages support such a feature, in any event you can do the same thing by simply unwrapping it like you did, since YOU know it in this case.

Alex
  • 3,861
  • 5
  • 28
  • 44
  • Thanks Alex. "when in theory it could be" is what I initially thought too, but in fact it cannot be determined at compile time, as Martin writes in his comment. "I'm not sure many languages support such a feature": indeed, I think it is for the realm of interpreted/scripted languages, where it is irrelevant whether a loop item has a .t1 member or not, until the loop is run and that point in code is reached. – P2000 Jan 21 '20 at 20:30
1

Swift arrays are collections of items of a single type. The type of the array

let bunch = [B(), B1(), B2()]

is inferred as [B] because B is the “nearest common superclass” of the given three elements. Consequently, bunch[1] has the type B and not B1. That is why bunch[1].t1 does not compile.

Of course you can print the actual type of an array element at runtime with type(of:), or check it against B1. The latter is better done with optional binding:

if let b1 = bunch[1] as? B1 {
    print(b1.t1)
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Perhaps one addition: the array elements must have a same type that can be determined at compile time. This is a fundamental requirement, so that iterators can operate with predetermined types. In my particular case it is inferred from the "let" literal (If I used var, I would have had to set it to type [B] in order to house B, B1 and B2) – P2000 Jan 21 '20 at 22:08
  • 1
    @P2000: It makes no difference if the array is `let` or `var`: If you don't annotate the type explicitly then it is inferred from the context, i.e. from the right-hand side. – Martin R Jan 21 '20 at 22:09
  • Yes. What I meant was that this case is pronounced if the elements are not literal (so not as in the example's let - or a similar var), but -as you clarified- are inserted into the array during runtime. I obscured it a bit due to the literal, and that was precisely how it became a question for me. Thank you for your help today. – P2000 Jan 22 '20 at 01:22