WWDC 2018総括
というわけで前回 予告どおり、WWDC 2018を本記事で振り返るわけですが、いやあ、地味なWWDCでしたね。新規ハードウェアの発表ゼロ、次期macOSであるMojaveの一番のウリはDark Mode(写真1 ) 。そしてSwift 5は今年ではなく来年。しかし地味なだけに、派手さに隠れて目立たなかったSwiftの問題を地味に片付けていったのは筆者としてはむしろ好感が持てました。
写真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
というプロトコル準拠を宣言する必要があるのは、Equatable
やHashable
と同様です。すべての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-0190 でhasTargetEnvironment()
が導入されたことにより、それが次のように自然と書けるようになります。すでにSwift 4.1で実装されています。
#if hasTargetEnvironment(simulator)
print("Simulator")
#else
print("Device")
# endif
Hashableプロトコルの刷新
SE-0185 がSwift 4.1で実装されたことで、Equatable
とHashable
の自動生成がすでに使えるようになっています。ストアドプロパティすべてがEquatable
やHashable
に準拠している型なら、型宣言のところでEquatable
やHashable
を付けるだけで……、
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
はハッシュ値生成には不要なので、name
とstate
のハッシュ値を何らかの形で合成して.hashValue
を生成すればよさそうに見えます。Swift 4.1以前では、次のようなコードをよく見かけました。
var hashValue: Int {
return name.hashValue ^ state.hashValue}
しかしこれはあまり良いやり方だとは言えません。衝突を起こす例は簡単に作れます(この場合name
とstate
の値を入れ替えただけでそうなる) 。つまり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を取り上げる予定です。
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「 使いたい」「 発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)