書いて覚えるSwift入門

第39回史上最も地味なWWDC?

WWDC 2018総括

というわけで前回予告どおり、WWDC 2018を本記事で振り返るわけですが、いやあ、地味なWWDCでしたね。新規ハードウェアの発表ゼロ、次期macOSであるMojaveの一番のウリはDark Mode写真1⁠。そしてSwift 5は今年ではなく来年。しかし地味なだけに、派手さに隠れて目立たなかったSwiftの問題を地味に片付けていったのは筆者としてはむしろ好感が持てました。

写真1 macOS Mojaveプレビュー
写真1 macOS Mojaveプレビュー

というわけで⁠not much⁠でも⁠not insignificant⁠What's New in Swiftを一緒に振り返りましょう。

Swift 5は2019年前半

まず重要なのは、Swift 5は今年には来ないことが正式に発表されたこと。次のXcodeであるXcode 10に搭載されるのはSwift 4.2です。これはSwift 5でABI(Application Binary Interface)がフリーズされ、以後は過去のモジュールをコンパイルし直さなくても新しいSwiftでリンクすることが決定されたからで間違いありません。

Cの完全上位互換言語として登場したC++がなぜCを置き換えられなかったかと言えば、ABIが未決定で、Cで書いたライブラリのようにC++で書いたライブラリを再利用しにくかったからというのが理由の筆頭ではないでしょうか。ABIが未決定でもコンパイルしなおせばいいだけの話ですが、それが許せるのは無尽蔵のCPU資源と時間を持っている人だけでしょう。GUI環境のコンパイルなんてまる1日かかりますし。

というわけでSwift 5以降はソースではなくコンパイル済みのライブラリやフレームワークやモジュールを使えるようになるのですが、だとしたらABIの仕様決定で失敗するとのちのちまでたたることを意味します。今年ではなく来年まで準備期間を置くというのは極めて納得のいくところです。

そういうこともあって、⁠What's New in Swift」のwhat's new はwhat was newという印象さえありましたが、あらためて見てみると「え? Swiftってこんなことが今までできなかったの?」という発見がかなりあり、もっと枯れないとABIフリーズできないよなという思いを新たにしました。

enumの自動列挙

その筆頭がこちら。enumはenumerationつまり列挙型ですが、今までは全列挙するための手段が用意されていませんでした。

enum DayOfTheWeek {
    case sun, mon, tue, wed, thu, fri, sat
}
for day in DayOfTheWeek {
    // error: type 'DayOfTheWeek.Type' does not
    // conform to protocol 'Sequence'
}

こうした場合は、そのためのメソッドを手で追加してやる必要がありました。しかし見てのとおりこのコードはDRYではありません。

enum DayOfTheWeek {
    case sun, mon, tue, wed, thu, fri, sat
    static var allCases:[DayOfTheWeek] {
        return [sun, mon, tue, wed, thu, fri, sat]
}
}
for day in DayOfTheWeek.allCases {
     今度はOKだが…… 
}

これを全自動でやろうというのがSE-0194で、Swift 4.2より実装されました。

enum DayOfTheWeek : CaseIterable {
    case sun, mon, tue, wed, thu, fri, sat
}
for day in DayOfTheWeek.allCases {
     これでよし 
}

CaseIterableというプロトコル準拠を宣言する必要があるのは、EquatableHashableと同様です。すべてのenumが列挙可能ではないことは、Swiftのenumが単なる列挙型を超えた、Cのunion(共用体)としても用いられていることからも明らかで、プロトコル準拠宣言で「普通の」列挙可能なenumであることをコンパイル時に保証できるわけです。

ターゲット環境ごとの条件付きコンパイル

たとえばシミュレータ用と本番環境用でコードを切り替えたいとします。今までは#ifでそれを切り替える方法がなかったので、os()cpu()を使ってしかたなく次のようにしていました。

 #if (os(iOS) ¦¦ os(watchOS) ¦¦ os(tvOS))
  && (cpu(i386) ¦¦ cpu(x86_64))
print("Simulator")
#else
    print("Device")#endif

SE-0190hasTargetEnvironment()が導入されたことにより、それが次のように自然と書けるようになります。すでにSwift 4.1で実装されています。

#if hasTargetEnvironment(simulator)
    print("Simulator")
#else
    print("Device")
# endif

Hashableプロトコルの刷新

SE-0185がSwift 4.1で実装されたことで、EquatableHashableの自動生成がすでに使えるようになっています。ストアドプロパティすべてがEquatableHashableに準拠している型なら、型宣言のところでEquatableHashableを付けるだけで……、

enum DayOfTheWeek : Int, Hashable {
    case sun, mon, tue, wed, thu, fri, sat

}

==(_:_)や.hashValueを実装しなくても使えるようになります。

let officeHour:[DayOfTheWeek:ClosedRange<Int>] = [
    .mon : (900...1700),
    .tue : (900...1600),
    .wed : (900...1500),
    .thu : (900...1400),
    .fri : (900...1300),
]

DayOfTheWeek(rawValue:0) == .sun

注意点としては、自動生成はextensionではダメで型宣言の時点で行わないといけないのと、Hashableを付けた場合には自動的にEquatableになる点です。実は後者の点は型によっては問題になります。たとえばFloatingPoint浮動小数点型には中身が同じでも==で比較したらfalseが返るnanという値がありますが、しかしこの場合でも自動Hashableはできます。筆者は現在swift-bignumという任意精度浮動小数点モジュールを開発しているのですが、ここでもHashableはSwiftに自動生成してもらっています。ではNaNなどの扱いをどうしているかというと、FloatingPointプロトコルでは.isEqual()というメソッドの実装を必須にしたうえでそちらで対応しているわけです。

このように自動生成が可能な場合でも手動で実装すればそちらが優先で使われるのですが、安易にオレ実装してほしくない場合も実はあります。Hashableはその代表格です。たとえば、

struct City {
    let name: String
    let state: String
    let population: Int
}

という形があったとします。この場合populationはハッシュ値生成には不要なので、namestateのハッシュ値を何らかの形で合成して.hashValueを生成すればよさそうに見えます。Swift 4.1以前では、次のようなコードをよく見かけました。

var hashValue: Int {
  return name.hashValue ^ state.hashValue}

しかしこれはあまり良いやり方だとは言えません。衝突を起こす例は簡単に作れます(この場合namestateの値を入れ替えただけでそうなる⁠⁠。つまりhashdosに対して脆弱ということで、iOSアプリならとにかく、Webサーバなどではたいへん困る。

そこでSE-0206が提案したのは、標準のハッシュ合成関数を用意して、手動で実装する場合にもそれを使うようにしようというものでした。Swift 4.2からはHashableには.hash()というメソッドが追加され、手動でHashableにする場合にもそれを使えということになりました。

func hash(into hasher: inout Hasher) {
  name.hash(into: &hasher)
  state.hash(into: &hasher)}

見てのとおり、これでアルゴリズムをSwift任せにしたうえでカスタマイズすることも可能となります。

ランダム値生成機能を標準装備へ

これまでSwiftはランダム値を生成する機能は標準では持たず、ユーザはその機能を持ったモジュールを各自勝手にimportしてきました。こんな感じです。

#if os(iOS) ¦¦ os(tvOS) ¦¦ os(watchOS) ¦¦
os(macOS)
  return Int(arc4random())
#else
  return random() // or Int(rand())
#endif

ここにもHashable同様、オレ定義による脆弱性リスクが隠れているのは明白です。SE-0202は標準でランダム値生成機能搭載を提案し、Swift 4.2で実装されます。使い方は次のとおり。

let randomIntFrom0To10 = Int.random(in: 0 ..< 10)
let randomDouble = Double.random(in:0.0 ..< 1.0)

たいへんわかりやすい。もっと早くそうすべきだったと筆者は率直に感じています。

IUOの撤廃

SE-0054で提唱されていたIUO(ImplicityUnwrapped Optional)の撤廃がSwift 4.2で実施されます。詳しくはSwift Blogの該当記事を参照していただくとして、筆者自身はSwift 2における++演算子の廃止同様にこの決定を支持します。ただし完全廃止ではなく、

func f() -> Int! { return 42 }
let i:Int = f()  f()! でない点に留意 

のようなケースは残るようです。筆者としては完全廃止でない点がちょっと残念です。演算子としての!つまり、

public prefix func !(v:Optional) {
  return value ?? fatalError()
}

さえあれば、explicit、つまり普通の暗黙ではないOptional型で事足りるとずっと考えているので。

排他的メモリアクセスの強制

次はたいへん行儀の悪いコードの例です。

extension Int {
    mutating func assignResultOf(_ f: ()->Int) {
        self = f()
    }
}
var x = 0
x.assignResultOf { x + 1 } // NO!
print(x)

何が行儀が悪いかというと、自分自身を書き換えるコードの中で自分自身を参照している点。メモリアクセスの排他性を破っているわけです。スクリプト言語のようにAST(抽象構文木)を仮想マシンで実行しているならとにかく、Swiftのようにネイティブコードを実行する言語ではたいへん危険です。

このようなコードは実行させる前に止めろというのがSE-0176の主旨で、Swift 4から徐々に実装されています。

たとえば、Swift 4.1のPlaygroundやREPLで実行すると、1と表示されますが、コンパイルしようとすると次のように止まります。

Simultaneous accesses to 0x1094e3000, but
modification requires exclusive access

つまりより精細な解析が可能な場合は、こうした危険を未然に防いでくれるわけです。

「徐々に」というのは、まだ検出できないケースもかなりあるから。たとえば次のコードは配列の列挙中に配列の要素を削除するという凶悪マシマシなコードなのですが、Swift 4.2-devでも素通りしてしまいました。

var a = [0,1,2,3]
a.forEach {
    print($0)
    a.removeLast()
}
print(a)

ただしSE-0176はこれもブラックリストに入れており、いずれは未然に防いでくれるようになると期待しています。

次回予告

というわけで次回からは前回予告どおり、Swift Package Managerを取り上げる予定です。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」⁠使いたい」⁠発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧