先週土曜日(9/6)にレーベにてSwift勉強会を実施しました。iPhoneアプリの著書で有名な森巧尚さんにもご参加いただき、大変濃い勉強会になりました。今回は勉強会の中で中心的に紹介したOptionalの話について紹介します。
何故この話を中心的に紹介したのかというと、このXcode6 beta7以降は、beta7より前に作られたWeb上やGitHub上にあるサンプルコードのほとんどに修正が必要になりました(ビルドが通らない)。今から勉強する方にも影響が大きいと思いましたので、詳しくご紹介します。
iOS8リリース直前のXcode6に大きな変更
2014年9月2日にXcode6 beta7がリリースされました。このbeta7で大きな変更が有り、既存のコード(beta7以前)で大量のエラーが出てしまい、コードの書き換えを要求される事になりました。
原因は多くのクラスの、多くの変数がOptionalに変更されたことです。
Optionalは変数がnilを取ることを許可するというフラグです。
影響が大きい箇所として、UITableViewCellのほぼ全てのプロパティが影響箇所となります。
beta7以前
cell.textLabel.text = "text"
cell.imageView.image = UIImage(named: "test.png")
beta7以降
cell.textLabel?.text = "text"
cell.imageView?.image = UIImage(named: "test.png")
このように開発の途中でプロパティがnilになる可能性が生まれる(た)時、果たしてどうするべきなのか、ということを考えたいと思います。 SwiftでOptionalのことを考える必要がある箇所は基本的には3つです。
- 関数の返り値
- クラスのプロパティ
- 関数の引数
これらについて「後で仕様変更がある」という事を前提にどうすれば良いか、おそらく2つのポリシーがあると思います。
- 全部nilになることもあるから最初から全部Optionalで良いんじゃない派
- 正しく設計して必要があったら書き換えて貰った方がいいよ派
個人的には「全部nilになることもあるから最初から全部Optionalで良いんじゃない派」です。
ただ、このパターンは先述の通りコードの端々に「!」「?」が書かれるためイケてない感じがしますのでその辺りについて考えてみます。
関数の返り値の設計について
UITableViewを例に出して説明します。 ある行のセルを返す関数はこんな感じで書けます。
//このメソッドの返り値は「AnyObject?」なので「UITableViewCell?」にキャスト
var cell = tableView.dequeueReusableCellWithIdentifier("CELL") as UITableViewCell?
if cell == nil { .... }
cell?.textLabel?.text = "text"
cell!.textLabel!.text = "text"
return cell!
- cellに対して一々「?」とか「!」とか書く必要がある
- 二種類の書き方が存在する
- 「return cell!」が直感的では無い
などなどいくつも問題点があります。
また、こんな感じにも書き換えることが出来ます。
var cell = tableView.dequeueReusableCellWithIdentifier("CELL") as UITableViewCell!
if cell == nil { .... }
cell.textLabel?.text = "text"
cell.textLabel!.text = "text"
return cell
ちょっと綺麗になりました。先ほどよりは違和感が減ったと思います。
ではこんな書き方をしたらどうでしょうか?
var cell = tableView.dequeueReusableCellWithIdentifier("CELL") as UITableViewCell
if cell == nil { ... }
このコードは動きません。なぜなら「as UITableViewCell」という形でキャストをした場合nil比較が出来ないのです。
何故か、というのはわかりません。Objective-Cなら出来ましたね。
少し整理します。
func func1() -> AnyObject?{ ... }
func func2() -> AnyObject{ ... }
func func3() -> AnyObject!{ ... }
という3つの関数があった時
func1() as SomeClass! //エラーにならない
func2() as SomeClass! //エラーになる
func3() as SomeClass! //エラーにならない
func1() as SomeClass? //エラーにならない
func2() as SomeClass? //エラーになる
func3() as SomeClass? //エラーにならない
感覚的にどうでしょうか? func2が非常に使い勝手が悪い気がしますが、それ以外については対して変わらないんじゃないかと思います。
「返り値はOptional(場合によっては!をつける)」という実装をオススメします。
例外はいくつかあります。
1. 配列を返すメソッド
これは空の配列を返す形でもOKだと思いますので、「func() -> [AnyObject]」でも可能です。
2. Object以外を返す
BoolやInt、CGFlotなどのプリミティブな値はそのままが良いです。「func() -> CGFloat」
3. クロージャーを返す
基本的にはクロージャーを返す場合はnilじゃない方が良いと思います。プロパティにクロージャーが入っている場合はその限りではありません。
クラスのプロパティ
プロパティをOptionalにしない場合、初期値を必要とします。
宣言部分に書けない場合はinitに書くことも可能です。
class TestObject{
var hoge1 : String = "hoge1 value"
var hoge1 : NSURL
var hoge2 : String?
var hoge3 : String!
override init() {
hoge1 = NSURL(string: "http://www.google.co.jp")
}
}
これについてはどちらとも言えます。
全てのOptionalにするUITableView関連はちょっとやり過ぎだと思います。
これら3つの違いをまとめました。
if hoge.hoge1 != nil { ... } //エラー
if hoge.hoge2 != nil { ... } //OK
if hoge.hoge3 != nil { ... } //OK
ifでのnilチェックについては前述の返り値のキャストとほぼ同じ結果になります。
ではasでのキャストはどうでしょうか?
hoge.hoge1 as String //OK
hoge.hoge1 as String? //OK
hoge.hoge1 as String! //OK
hoge.hoge2 as String //エラー
hoge.hoge2 as String? //OK
hoge.hoge2 as String! //OK
hoge.hoge3 as String //OK
hoge.hoge3 as String? //OK
hoge.hoge3 as String! //OK
こちらについては結果が異なります。
まとめると以下のようになります。
無印
- nil比較が出来ない
- nil比較をする場合は一度String?にダウンキャストする
- 初期値を入れないとエラーになる
Optional
- 無印へのキャストがエラーになる
- プロパティのプロパティを呼び出す歳にunwrap処理が必要
unwrap
- 宣言と異なり、nilでも動作するが実行時エラーとなる
プロパティについては
「コンストラクタで初期値を入れる物については無印、それ以外についてはOptional」
というルールが良いと思います。
現状メソッドを呼び出す際はunwrapする必要があり、コードとしてはイマイチです。
ローカル変数に一度代入する方法もありますが、可読性も低くなります。
Optionalなプロパティを使う場合、if文でチェックしてたらunrapが不要になるというようにコンパイラが進化してくれれば理想的です。将来的にはそうなるんじゃないかと期待しています。
if hoge.hoge2 != nil {
hoge.hoge2!.doSome()
hoge.hoge2.doSome() //これは動作しない
}
引数
これについては「ケースバイケース」です。 なぜなら関数のオーバーロードが可能だからです。
func doSome(hoge: NSNumber){
NSLog("doSome1")
}
func doSome(hoge: NSNumber?){
NSLog("doSome2")
}
のように同じ引数で違う関数を作ることが出来ます。
それぞれ宣言によって呼び出される内容が違います。
hoge.doSome(NSNumber(bool: 1))
-> doSome1 (1)
var value : NSNumber? = NSNumber(bool: 1)
hoge.doSome(value)
-> doSome2 (2)
hoge.doSome(value!)
-> doSome1 (3)
var value : NSNumber?
hoge.doSome(value)
-> doSome2 (4)
(3)だけ想像と異なり、unwrapするとdoSome1になりました。
引数については仕様が変わった結果nilになっても回避が可能ということでケースバイケースで自由な運用が出来ると思います。
まとめ
- SDK側の書き換えに対処する地獄は続く
- ?とか!とか考えながらやるのは嫌だなぁ…
beta7はファイナルバージョンではないので、もう少し調整がある可能性は十分にあります。iOS8登場後の正式版のXcode6リリース後にまたフォロー出来ればと思います。
[Writer: Ryosuke Miyazawa , Masaaki Fujii]