Removing var from Function Parameters
01 何が問題だったのか
初期のSwiftでは、関数の引数宣言に var を付けて引数をミュータブルなローカル変数として扱うことができました。
func foo(var i: Int) {
i += 1 // OK。ただし呼び出し側からは変化が見えない
}
この機能は一見便利に見えますが、実際には混乱のもとになっており、言語の一貫性を損ねていました。
inout との意味の取り違え
もっとも大きな問題は、var 引数が inout 引数と混同されやすいことです。どちらも「引数をミュータブルに扱える」という見た目を持ちますが、意味はまったく異なります。var 引数はローカルコピーを変更するだけで呼び出し側には反映されませんが、inout 引数は呼び出し側の値そのものに書き戻されます。
func doSomethingWithVar(var i: Int) {
i = 2 // 呼び出し側の値は変わらない
}
func doSomethingWithInout(inout i: Int) {
i = 2 // 呼び出し側の値が 2 になる
}
var x = 1
doSomethingWithVar(x)
print(x) // 1
doSomethingWithInout(&x)
print(x) // 2
var と書くだけで呼び出し側の値が変わると誤解したり、値型に参照型のような挙動を与えられると誤解したりするケースが後を絶たず、バグや混乱を招いていました。
パターンマッチとの整合性もない
Swiftの if / while / guard / for-in / case などの文では、var や let は「パターン」の一部として束縛を導入する役割を持ちます。しかし関数の引数リストはそもそもrefutable patternではなく、そこに現れる var はパターンとしての var とも意味が異なります。結果として、同じ var というキーワードが文脈ごとに別の意味を持ち、言語ルールとしての筋が通らない状態になっていました。
得られる利点が小さい
var 引数の唯一の実用的な利点は、関数本体の冒頭で var i = i のようにシャドーイングするコードを1行省略できる、という程度のものです。その小さな利便性のために inout との混同を招いているのは割に合わず、機能として正当化しづらい状況でした。
02 どのように解決されるのか
関数・メソッド・イニシャライザ・クロージャなどの引数宣言から var を取り除き、引数は常にイミュータブルなものとして扱うようにします。Swift 2.2 では var 引数が非推奨警告となり、Swift 3 では構文エラーになりました。
移行方法
引数をミュータブルに扱いたい場合は、関数本体の冒頭でローカル変数に束縛し直します。これは機械的な変換で対応できます。
// Before:
func foo(var i: Int) {
i += 1
// i を使った処理
}
// After:
func foo(i: Int) {
var i = i
i += 1
// i を使った処理
}
こうすることで、従来の var 引数と同じく「ローカルコピーを書き換えられる」挙動を保ちつつ、inout のような書き戻しの意味は持たないことが明示されます。
一方で、このシャドーイング自体がアンチパターンになっている場合も少なくありません。多くのケースでは、そもそも引数を書き換える必要があるのか、より適切な関数設計に直せないかを見直すきっかけとして扱うのが望ましいでしょう。
inout との違いが明確になる
この変更により、「引数の値を呼び出し側に反映させたい」ときは inout を使うしかなくなります。var と inout が同じ引数位置で競合する状況がなくなり、inout の意味(値型に対して明示的に書き戻しを行う)が素直に伝わるようになります。
func increment(inout i: Int) {
i += 1
}
var x = 1
increment(&x)
print(x) // 2
パターンとしての var への影響はない
この提案は関数の引数宣言から var を取り除くことにスコープを絞っており、if case var ... や for var x in ... のようなパターンとしての var 束縛はそのまま残ります。提案の当初案ではrefutable patternにおける var の削除も含まれていましたが、Swift 2 のコードで広く使われていたミュータブルな束縛パターンへの影響が大きすぎるため、最終的には関数引数に限定した形で採択されています。