唯一変わったのは、そのすべて
「唯一変わったのは、そのすべて」 。iPhone 6sのキャッチコピーですが、むしろそれはSwiftにこそふさわしい一言ではないかというぐらいSwiftは大きく変わりました。前回 はそれを広く浅く紹介したのですが、今回からはそれぞれの変化を深く見ていきましょう。
Type?
Swiftの最大の特長は何かと問われたら、筆者はOptional型の多用だと答えます。本連載を最初から追いかけてくださっている読者の皆さんは納得していただけると思いますが、そうではない読者のために、ここで一度おさらいしておきましょう。
次のようなDictionary
があったとします。
var supportedLanguages = [
"C" : 1,
"ObjectiveC" : 2,
]
次はなんとprint
するでしょうか?
print(supportedLanguages["C"])
1
ではなく、Optional(1)
ですね。では次は?
print(supportedLanguages["Swift"])
nil
となります。
今度はOptional(1)
ではなく1
となります。この挙動を、型に着目して追っていきましょう。まずsupportedLanguages
の型は[String:Int]
です。つまりString
を添字にすると、対応するInt
が返ってくるデータ型なのですが、その中にないString
を添字には何を返したらよいでしょう? クラッシュするか「何もない」を何らかの形で返すかのどちらかということになります。Swiftが採用したのは後者でした。この「何もない」のがnilで、「 nil
か値を返す」のがOptional型です。つまりsupportedLanguages[k]
の型は、Int
ではなくOptional<Int>
つまりInt?
ということになります。
ところでSwiftには、enum
があります。Optional型をenum
で表現するとどうなるでしょうか? こんな感じでしょうか。
enum Optional<T> {
case Nil
case Some(T)
}
Swiftの実装は、まさにそのようになっています。[String, Int]
が実はDictionary<String,Int>
の構文糖衣であるように、Int?
というのはOptional<Int>
の構文糖衣に過ぎないのです。
There's more than one way to fail
以上を踏まえて、次を見てみましょう。
var language = "C"
if let i = supportedLanguages[language] {
print(i)
} else {
print("Swift is not supported");
}
Optional(1)
ではなく、1
と表示されます。i
の型はInt?
ではなくInt
で、if
に続く{}
の中では100%例外なくiはInt
であることが保証されている一方、else
に続く{}
の中ではsupported Languages[language]
がnil
だったことが100%例外なく保証されているわけです。これがSwiftにおけるエラー処理の基本でした。Optional(と型変数)導入により、静的な型でもDictionary
のような動的に扱いたい型の扱いが動的言語なみに楽になったのです。
しかし、実際には「うまくいかない」だけではうまくいかないケースは少なくありません。「 何がどううまくいかなかった」かによって、処理を変えたいケースも多いのです。たとえば「軽い」エラーならデフォルト値を代わりに使って続行し、「 重い」エラーならプログラムを終了する。そういった場合、どうしたらよいのでしょう?
SwiftのOptionalがenum
で実装されていることを知っていれば、次のようなSuperOptional
を定義してしまえばその問題は解決しそうです。
enum SuperOptional<E,T> {
case Error(E)
case Some(T)
}
func handleSuperOptional<E,T>(so:SuperOptio
nal<E,T>) {
switch(so) {
case let .Some(i):
print(i)
case let .Error(s):
print("Error:\(s)")
}
}
var so:SuperOptional<String, Int> =
.Some(42)
handleSuperOptional(so)
so = .Error("Not a number")
handleSuperOptional(so)
ところが、Swift 1では型変数を複数持つ総称型enumはサポートしていなかったのです(図1 ) 。
図1 総称型enumの動作
見てのとおりSwift 2では期待どおり動いていますが、Swift 1.xではコンパイラがクラッシュしてしまいます。
Swift 1におけるOptional
はenum
で実装されていましたが、Swift 2におけるtrycatch
もenumによって実現されています。
give it a try
というわけでSwift 2のtry catch
を実際に使ってみましょう。ここでは例題として、「 クラッシュしない配列」を実装してみます。
SwiftのArray
の要素に範囲外の添字を与えると、問答無用でクラッシュします。
var ary = [0,1,2,3]
ary[4] // ここでクラッシュ
これはDictionary
とは異なる振る舞いです。
var dict = [0:0, 1:1, 2:2, 3:3]
dict[4] // nil
問答無用でクラッシュする代わりに、何らかのエラーを返すにはどのようにしたらよいでしょうか?
Swift 2では、まずどんなエラーを返すかを定義します。
enum ArrayError : ErrorType {
case RangeError
}
見てのとおり、エラーはErrorType
型を継承したenum
です。次に、エラーを起こしうるメソッドを次のように定義します。
extension Array {
func valueAtIndex(i:Int) throws ->
Element {
if self.count <= i {
throw ArrayError.RangeError
}
return self[i]
}
}
通常のfunc
と異なる点は2つ。1つは->
の前にthrows
というキーワードが追加されていること、もう1つは範囲外であることを検出したうえで、その場合はthrow ArrayError.Range Error
していること。
あとはこれを使うだけ。
var ary = [0,1,2,3]
do {
var v:Int
v = try ary.valueAtIndex(0)
v = try ary.valueAtIndex(4)
} catch {
print("Array out of range")
}
たしかに今度はクラッシュせず、“ Array out of range” と表示されるようになりました(図2 ) 。ここでコードを見てみましょう。まず、実行ブロックがtry
ではなくdo
で始まっています。そしてtry
はary.valueAtIndex()
の前についています。try
を取り除くとどうなりましたか?
図2 trycatchのエラー
Java(Script)と同様、catch
は特定のエラーだけを捕まえることもできます。たとえば次のようにコードを書き換えてみましょう。
enum ArrayError : ErrorType {
case OutOfBounds
case NegativeBounds
}
extension Array {
func valueAtIndex(i:Int) throws ->
Element {
if i < 0 {
throw ArrayError.NegativeBounds
}
if self.count <= i {
throw ArrayError.OutOfBounds
}
return self[i]
}
}
var ary = [0,1,2,3]
do {
var v:Int
v = try ary.valueAtIndex(0)
v = try ary.valueAtIndex(-1)
} catch ArrayError.NegativeBounds {
print("Array Index must be zero or
lager")
} catch ArrayError.OutOfBounds {
print("Array Index too large")
} catch {
print("Unknown Error")
}
例外を例外扱いしないSwift
さらにSwiftならではの特長として、catch
でまとめて捕まえるのではなく、if let
やguard
で捕まえることもできます。
if let v = try? ary.valueAtIndex(4) {
print(v)
} else {
print("Array out of range")
}
もしくは
guard let v = try? ary.valueAtIndex(3) else
{
print("Array out of range")
}
まとめると、次のとおりとなります。
TypeError
を継承したエラー型を定義
エラーを起こしうるfunc
には->
の前にthrows
をつける(エラーを起こしたら定義したエラーをthrow
)
エラーを起こしうる関数/メソッドはtry
する(エラーはcatch
だけではなくif let
やguard
で使うこともできる)
このようにSwift 2のtry catch
機構は、Java(Script) ?のそれと比べると少し面倒ですが、例外ではなく飽くまで一般的なデータ型であるenum
の自然な拡張として実現されている点が実に特長的です。構文の視点で見るとdo catch
は「例外処理」ですが、型の視点で見ると飽くまでで普通の処理。Swift 2に合わせて改定された[The Swift Programming Language] でも、「 例外」( exception)という言葉を避けて飽くまで「エラー処理」( Error Handling)としているのもそのためでしょう。
次号は
「なるべく一般的かつ包括的に」 。それがさらに反映されているのが、Protocol Oriented Programmingという新たなスローガンでしょう。
次回は、いよいよこのProtocol Oriented Programmingを取り上げます。
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「 使いたい」「 発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)