第61回 Cocoa勉強会関西で“Swift 1.2 The long-awaited language updates”と題して発表した、Swift 1.2の主だった(おもしろい)変更点の紹介です。
if let
Swift 1.2で最も改善されたのはif文です。if let
でOptionalをunwrapできる機能が大きく向上し、複数のOptionalを同時にunwrapできるほか、unwrapされた値について条件を加えることができるようになりました。
例えばcondition: Bool
が真でふたつのOptional<Int>
がnil
ではなく、大小関係にも条件がある、という条件を表してみます。
let condition = true
let aNumber: Int? = 3
let anotherNumber: Int? = 7
if condition {
if let a = aNumber {
if let b = anotherNumber {
if a < b {
println(a + b)
}
}
}
}
let condition = true
let aNumber: Int? = 3
let anotherNumber: Int? = 7
if condition, let a = aNumber, let b = anotherNumber where a < b {
println(a + b)
}
極端な例ではありますが、Swift 1.1ではifを4回重ねていたものを、Swift 1.2では1つのifで表現できます。
let constant must be initialized before use
let
定数は再代入できません。Swift 1.1ではそのために、宣言と同時に値を決める必要がありました。Swift 1.2ではその制限が少し緩和され、値が使われる前に確実に初期化されるなら、宣言の後で値が決まってもよいことになりました。
let hour = NSCalendar.currentCalendar().component(.CalendarUnitHour, fromDate: NSDate())
var meridiem: String
if hour < 12 {
meridiem = "AM"
} else {
meridiem = "PM"
}
println(meridiem)
let hour = NSCalendar.currentCalendar().component(.CalendarUnitHour, fromDate: NSDate())
let meridiem: String
if hour < 12 {
meridiem = "AM"
} else {
meridiem = "PM"
}
println(meridiem)
Swift 1.1ではvar
にしなければならなかったmeridiem
が、Swift 1.2ではlet
にできます。ifに限らずswitchなども利用できますが、全ての条件で初期化されることが保証されている必要があります。
Set
CollectionType
にArray
やDictionary
などに加えてSet
が追加されました。Setは集合を表し、Foundation.frameworkのNSSet
に対応します。Setの導入に合わせて、Objective-CでNSSetを利用していたAPIをSwiftから利用する場合にはSwiftのSetを使うことになります。
let abc: NSSet = NSSet(objects: "A", "B", "C")
if NSSet(objects: "A").isSubsetOfSet(abc) {
println(abc)
}
let abc: Set<String> = ["A", "B", "C"]
if abc.isSupersetOf(["A"]) {
println(abc)
}
NSSetと違って内包する値の型を指定でき、またArrayリテラルで表現できます。
Static methods and properties
Swift 1.2からclass
にもstatic
な関数やプロパティを持てるようになりました。これまでもclass func
がありましたが、static func
はclass
かつfinal
という意味になります。またstatic let
で宣言した定数は最初にアクセスされた時点で評価されるという特徴を持ちます。
class StaticPropertiesAndMethods {
static func printDate() {
println(date)
}
static let date = NSDate()
}
StaticPropertiesAndMethods.printDate()
クラス変数はObjective-Cには存在せず代替的な手法で実現していましたが、Swiftでついに用意された文法と言えます。
Non-Void return types in Void contexts
Swift 1.1でクロージャを使うときに、返り値としてVoid
を要求するところに1行だけ、Voidではない値を返す式があると、コンパイルエラーになっていました。これはクロージャが1行だけの式を持つとき、その式の結果がクロージャ全体の返り値として取り扱われ、型がミスマッチだと判定される結果です。このために無意味なreturn
を付け加えるなどして回避していました。
Swift 1.2ではこのようなときにVoidではない値が返っても許容されるようになりました。これで意味上は不要なreturn
を書く必要がなくなり、すっきりします。
let wantsVoid: (() -> Void) = {
"non-Void here"
return
}
let wantsVoid: (() -> Void) = {
"non-Void here"
}
zip
Swift 1.2ではzip
関数が導入されました。ふたつのSequenceType
の組み合わせを簡単に作ることができます。要素数が異なる場合には短い方に合わせられます。
let ordinals = ["first", "second", "third"]
let values = [1, 2, 3]
var ordinalsDict: [String: Int] = [:]
for (key, value) in zip(ordinals, values) {
ordinalsDict[key] = value
}
println(ordinalsDict)
Type casting
キャスト関連でも非互換な変更などがあります。
let any: AnyObject = 3
let number: Int = any as! Int
危険な操作であるときの一貫性のため、ダウンキャストするときas!
を使うことになりました。
protocol SomeProtocol {
func something() -> String
}
class SomeClass: SomeProtocol {
func something() -> String {
return "Something awesome"
}
}
let some: AnyObject = SomeClass()
if some is SomeProtocol {
println((some as! SomeProtocol).something())
}
@objc
ではない通常のprotocolにもキャスト関連の操作ができるようになっています。
@noescape
関数が引数としてクロージャを受け取るとき、クロージャの内部で利用される変数はキャプチャされ、それぞれ強参照された状態となります。またこのためにクロージャの外のオブジェクトのプロパティなどを利用する場合にはself.
で修飾する必要があります。これによってクロージャを実際に評価するタイミングはいつでもよいことになっています。
しかしクロージャを受け取ってすぐに評価することが決まっている関数の場合はどうでしょう。そういうときはキャプチャする必要が無いし、self.
も不要です。キャプチャしない方がパフォーマンス上でも有利でしょうし、最適化しやすいかもしれません。このようにすぐに評価されることがわかっているクロージャのために追加された新しいアノテーションが@noescape
です。
func map<U>(f: @noescape (T) -> U) -> U?
Optional
のmap
では上記のように@noescape
されています。このためクロージャで使う変数がプロパティであってもself.
は不要です。
このようにSwiftではこれらの挙動をescapeという言葉で表しています。
func map<U>(transform: (T) -> U) -> [U]
ところでArray
のmap
を見てみると、上記のように@noescape
アノテーションがないことが分かります。一貫性がないように思われるかもしれませんが、これには理由があるように思えます。lazy()
関数などを用いて遅延評価する場合にはescapeせざるを得ません。これは簡単な実験で確かめられます。
let start = NSDate()
let array = Array(0..<3)
let result = lazy(array).map { (number: Int) -> Int in sleep(1); return 100 * number }
println(NSDate().timeIntervalSinceDate(start))
このようにlazy()
関数を使えばmap
中でsleep(1)
していても一瞬で実行が終わり、result[2]
などresult
の要素にアクセスしたとき初めてmap
のクロージャが評価されます。このlazy()
を消すと、このスニペットは少なくとも3秒の実行時間を必要とします。
これがArray.map
がescapeを必要とする理由です。
この@noescape
が追加されたのに合わせてデフォルトでは@autoclosure
もescapeされなくなりました。escapeしてほしい場合には@autoclosure(escaping)
と書く必要があります。
flatMap
新しいflatMap
関数を使うとmap
ではできないちょっとしたことが簡単にできるようになります。
let mapped = ["A B C", "D E", "F"].map {
$0.componentsSeparatedByString(" ")
}
println(mapped)
let flatMapped = ["A B C", "D E", "F"].flatMap {
$0.componentsSeparatedByString(" ")
}
println(flatMapped)
上記の例のように、Arrayの要素に何らかの変換を行うとき、flatMap
なら要素毎に新たなArrayを返しても2次のArrayになりません。
func toInt(value: String) -> Int? {
return value.toInt()
}
let one: String? = "1"
let mapOne = one.map { toInt($0) }
println(mapOne)
let flatMapOne = one.flatMap { toInt($0) }
println(flatMapOne)
また上記の例では、Optional
のmap
とflatMap
の違いを示しています。map
の中でさらにOptionalな値を返すとOptional<Optional<Int>>
を得ることになります。flatMap
ではOptional<Int>
です。これは非常に有用であることがわかるでしょう。
このようにflatMap
はmap
とよく似ていますが、最初のArrayと要素数が変わったArrayが返ったり、多重のOptionalを避けられたりといった特徴があります。
let intLike = ["12.3", "45", "6-7-8"]
let results = intLike.flatMap {
$0.toInt().map { [ $0 ] } ?? []
}
println(results)
上記はこれの応用例で、ふつうのmap
であればArray<Int?>
が返るところを、nil
を除去してArray<Int>
にしています。
NSEnumerator.generate() -> NSFastGenerator
Swift 1.2ではFoundation.framework
にも少し拡張が増えています。
let fonts = "/System/Library/Fonts"
let files = NSFileManager().enumeratorAtPath(fonts)!
for path in files {
println(path)
}
enumeratorAtPath
はNSDirectoryEnumerator
というNSEnumerator
のサブクラスを返します。Swift 1.2より前では、NSEnumerator
はSequenceType
ではなく、for in
が使えませんでした。Swift 1.2からはgenerator()
メソッドが加わり、protocol SequenceType
を満たすようになったためfor in
できるようになりました。
Swiftはこのように、Foundation.framework
についてSwiftから利用しやすいように多少の拡張を加えており、今後もこれらが拡充されていく可能性があります。
その他の非互換な変更
Remove implicit conversions from NSString/NSArray/NSDictionary to String/Array/Dictionary
NSString
などのFoundation.framework
のオブジェクトから対応するSwiftのString
などへの暗黙的な変換が行われなくなりました。明示的にas String
などと書いてキャストしておく必要があります。逆のString
からNSString
といった変換はこれまで通り暗黙的に行われます。
SwiftからObjective-Cのヘッダを見たときにはNSString
はString
になっており、Swiftからはほとんど常にString
を利用していることになるので、実際的にこれが問題になるケースは少ないでしょう。ただしCoreFoundation.framework
に関するオブジェクトを利用するときは別です。
countElements → count
要素数を数える関数の名前が変わりました。分かりやすいですね。
パフォーマンスに関する変更
Swift 1.2と共に様々なパフォーマンスが改善されました。Swiftで書かれたプログラムのパフォーマンスが改善されたほかにも興味深い変更があります。
Incremental build
Swiftのコンパイルがインクリメンタルになりました。これまでは、ひとつのファイルを変更したときにも全てのファイルのコンパイルをやり直す必要がありましたが、これからは必要最低限のファイルをコンパイルし直すだけでよくなりました。ただし今後も、変更したファイルだけをコンパイルし直すのではなく、変更が影響するであろうファイル全てをコンパイルし直す必要があります。
Swift 1.2でも思ったよりコンパイルが遅いということがあるかもしれませんが、もしかするとXcode 6.3.1の問題かもしれません。
Whole Module Optimization
オブジェクト指向のパラダイムにおいて、クラスを継承してメソッドをオーバーライドするというのがあります。すなわち、メソッドを呼び出す際にはオーバーライドされた後の実装を呼び出す必要があるということです。プログラムの実行中、メソッドの呼び出しやプロパティを操作する時に、実際に呼び出すべき対象を動的に解決します。これをdynamic dicpatchと呼んでいます。
ここで、例えばfinal
修飾されているメソッドは、ぜったいにオーバーライドできません。そういった場合にはdynamic dispatchする必要がないので、Swiftのコンパイラは静的に解決してくれます。dynamic dispatchが行われない分、実行時のパフォーマンスは改善します。
さらにアクセス制御のprivate
で修飾されていれば、そのファイルの外からはオーバーライドできません。つまりそのファイル中でオーバーライドされているか検査することで不要なdynamic dispatchをしないようにコンパイルできます。private
修飾はパフォーマンスにも役立っていることがわかります。
さらにinternal
でもこれを可能にするのがWhole Module Optimizationです。internal
で修飾されていればそのモジュールの外からオーバーライドされません。つまり、モジュール全体を検査することで、不要なdynamic dispatchを避けることができます。この最適化は比較的時間がかかるため、現在のところオプションになっています。
これらの話題はSwiftのブログにも書かれています。
Swift 1.2に合わせてObjective-Cにも変更があります。変数や引数、返り値などがnil
を取り得るかをアノテートできるようになりました。nullable
やnonnull
で修飾することでこれが可能です。また全てにこれらのアノテーションを付けて回るのは現実的ではないので、NS_ASSUME_NONNULL_BEGIN
とNS_ASSUME_NONNULL_END
で囲われている部分はnonnull
ということになっています。基本的にはこれで囲んでおいて、nullable
だけ明示的に書くのがよいでしょう。
これらのアノテーションを書いておくと、Swiftから見たときにImplicitly Unwrapped OptionalではなくOptionalかそうでないかはっきりします。さらにObjective-Cにおいても、nonnull
とアノテートされたポインタにnil
を入れようとしていれば静的解析で検出されます。ただし実行時には影響しません。
Swiftはまだ変わり続ける生きた言語です。Swift 1.2はSwiftの正式なリリース以来もっとも大きな変化で、開発者にとっても非常に歓迎すべき改善が数多くあります。今後のSwiftの進化に期待しながら、Swift 1.2でバリバリ開発していきたいですね。
いっしょにSwiftでバリバリ開発しませんか。