読者です 読者をやめる 読者になる 読者になる

cockscomblog?

cockscomb on hatena blog

Swift 1.2

第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ではなく、大小関係にも条件がある、という条件を表してみます。

Swift 1.1

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)
            }
        }
    }
}

Swift 1.2

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ではその制限が少し緩和され、値が使われる前に確実に初期化されるなら、宣言の後で値が決まってもよいことになりました。

Swift 1.1

let hour = NSCalendar.currentCalendar().component(.CalendarUnitHour, fromDate: NSDate())

var meridiem: String
if hour < 12 {
    meridiem = "AM"
} else {
    meridiem = "PM"
}

println(meridiem)

Swift 1.2

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

CollectionTypeArrayDictionaryなどに加えてSetが追加されました。Setは集合を表し、Foundation.frameworkのNSSetに対応します。Setの導入に合わせて、Objective-CでNSSetを利用していたAPISwiftから利用する場合にはSwiftのSetを使うことになります。

Swift 1.1

let abc: NSSet = NSSet(objects: "A", "B", "C")
if NSSet(objects: "A").isSubsetOfSet(abc) {
    println(abc)
}

Swift 1.2

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 funcclassかつfinalという意味になります。またstatic letで宣言した定数は最初にアクセスされた時点で評価されるという特徴を持ちます。

class StaticPropertiesAndMethods {
    static func printDate() {
        println(date)
    }
    static let date = NSDate()
}

StaticPropertiesAndMethods.printDate() // => "2015-04-10 07:01:00 +0000"

クラス変数はObjective-Cには存在せず代替的な手法で実現していましたが、Swiftでついに用意された文法と言えます。

Non-Void return types in Void contexts

Swift 1.1でクロージャを使うときに、返り値としてVoidを要求するところに1行だけ、Voidではない値を返す式があると、コンパイルエラーになっていました。これはクロージャが1行だけの式を持つとき、その式の結果がクロージャ全体の返り値として取り扱われ、型がミスマッチだと判定される結果です。このために無意味なreturnを付け加えるなどして回避していました。

Swift 1.2ではこのようなときにVoidではない値が返っても許容されるようになりました。これで意味上は不要なreturnを書く必要がなくなり、すっきりします。

Swift 1.1

let wantsVoid: (() -> Void) = {
    "non-Void here"
    return
}

Swift 1.2

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) // => ["first": 1, "second": 2, "third": 3]

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?

Optionalmapでは上記のように@noescapeされています。このためクロージャで使う変数がプロパティであってもself.は不要です。

このようにSwiftではこれらの挙動をescapeという言葉で表しています。

func map<U>(transform: (T) -> U) -> [U]

ところでArraymapを見てみると、上記のように@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) // => [[A, B, C], [D, E], [F]]

let flatMapped = ["A B C", "D E", "F"].flatMap {
    $0.componentsSeparatedByString(" ")
}
println(flatMapped) // => [A, B, C, D, E, F]

上記の例のように、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) // => Optional(Optional(1))

let flatMapOne = one.flatMap { toInt($0) }
println(flatMapOne) // => Optional(1)

また上記の例では、OptionalmapflatMapの違いを示しています。mapの中でさらにOptionalな値を返すとOptional<Optional<Int>>を得ることになります。flatMapではOptional<Int>です。これは非常に有用であることがわかるでしょう。

このようにflatMapmapとよく似ていますが、最初のArrayと要素数が変わったArrayが返ったり、多重のOptionalを避けられたりといった特徴があります。

let intLike = ["12.3", "45", "6-7-8"]
let results = intLike.flatMap {
    $0.toInt().map { [ $0 ] } ?? []
}

println(results) // => [45]

上記はこれの応用例で、ふつうの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)
}

enumeratorAtPathNSDirectoryEnumeratorというNSEnumeratorのサブクラスを返します。Swift 1.2より前では、NSEnumeratorSequenceTypeではなく、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のオブジェクトから対応するSwiftStringなどへの暗黙的な変換が行われなくなりました。明示的にas Stringなどと書いてキャストしておく必要があります。逆のStringからNSStringといった変換はこれまで通り暗黙的に行われます。

SwiftからObjective-Cのヘッダを見たときにはNSStringStringになっており、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のブログにも書かれています。

Objective-CのNullability

Swift 1.2に合わせてObjective-Cにも変更があります。変数や引数、返り値などがnilを取り得るかをアノテートできるようになりました。nullablenonnullで修飾することでこれが可能です。また全てにこれらのアノテーションを付けて回るのは現実的ではないので、NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_ENDで囲われている部分はnonnullということになっています。基本的にはこれで囲んでおいて、nullableだけ明示的に書くのがよいでしょう。

これらのアノテーションを書いておくと、Swiftから見たときにImplicitly Unwrapped OptionalではなくOptionalかそうでないかはっきりします。さらにObjective-Cにおいても、nonnullとアノテートされたポインタにnilを入れようとしていれば静的解析で検出されます。ただし実行時には影響しません。


Swiftはまだ変わり続ける生きた言語です。Swift 1.2はSwiftの正式なリリース以来もっとも大きな変化で、開発者にとっても非常に歓迎すべき改善が数多くあります。今後のSwiftの進化に期待しながら、Swift 1.2でバリバリ開発していきたいですね。

詳解 Swift

詳解 Swift


いっしょにSwiftでバリバリ開発しませんか。