Swift 5では,変数がプログラムの他の部分で変更されている間,別の名称を使ってアクセスできないようにすることで,Swiftプログラムのメモリ安全性が向上している。この変更は,既存のアプリの振る舞いにも,Swiftコンパイラ自身にも重大な影響を及ぼす。
メモリへの排他アクセスの問題は,さまざまな状況で現れる。多くはコンパイラが静的にキャッチ可能だが,ランタイム時にのみ処理可能なケースも存在する。クロージャのエスケープによる排他侵害,クラス型プロパティ,静的プロパティ,グローバル変数などがそれに含まれる。
問題を説明するために,一般的なケースについて考えてみよう。関数のinout
引数として変更される変数が,その関数内で実行されるクロージャの引数としても使用されることで,同じ変数が同一スコープ内で,2つの異なる名称でアクセスされる場合である。
func modifyTwice(_ value: inout Int, by modifier: (inout Int) -> ()) {
modifier(&value)
modifier(&value)
}
func testCount() {
var count = 1
modifyTwice(&count) { $0 += count }
print(count)
}
この例では,modifyTwice
のinput
引数として使用されているcount
が,同時にmodifier
でも使用されることによって問題が発生する。この影響として,print
文で何が出力されるべきなのかが不明確になっている。count
が最初にインクリメントされた時,その値は2に増加する。では,2回目の加算が実行された時,$0
の値に加算されるcount
の値は何だろうか?メモリ操作は必ずしも即時に実行されないので,これにはさまざまな要因が関わってくる。さらに悪いことに,コンパイラが最適化を行うことで,このようなシナリオをますます複雑にする可能性があるのだ。
この問題は,前述のような,異なる変数名によるメモリの同時変更の予測不能性だけではなく,これがコンパイラに課す複雑さにも関係している。
これは予期しない,混乱を招く結果をもたらす可能性があります。それと同時に,異常な状況下でもプログラムの基本的健全性(クラッシュや未定義動作を行わないこと)を保証するために,コンパイラや標準ライブラリの実装に対しても,極めて保守的な動作を強いることになります。
これはすべて,Swift 5を使用してコンパイルされたアプリケーションは,排他アクセス違反があった場合は実行時にクラッシュする,という意味になる。この動作は従来,Swift 4コンパイラのデバッグモードでは使用できていた。従って,Runtimeモードでのみテストされたプログラムについては,Swift 5でコンパイルするとクラッシュする危険がある。
排他アクセス違反を修正するための一般的なアプローチは,データのコピーを作ることだ。先程の例であれば,次のようになる。
func modifyTwice(_ value: inout Int, by modifier: (inout Int) -> ()) {
modifier(&value)
modifier(&value)
}
func testCount() {
var count = 1
let increment = count
modifyTwice(&count) { $0 += increment }
print(count)
}
このような問題が起きた場合,排他アクセス違反チェックを無効にするという方法もあるが,このプラクティスは決して推奨できない。
ランタイムチェックを無効にすればパフォーマンス低下を回避できるかも知れませんが,排他違反が安全であるということにはなりません。チェックが無効になれば,排他ルールの遵守はプログラマの責任になるのです。
詳細や他の例については,オリジナルの記事を参照してほしい。