Swift 1.2 の不変値変数で定義と初期化を分けられる構文について考えてみる

コラム

Swift 1.2 で不変値変数の定義と初期化を分けて書けるようになりましたけど、分けない方が何かと都合が良いようにも感じました。


Swift 1.2 で不変値変数の定義と初期化を分けて書けるようになりましたけど、なんとなくイマイチな仕様のようにも感じました。

let x : SomeThing

if condition {

	x = foo()
}
else {

	x = bar()
}

定義のところで初期値を与えず、その後のコードの中で初期値を与える書き方です。

まるで DVD-R みたいに 1 度だけ書き込める変数な感じで、初期化して初めて読み取れるようになります。

従前通りの定義と初期化を同時に行うこともできて、分けなければ書けないこともないので、使う側の使い方に終始するでしょうけれど、できるだけlet で定義したのと同じタイミングで初期値を与えるのが良いように感じました。

従来からの条件による初期値の指定

そもそも、これまでの Swift 1.1 でも、条件によって初期値を変えることは簡単でした。

let x:SomeThing = condition ? foo() : bar()

もう少し複雑な処理が必要でも、クロージャーで普通に書けていました。

let fooBar:() -> SomeThing =  {
	
	if condition {
		
		return foo()
	}
	else {
		
		return bar()
	}
}

let x:SomeThing = fooBar()

何よりもこの『今まででも普通に理想的に書けていた』というのが、これから綴る定義と初期化を分けるメリットが感じられない理由に対する端的な結論です。

定義と初期化の分離について

上記のように普通に条件で初期値を切り替えられていた背景を踏まえ、その上で定義と初期化を分けたとき、どんなところに不安な点を感じたのかを綴ってみたいと思います。

初期化の不明瞭化

クロージャーを使った場合は一見すると定義と初期化を分けるのと似たような感じになるので意見が分かれるかもしれませんが、少なくとも定義の行でクロージャーを呼び出すことで、なにでその不変値変数を初期化しているのかが直ぐに分かります。

定義と初期化を分けた場合でも、そんなに離れたところで初期化することはないとは思いますけど、どこで初期化しているかを把握しないといけない手間が増えるので、コードの可読性が下がりそうにも思えます。


もし定義と初期値を分離するかどうかに関わらず、複雑な初期化をしないといけないとなったとしても、定義と初期化が分けられないと決まっていれば、必ずその一文で初期値が定まる保証があります。

この安心感はコードを読む上で価値が大きいように感じるのでした。

初期化されない可能性

あと、初期化しないケースが残るところも気になりました。

これについてはコンパイラがちゃんと指摘してくれて、ビルド段階で発見されるのでそれほど問題にならない気はしますが、そういうケースを考慮しないといけない点が増えることも、些細ながらデメリットにも思えます。

たとえば switch 文で不変値変数を初期化しているように見えて、実は一部で初期化を遅らせている、みたいなことも想定しないといけなくなります。

let x:SomeThing

switch e {
	
	case A:
		x = foo()
		
	case B:
		break
		
	default:
		x = bar()		
}

さすがにこういうコードは肩すかし的で、定義と初期化をどうするか以前に考えるべき問題なようにも思いますが。


ともあれこうしてみたときに、今回であればswitch 文の中で変数x が初期化されているかどうか読んでみないとわからないところが、初期化を後に遅らせる構文の良くないところのように感じています。

逆に考えると、不必要に初期化をしないで済むケースがメリットになるかもしれませんけど、その場合はきっと不変値変数自体の定義場所自体を見直した方が良いでしょう。

未初期化なまま入力候補に表示される

あと、プログラミングで参照する変数を書こうとしたときに、定義だけされてまだ初期化されていない不変値変数の名前が候補に上がるところも気になります。

可変値変数では普通なことでしたし、うっかりそれを使ってもコンパイルエラーになるだけなのでほとんど問題はないのですけど、今までの不変値変数であればそういう煩わしさがなかったので、あくまでも今までと比べてみれば些細ながらもデメリットのように感じました。

可能な限り定義と初期値はセットで行う

今のところ、不変値変数の定義と初期化を分けることのメリットはみつからないので、通常であれば、可能な限りは定義と初期化は一緒に行うのが良いんじゃないかと思ってます。

定義に続けて等価演算子を書けば、定義と合わせて初期化されることが保証されるので、そうすれば何も心配いらなくなります。

どうしても分けないと書けないときがあれば、分けて初期化をするのが良いと思いますけど、そのときでも出来る限り簡潔に、その直後くらいで初期化を完全に済ませるように心がけたいところに思います。

良さそうな使い方を考える

そんな不変値変数の定義と初期化を分けることについて、もしかしてメリットがある場面もあるんじゃないかと考えてみましたが、今のところはそれといって良いことはなさそうに思えています。

ひとつの条件で複数の不変値変数を初期化するとき

便利かもしれないと思った場合ですけれど、たとえば『ひとつの条件で複数の不変値変数の初期値を切り替えたいようなとき』というのがあるかもしれません。

let x:SomeThing
let y:SomeThing

if condition {
	
	x = foo()
	y = bar()
}
else {
	
	x = bar()
	y = foo()
}

ただ、これもそんなセットであるなら、入れ子の構造体かタプルを使ってまとめた方が、まとまりもコードに含められて、扱いが良いのかなとも感じました。

構造体を使うケース

struct SomeValues {
	
	var x:SomeThing
	var y:SomeThing
}

let v = condition ? SomeValues(x: foo(), y: bar()) : SomeValues(x: bar(), y: foo())

タプルを使うケース

let v2:(SomeThing, SomeThing) = condition ? (foo(), bar()) : (bar(), foo())

これらの例はとりあえず埋め込んでみた形なので読みにくくなってますけど、クロージャーなどの関数を併用すればもっと分かりやすく書けるはずです。