Swift プログラミング
基本構文
Swift の switch 文では整数型の値に限らず任意の型を使用できます。
独自のクラスを switch 文で自由に篩い分けする方法を調べてみました。
Swift の switch 文で独自のクラスを使用する
Swift言語
のswitch
文では、整数型だけではなく任意の型を使って条件分岐ができます。ここで独自のクラスを使用した場合の振る舞いについて調べてみました。
switch
で扱うには最低限の準備が必要
まず、次のような何も実装されていないクラスを扱う場合を見てみます。
class MyClass {
}
このような単純なクラスでは、switch
文で使おうとすると次のようなエラーになります。
error: type 'MyClass' does not conform to protocol 'IntervalType'
独自のクラスをswitch
文で使うためには、いくつかの方法でクラスに機能を実装する必要があるようでした。
独自クラスをswitch
文に対応させる
独自クラスのインスタンスを使って分岐できるようにする
自分自身と同じクラスのインスタンスとswitch
文で比較できるようにするには、クラスがEquatableプロトコル
に準拠している必要があります。
このプロトコルでは独自クラスの型同士を==演算子 で評価できるようにすることが求められるので、たとえば次のような実装になります。
class MyClass : Equatable {
}
func ==(lhs: MyClass, rhs: MyClass) -> Bool {
return lhs === rhs
}
このようにすることで、今回であればMyClassクラス
のインスタンスを使ってswitch
による条件分岐ができるようになります。
let obj1 = MyClass()
let obj2 = MyClass()
switch obj1 {
case MyClass():
println("new")
case obj2:
println("obj2")
default:
println("others")
}
Swift言語
のcase
では変数も指定できるので、事前に変数に用意しておいた値を使った条件分岐が可能です。また、case 文でインスタンスを作って判定することも可能なようです。
今回の例では==演算子 の中でポインタの一致を判定していますが、もちろんクラスに実装したプロパティ等を使って判定することができます。
このとき、各case
文では、上から順に条件判定が行われて、その時にEquatableプロトコル
で実装した==演算子
が呼び出されます。そして一致と判定された段階でcase
文内の処理が実行されて、switch
文が終了します。それ以降のcase
の比較は行われません。
リテラルを使って分岐できるようにする
独自クラスをswitch
に設定して、case
にリテラルを指定して分岐を行いたいときは、上記で紹介したEquatableプロトコル
の準拠と合わせて、独自クラスがリテラルからの変換をサポートしている必要があります。
整数リテラルで分岐できるようにする
たとえば整数リテラルを使って条件分岐したい場合は、独自クラスをEquatableプロトコル とIntegerLiteralConvertibleプロトコル に準拠させます。
class MyClass : Equatable, IntegerLiteralConvertible {
init() {
}
required init(integerLiteral value: IntegerLiteralType) {
}
}
func ==(lhs: MyClass, rhs: MyClass) -> Bool {
return lhs === rhs
}
このようにすることで、整数値リテラルを使った条件分岐が可能になります。
let obj1 = MyClass()
switch obj1 {
case 1:
println("1")
case 2:
println("2")
default:
println("others")
}
これに従って変換イニシャライザでインスタンスを適切に生成して、比較演算子での判定を適切に実装すれば、リテラルによる自由な分岐が行えます。
リテラルで分岐するときの動作としては、リテラルで指定されたcase
を判定するときに、まず、指定されたリテラルを使って変換イニシャライザが呼び出され、リテラルに適したインスタンスが生成されます。そのインスタンスとswitch
で指定したインスタンスとを比較して、そのケースに該当するかが判定されます。
文字列リテラルで分岐できるようにする
文字列リテラルを使って分岐したい場合は、独自クラスをStringLiteralConvertibleプロトコル に準拠させます。
このプロトコルはさらにExtendedGraphemeClusterLiteralConvertibleプロトコル に準拠し、さらにUnicodeScalarLiteralConvertibleプロトコル に準拠する必要がありますが、これらはどれもString型 を引数にとる変換イニシャライザの実装が求められます。
class MyClass : Equatable, StringLiteralConvertible {
init() {
}
required init(stringLiteral value: StringLiteralType) {
}
required init(extendedGraphemeClusterLiteral value: String) {
}
required init(unicodeScalarLiteral value:String) {
}
}
func ==(lhs: MyClass, rhs: MyClass) -> Bool {
return lhs === rhs
}
このようにすることで、文字列リテラルを使った条件分岐が可能になります。
let obj1 = MyClass()
switch obj1 {
case "Case A":
println("A")
case "Case B":
println("B")
default:
println("others")
}
こちらも整数リテラルのときと同様、case 文で指定された文字列リテラルから変換イニシャライザを使ってインスタンスを生成して、それとswitch に指定したインスタンスとが比較されます。
StringLiteralConvertibleプロトコル 自身が準拠するUnicodeScalarLiteralConvertibleプロトコル では、単一のUnicode 値で構成された文字列を想定するようです。またExtendedGraphemeClusterLiteralConvertibleプロトコル では、Extended grapheme clusters と呼ばれるUnicode 文字も想定する様子です。
文字列リテラルが渡されたときにはStringLiteralConvertibleプロトコル の変換イニシャライザが呼び出されるようなので、こちらをしっかり実装すれば大丈夫そうです。標準のString型 自体がこれらの変換イニシャライザをを全て持っているので、他の変換イニシャライザの実装では、受け取った値でString型 の値を作ってから、それを使って処理をすると良いでしょう。
範囲を使って分岐できるようにする
独自のクラスもComparableプロトコル
を実装することで、case
文で1...3
のような範囲型を使って条件を記載できるようになります。
ここでは整数リテラルで範囲を指定することを想定して、併せてIntegerLiteralConvertibleプロトコル にも準拠させておくことにします。Equatableプロトコル はComparableプロトコル に含まれているので明記は不要です。
class MyClass : Comparable, IntegerLiteralConvertible {
init() {
}
required init(integerLiteral value: IntegerLiteralType) {
}
}
func ==(lhs: MyClass, rhs: MyClass) -> Bool {
}
func <=(lhs: MyClass, rhs: MyClass) -> Bool {
}
func >=(lhs: MyClass, rhs: MyClass) -> Bool {
}
func >(lhs: MyClass, rhs: MyClass) -> Bool {
}
func <(lhs: MyClass, rhs: MyClass) -> Bool {
}
上記の例では演算子の実装内容は省略していますが、ここで適切な大小関係を判定することで、case
文で指定した範囲に合致するかが判定されるようになります。
let obj1 = MyClass()
switch obj1 {
case Range(3...5):
println("A")
default:
println("others")
}
範囲で指定されたcase
文は、このうちの>=演算子
と<演算子
を使って判定が行われるようです。実際に範囲の判定が行われる様子を見てみると、次のような動作をするようでした。
- まず範囲自体が適切な大小関係かを "範囲の終点 >= 範囲の始点" で確認します。
- 範囲自体が正しければ、まず "与えられた値 >= 範囲の始点" で範囲の開始点から先にあるかを調べます。
- 続いて "与えられた値" と "範囲の終点" とを、終点を含む場合は "<=" で、含まない場合は "<" で判定します。
このようにして、switch
で与えられたインスタンスが、case
で指定された範囲に収まっているかが判断されます。
なお、Comparableプロトコル
はEquatableプロトコル
にも準拠しているため、必要な機能を実装すれば、必然的にインスタンス単体でのcase
にも対応します。今回はIntegerLiteralConvertibleプロトコル
にも準拠させているため、整数値リテラル単体でのcase
にも対応することになります。
今回の例ではIntegerLiteralConvertibleプロトコル に対応させましたが、これに対応させなくても、MyClassクラス で範囲を作れば、インスタンスだけで条件分岐を構成できます。範囲の始点と終点をインスタンスで指定しても、Comparableプロトコル に準拠しているため、インスタンスの大小関係を正しく処理できます。