Checking type of Any using switch statement vs sequence of "if" statements

The Swift Programming book shows an example of using a switch statement to check if an Any is holding a particular type:

switch thing {
   let x as T1:  <code>
   let x as T2: <code>
}

Is the above any more efficient than just writing a sequence of if statement? I.e.:

if let thing as? T1 { <code> }
else if let thing as? as T2  { <code> }
else ...

I would just say no. I prefer writing switch statements when possible instead of if statements because it creates visually a nicer block of code.

For example:

switch cell {
case let cell as MyTableViewCell:
  configureMyTableViewCell(cell)
case let cell as OtherCell:
  configureOtherCell(cell)
default:
  assertionFailure(“Unknown cell dequeued \(type(of: cell))“)
}

So if I was in C++, and I had a type-erased container where I could get back a std::type_info, I'd use a map of some sort to use that std::type_info as a key and do a fast lookup to get back a function i could call which would do the proper work, rather than sequentially trying each cast, to discover what I want to do.

Is there anything similar one could do in Swift, to essentially lookup the type of the thing inside the Any as a key to look up a closure in a map, keyed by that returned type information? I'm concerned that if I have (in my particular use case) 10-12 different types to check every time, this might be a bit slow.

I don't think that the switch statement does a sequential evaluation, unlike chain of if statements (which can be optimized as well). Most likely uses a jump table of some sort.

I‘m no C++ developer (yet), so forgive me for the lack of my knowledge. As far as I can understand your question, you look for a way to map a type to a closure for example inside a dictionary?

Here are some hidden Swift quirks. Swift has metatypes expressed as T.Type (there are traps like T.Protocol for protocols or existentials in generic context when trying to obtain the metatype but we leave them aside for now).

Metatypes do not conform to any protocols so they are not hashable which makes it not possible to create a dictionary like [Any.Type: () -> Void]. But you can easily workaround that problem by using ObjectIdentifier, which is hashable.

To extract the bottom type from the upcasted instace of Any, you can use type(of:) function which will return Any.Type then instantiate ObjectIdentifier with it and extract the closure.

Here is an example:

class A {}
class B: A {}

typealias Work = () -> Void

let dictionary: [ObjectIdentifier: Work] = [
  ObjectIdentifier(A.self): { print("A") },
  ObjectIdentifier(B.self): { print("B") }
]

let x = A.self
let y = B.self
let any_x: Any.Type = x
let any_y: Any.Type = y
let x_o = ObjectIdentifier(x)
let y_o = ObjectIdentifier(y)
let x_any_o = ObjectIdentifier(any_x)
let y_any_o = ObjectIdentifier(any_y)
x_o == x_any_o // true
y_o == y_any_o // true

let a = A()
let b = B()
let b_as_a: A = b

let any_a: Any = a
let any_b: Any = b

let extracted_type_from_any_a = type(of: any_a)
let extracted_type_from_any_b = type(of: any_b)
let extracted_type_from_b_as_a = type(of: b_as_a)

dictionary[ObjectIdentifier(extracted_type_from_any_a)]?()  // prints "A"
dictionary[ObjectIdentifier(extracted_type_from_any_b)]?()  // prints "B"
dictionary[ObjectIdentifier(extracted_type_from_b_as_a)]?() // prints "B"

For other boxing techniques that can restore some typesafety check out this amazing implementation of AnyHashable box:

Have fun hacking with Swift ;)


P.S: type(of:) function is pure compiler magic in the stdlib and is not even called as it would just trap, that is due to the mentioned issues with T.Protocol metatypes. I wish that we can fix that issue one day, but it's a different story.

(Smacks forehead). All I needed for you to say was "type(of: someAny)". I was stupidly thinking it would return "dude, it's an Any!" but of course it returns the type of the underlying object.

That's all one needs, and is the equivalent of what I was saying in C++ about getting a std::type_info. Armed with that, I can indeed look up the work function in a dictionary.

But should I? switch statements in C/C++ do indeed do some sort of a jump table thing, but that's basically just mapping an int to something else. Is the Swift compiler smart enough to do something equivalent for cases where you're casting the contents of an Any? If so, I'd happily use the switch formulation because I can enumerate all the type cases in my switch. If it evaluates sequentially though, I'd opt for a map.

I guess the right answer is write both, time them, and see which is better. I should probably give that a try and report back!


Reporting back: I made a map with 10 types, and compared it to using a switch statement that enumerated the 10 casts as cases in the switch.

If the type passed in is the first case in the switch statement, the running time was 0.43 seconds (over some large number of iterations). If the type was the last case of the switch statement, running time was 4.3 seconds.

In the map case, running time was approx 2.3 seconds, no matter what the type was.

4 Likes

Actually, I found a far more effective way to do this using switch, which beats the pants off of a map, and is independent of the switch order. The switch statement I referenced above (with running time between 0.43 and 4.3, depending on what's in a seconds for a few million tests) looked like this:

func testCastSwitch(_ a: Any) -> Bool {
    switch a {
      case let x as Bool:
         <code>
      case let x as Int:
        <code>
      ...
     }
 }

A version which brings the running time to 0.07 seconds for the same run count, and doesn't vary its time as you vary what's in a looks like this:

func testCastSwitch2(_ a: Any) -> Bool {
    let t = ObjectIdentifier(type(of: a))
    switch t {
      case ObjectIdentifier(Bool.self):
         <code>
      case ObjectIdentifier(Int.self):
          <code>
       ...
     }
}

Not really equivalent, since you no longer have access to actual values. But if all you need to to change behavior based on type then sure, nice optimization.

It also won’t respect the class hierarchy or bridge foundation types.

But in cases where that doesn’t matter, it is pretty clever.

Can you check if this would produce the same result?

func testCastSwitch3(_ a: Any) -> Bool {
  let t = type(of: a)
  switch t {
  case is Bool.Type:
    <code>
  case is Int.Type:
    <code>
  ...
  }
}

Changing to "case is XXX.Type" suggests that once again, it is testing sequentially. If the type passed in the any is the first type in the switch statement using that syntax, the running time is similar to that of testCastSwitch2().

However, if the type is the last of out of ten possibilities, the running time goes up to around 0.5 seconds. So, still much better than actually casting the value (and, yes, this is mostly about determining what type is in the Any, so as to dispatch to the correct code), but still not as good as using ObjectIdentifier, which appears to let one construct some sort of jump table.

Good idea, though, it's a shame your elegant construct doesn't work as well as being forced to use ObjectIdentifier().

1 Like

I’m curious why you’re dealing with Any here. If I were in your shoes I’d avoid replace Any with a custom protocol that returns the info I need to dispatch to the right case. For example:

enum VarnishType {
    case floor
    case waffle
}

protocol Varishable {
    var appropriateVarnish: VarnishType { get }
}

func varnish1(_ v: Varishable) {
    switch v.appropriateVarnish {
    case .floor: … your code here …
    case .waffle: … your code here …
    }
}

Here I’m using a switch statement on a numeric value (which should dispatch through a table) but you could just as easily replace appropriateVarnish with a method that you call directly.

Even if you start with an Any, you can use as? to get a Varishable and proceed from there:

func varnish2(_ a: Any) {
    guard let v = a as? Varishable else {
        return
    }
    … as above …
}

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

I was thinking that Any would be familiar to my end users, and less work for me. But in the case where I both know and completely control the set of allowable types, what you say makes perfect sense and will actually clean things up.

I'm still not used to being able to import "magic" to such lowlevel things like Int (i.e. make Int conform to Varnishable, in your example) that I completely forget I can do this.

Thanks, this will actually give me complete type-safety (in that I don't have to deal with an Any holding a type I can't handle) and simplify some stuff. (Still, I'm glad to have had the discussion overall.)

3 Likes
Terms of Service

Privacy Policy

Cookie Policy