In depth analysis of Swift switch statements for case data type matching support

  • 2020-05-13 03:32:52
  • OfStack

Swift can match the values of different data types in switch:


var things = Any[]()

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name:"Ghostbusters", director:"Ivan Reitman"))

for thing in things {
 switch thing {
 case 0 as Int:
 println("zero as an Int")
 case 0 as Double:
 println("zero as a Double")
 case let someInt as Int:
 println("an integer value of (someInt)")
 case let someDouble as Double where someDouble > 0:
 println("a positive double value of (someDouble)")
 case is Double:
 println("some other double value that I don't want to print")
 case let someString as String:
 println("a string value of "(someString)"")
 case let (x, y) as (Double, Double):
 println("an (x, y) point at (x), (y)")
 case let movie as Movie:
 println("a movie called '(movie.name)', dir. (movie.director)")
default:
 println("something else")
}
}

// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of"hello"
// an (x, y) point at 3.0, 5.0
// a movie called 'Ghostbusters', dir. Ivan Reitman

So it's going to match the value of thing to the value of case.

Today, I suddenly thought of a question, which makes me think it is necessary to summarize the switch statement. We know that switch in swift is far more powerful than C, which can only compare integers, but the question is, which types can be compared in switch, and can objects be compared?

Official documentation explains the use of switch as follows:

Cases can match many different patterns, including interval matches, tuples, and casts to a specific type.
This means that in addition to the most common comparisons of integers, strings, and so on, switch can be used to match ranges, tuples, conversions to a particular type, and so on. But the including in the document is really dumb, because it doesn't specify all the types that can be compared in switch, and the question at the beginning of the article remains unanswered.

Let's try 1, using switch to match objects:


class A {

}

var o = A()
var o1 = A()
var o2 = A()

switch o {
case o1:
  print("it is o1")
case o2:
  print("it is o2")
default:
  print("not o1 or o2")
}

Sure enough, the compiler reported an error: "Expression pattern of type 'A' cannot match values of type 'A'". At least we don't yet understand what "expression pattern" is and how type A can't match type A.

Let's make one change and add let after case:


switch o {
case let o1:
  print("it is o1")
case let o2:
  print("it is o2")
default:
  print("not o1 or o2")
}

OK, compile and run, the result is: it is o1. This is because case let is not a match value, but a value binding, which assigns the value of o to the temporary variable o1, which is useful when o is an optional type, like if let, which implicitly resolves the optional type. I didn't type it is o2 because switch in swift matches only the first case that matches, and then I'm done, even if I don't write break it doesn't jump to case.

Anyway, to get back to the point, since let doesn't work, we have to think of something else. Consider how the switch statement is implemented. As far as I can guess, it is similar to using many if to judge whether there is a match for case. In that case, let's overload the type A with 1 == operator:


class A {}

func == (lhs: A, rhs: A) -> Bool { return true }

var o = A(); var o1 = A() ;var o2 = A()

switch o {
case o1:
  print("it is o1")
case o2:
  print("it is o2")
default:
  print("not o1 or o2")
}

Obviously, it failed again. If that's the answer, this article is too lame. The error message is the same as the previous one. The problem is that we have overridden the == operator, so why can't A match A? Doesn't switch have to judge whether the two variables are equal or not?

As a statement matching multiple conditions, switch naturally determines whether the variables are equal or not, but it does so not by the == operator, but by the ~= operator. Here's another explanation from the official document:

An expression pattern represents the value of an expression. Expression patterns appear only in switch statement case labels.
And this:

The expression represented by the expression pattern is compared with the value of an input expression using the Swift standard library ~= operator.
"express pattern" refers to the value of the expression. This concept is only found in the case tag of switch. So the previous error message was: "the value of the expression o1 (again o1) and the passed parameter o are of type A, but they do not match". The answer to why it doesn't match is in the second sentence, because the match between o1 and o is done by calling the ~= operator in the standard library.

So, just replace overloading == with overloading ~=. You change one character, you don't have to change anything else, and then the program runs. Swift calls the == operator in the ~= operator by default, which is why we don't feel the extra processing required to match integer types. For custom types, however, it is useless not to overload the ~= operator, even if you do.

Another solution is to have the A type implement the Equatable protocol. This eliminates the need to overload the ~= operator. The answer is in the last few lines of Swift's module:


@warn_unused_result
public func ~=<T : Equatable>(a: T, b: T) -> Bool

Swift has overloaded the ~= operator for all classes that implement the Equatable protocol. Although implementing Equatable only requires overloading the == operator, swift will not know if you do not explicitly indicate that you are complying with Equatable. So, if you overload the == operator, you can implement the Equatable protocol under the label 1, which has many benefits, such as SequenceType's split method.

Conclusion 1 sentence:

Types that can be placed in switch statements must either overload the ~= operator or implement the Equatable protocol.


Related articles: