Defaulting non-Void functions so they warn on unused results
01 何が問題だったのか
Swift 2 では、戻り値のある関数・メソッドを呼び出して結果を使わなくても、コンパイラは何も言いませんでした。たとえば次のコードは警告なしにコンパイルされ、加算の結果は黙って捨てられます。
4 + 5 // 警告なし
プレイグラウンドのように結果そのものを眺めたい場面を除けば、このようなコードはたいていバグです。結果を代入し忘れた、あるいは使う予定の値を書き漏らした、というケースが大半で、実プロジェクトでは発見されにくい不具合の温床になっていました。
「使ってね」を明示する既定
当時は逆に、「この関数の戻り値は使うべき」ということを @warn_unused_result というアトリビュートで明示する仕組みでした。付けなければ、戻り値を捨てても警告は出ません。
@warn_unused_result(mutable_variant="sortInPlace")
public func sort() -> [Self.Generator.Element]
これは主に、ミュータブル版とイミュータブル版の対(sort と sortInPlace など)を区別する用途で使われていました。しかし、戻り値を捨てたら警告すべきケースのほうが圧倒的に多く、既定が逆になっているのは実情に合いません。標準ライブラリやサードパーティのAPIを一つずつ @warn_unused_result で装飾していくのも、書き漏らしが起こりやすく現実的ではありませんでした。
スネークケース由来の不揃い
加えて @warn_unused_result や mutable_variant はスネークケースで、全体がローワーキャメルケースに統一されつつあったSwiftの中では浮いた存在でした。命名規則の観点からも、置き換えたい対象になっていました。
02 どのように解決されるのか
既定の向きを反転します。戻り値の型が Void ではない関数・メソッドを呼び出して結果を使わないと、コンパイラが警告を出すようになります。戻り値を捨てても問題ないAPIについては、宣言側で @discardableResult を付けることで警告を抑えます。@warn_unused_result は廃止されます。
基本の使い方
通常の戻り値つき関数は、結果を使わずに呼び出すと警告になります。
func g() -> Int { return 42 }
g() // 警告: 結果が使われていません
_ = g() // OK: 明示的に捨てる意図を示している
戻り値を捨てる呼び出しを正当な使い方として認めたいときは、宣言に @discardableResult を付けます。
@discardableResult
func f() -> Int { return 42 }
f() // OK: 手続き的に呼び出してよい
戻り値が Void の関数はそもそも対象外です。これまでどおり警告は出ません。
func h() { } // 対象外
呼び出し側での打ち消し方
APIに @discardableResult が付いていないときでも、呼び出し側で一時的に結果を捨てたい場合があります。そのときは従来どおり _ = を使います。
_ = g() // 意図的に捨てていることを明示
_ = は「結果を捨てる意図が呼び出し側にある」ことを示すコードレビュー上のマーカーとしても機能します。一方で @discardableResult は「APIの設計として捨ててよい」ことを宣言側で表明する手段です。役割が違うため、用途に応じて使い分けます。
関数値に代入したときの扱い
@discardableResult は関数の宣言に紐づく属性で、関数値の型自体には乗りません。そのため、変数に代入すると属性は引き継がれず、呼び出し時に警告が戻ってきます。
@discardableResult
func f() -> Int { return 42 }
let c1: () -> Int = f // OK
let c2: () -> Void = f // エラー: 戻り値型が合わない
let c3 = f // 代入で属性は失われる
c3() // 警告: 結果が使われていません
_ = c3() // OK
ミュータブル版・イミュータブル版の対応付け
@warn_unused_result には mutable_variant や message といった引数があり、sort() を結果未使用で呼んだら sortInPlace() を勧める、といった案内に使われていました。この役割はドキュメントコメントに移ります。具体的には、ドキュメントコメント側で MutatingCounterpart と NonmutatingCounterpart というフィールドを使って、対応する別名を紐づけます。コンパイラの検査ではなく、QuickHelpやコード補完側で扱われる情報になります。
C/Objective-C からインポートされたAPIの扱い
この提案の対象は純粋なSwiftコードです。ClangインポータでCやObjective-CのAPIをSwiftに取り込む場合、ソース側で ((warn_unused_result)) が付いていない関数には、自動的に @discardableResult 相当の扱いが与えられます。つまり、インポートされた関数の戻り値を捨てても、既定では警告は出ません。膨大な既存APIに対して一斉に警告が出ることを避けるための措置で、将来的に実コードへの影響を見極めた上で、インポートAPIにも既定を広げるかどうかは別途検討されます。