Switch on Array type
Asked Answered
O

2

6

Overview

I would like to use switch statement by matching array type. I have the following classes.

Class:

class A {}    
class B : A {}

Switch on single value works:

let a : A = B()

switch a {
case let b as B:
    print("b = \(b)")
default:
    print("unknown type")
}

Switch on array (Compilation Error):

let aArray : [A] = [B(), B()]

switch aArray {
case let bArray as [B] :
    print("bArray = \(bArray)")
default:
    print("unknown type")
}

Error:

Downcast pattern value of type '[B]' cannot be used

Note: Tested on Swift 4

Question:

  • How can I achieve this ?
Otoplasty answered 21/3, 2018 at 12:45 Comment(7)
Seems to be a known issue: bugs.swift.org/browse/SR-5671, also observed here: #24356013.Cheerleader
@MartinR Thanks for that update, not sure if conditional conformance (Swift 4.1) would help in anyway but seems like an open bugOtoplasty
@Hamish Yes, I could even use is instead but then I wanted to convert it into [B] in the process. Presently I don't think there is a way to convert it in [B]Otoplasty
@Hamish my bad, I didn't read your comment properly, It does work ok. I am just puzzled. So as Any is just to suppress the compiler error ?Otoplasty
could you post it as an answer by mentioning the workaround and the bug so that it would help others facing the same issue. many thanksOtoplasty
Let us continue this discussion in chat.Otoplasty
@Otoplasty I posted an answer, so I've deleted my commentsDramatist
B
2

For anyone that has the same question, it's has been fixed in Swift 5.8.

Switch on array (original code) compiles fine.

As mentioned in Hacking with swift, you can now do this:

class Pet { }
class Dog: Pet {
    func bark() { print("Woof!") }
}

func bark(using pets: [Pet]) {
    switch pets {
    case let pets as [Dog]:
        for pet in pets {
            pet.bark()
        }
    default:
        print("No barking today.")
    }
}
Beadsman answered 25/5, 2023 at 9:23 Comment(0)
D
11

In Swift 4.1, you get a better error message (thanks to #11441):

Collection downcast in cast pattern is not implemented; use an explicit downcast to '[B]' instead

In short, you're hitting a bit of the compiler that isn't fully implemented yet, the progress of which is tracked by the bug SR-5671.

You can however workaround this limitation by coercing to Any before performing the cast:

class A {}
class B : A {}

let aArray : [A] = [B(), B()]

switch aArray /* or you could say 'as Any' here depending on the other cases */ {
case let (bArray as [B]) as Any:
  print("bArray = \(bArray)")
default:
  print("unknown type")
}

// bArray = [B, B]

Why does this work? Well first, a bit of background. Arrays, dictionaries and sets are treated specially by Swift's casting mechanism – despite being generic types (which are invariant by default), Swift allows you to cast between collections of different element types (see this Q&A for more info).

The functions that implement these conversions reside in the standard library (for example, Array's implementation is here). At compile time, Swift will try to identify collection downcasts (e.g [A] to [B] in your example) so it can directly call the aforementioned conversion functions, and avoid having to do a full dynamic cast through the Swift runtime.

However the problem is that this specialised logic isn't implemented for collection downcasting patterns (such as in your example), so the compiler emits an error. By first coercing to Any, we force Swift to perform a fully dynamic cast, which dispatches through the runtime, which will eventually wind up calling the aforementioned conversion functions.

Although why the compiler can't temporarily treat such casts as fully dynamic casts until the necessary specialised logic is in place, I'm not too sure.

Dramatist answered 21/3, 2018 at 14:56 Comment(2)
I tried this with dictionaries instead of arrays. If you have more than one case in the switch block testing if the dictionaries are of different types, in my testing the behavior may be unpredictable and not reliable, especially if the dictionary is empty. Swift will also complain that the other cases are redundant with yellow ! warnings too. So just be careful, and test this thoroughly in your implementation before committing to it.Truong
@Truong Could you please elaborate on the unpredictable behaviour you're experiencing with dictionaries? The "Case is already handled by previous patterns; consider removing it" warning you're getting is a bug that will be fixed in Swift 4.2. Also, if the dictionary is empty, it will be castable to a dictionary of any type (you get the same behaviour with empty arrays and nil optionals) – there's some discussion of this in the comments of SR-6192.Dramatist
B
2

For anyone that has the same question, it's has been fixed in Swift 5.8.

Switch on array (original code) compiles fine.

As mentioned in Hacking with swift, you can now do this:

class Pet { }
class Dog: Pet {
    func bark() { print("Woof!") }
}

func bark(using pets: [Pet]) {
    switch pets {
    case let pets as [Dog]:
        for pet in pets {
            pet.bark()
        }
    default:
        print("No barking today.")
    }
}
Beadsman answered 25/5, 2023 at 9:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.