はじめに
世の中には変数への再代入を禁止しようという流派が存在します1。
詳細については「再代入 禁止」などでググってみると色々出てくるかと思いますが,要するにプログラムの見通しを良くしたいわけです。
ということを頭ではわかっていても,人間とは得てしてやらかしてしまうもので,再代入してしまうこともあります。
そこで,言語として再代入に対してエラーを吐いてもらえるととても助かります。
C#ではフィールドに対してreadonly
を付けることで宣言時およびコンストラクタ内以外での代入を禁止することができますが,ローカル変数に対してこのキーワードを付けることはできません。
JavaScriptにおけるlet
に対するconst
のような宣言が使えれば良いのですが,C#ではconst
の値はコンパイル時に決定されなければならないため使い勝手がよろしくありません。
既存の手法としてはIDisposable
を実装するラッパーを噛ませてusing var
で変数を宣言するというものがありますが,これはベストプラクティスであるとは言えないでしょう。
そこで,もっと真面目に(?)再代入を禁止しようということで,Roslynアナライザを使った検査を行うようにしてみました。
※ この記事は作成したアナライザを紹介するものです。アナライザの作成方法については別記事を参照してください。
使ってみよう
ReadonlyLocalVariables
というアナライザを作成しました。
このアナライザをプロジェクトに追加すると,ローカル変数への再代入が禁止されます。
インストール
NuGetパッケージとして公開しています。
コマンドラインでもGUIでもなんでも使ってインストールできます。
GitHubからクローンしてビルドして頑張っても大丈夫です。
(めんどくさいのでタグ付けてリリースとかはしていないです)
仕様
原則としてすべてのローカル変数に対する再代入が禁止されます。
using System;
void Foo()
{
var local = 0;
Console.WriteLine(local);
local = 1; // <-- エラー
Console.WriteLine(local);
}
しかし,何らかの都合でどうしても再代入したい場合にはReadonlyLocalVariables.ReassignableVariable
属性によって再代入を許可するローカル変数を指定することもできます。
using ReadonlyLocalVariables;
using System;
[ReassignableVariable("local")]
void Foo()
{
var local = 0;
Console.WriteLine(local);
local = 1; // <-- エラーにならない
Console.WriteLine(local);
}
ReassignableVariable
属性に同時に複数の識別子を与えたり([ReassignableVariable("foo", "bar")]
),複数の属性を使ったりすることもできます。
パラメータ
パラメータとして受け取ったものも再代入を禁止します。
ただし,out
パラメータ修飾子を付けたものについては帰る前に必ず値を設定しなければならないためこの限りではありません。
for
文
for
文の制御部分では再代入の検査を行いません。
ブロック内では検査されます。
for (var i = 0; i < 10; i += 2) // <-- OK
{
i -= 1; // <-- エラー
}
out
パラメータ修飾子付き引数
out
パラメータ修飾子を付けた引数は呼び出し先で必ず書き換えられることが保証されています。
そのため,既に宣言されているローカル変数をout
とともに渡すことも禁止します。
var i = 0;
if (int.TryParse("1", out i)) // <-- エラー
Console.WriteLine(i);
out var
による宣言か属性による許可によってエラーを解消できます。
クラスメンバ
クラスのフィールドについてはreadonly
という専用のキーワードが存在するため検査の対象とはなりません。
プロパティについてもgetterのみを書けばよいので検査しません。
タプル
既に宣言されているローカル変数を含むタプルへの代入もエラーとなります。
var x = 0;
var y = 0;
(x, y) = (1, 2); // <-- エラー
タプル内での変数宣言か属性による許可によってエラーを解消できます。
インクリメント/デクリメント
現時点ではインクリメントやデクリメント演算子による値の変更は検査されません。
これは新しい変数の宣言による修正等が困難であるためです。
また,インクリメント等で値を変更するような変数はcount
であったりindex
であったりと値が更新されることに意味があるであろう場合が多いと思われるということも理由として挙げられます。
そして,そのような場合にはプログラムの見通しを良くするために値の変更を禁止すると言ったことは意味がなく,適切な命名により値が更新され得ることを表現すべきであると考えます。
コード修正
ローカル変数への再代入を回避するために,コード修正機能を用いて新たな変数宣言を追加することができます(v2.0.0で実装)。
var local = 0;
Console.WriteLine(local);
-local = 1;
-Console.WriteLine(local);
+var local1 = 1;
+Console.WriteLine(local1);
新たな変数の追加によって変更されるべき変数の参照も更新されます(ローカル関数では正しく変更されない場合があります)。
変数名は元の変数名に数字を付けるだけの簡易的なものであるため,すぐにリファクタリングすることを推奨します。
また,どうしても再代入が必要な場合には属性を追加することもできますが,この処理も自動的に行うことができます。
+using ReadonlyLocalVariables;
+[ReassignableVariable("local")]
void Func()
{
var local = 0;
Console.WriteLine(local);
local = 1;
Console.WriteLine(local);
}
out
パラメータ修飾子やタプルに関するエラーについても同様にしてコード修正を行うことができます。
仕様について
この機能を実装するにあたって,仕様の決定が大きな問題となりました。
readonly
キーワードを使えるようにできればいいのですが,少なくとも私の技術ではかようなアナライザを書けなかったため諦めました。
識別子の命名方法(例えば大文字から始まる)などで許可/禁止を区別する方法も考えたのですが,再代入禁止派としては原則としてすべて禁止してしまっても良いのではないかと思い直して現在の仕様となりました。
その他
CS8032
アナライザを追加したプロジェクトをビルドする際にCS8032
が発生する場合があります。
その際は,以下のようにパッケージの参照を追加することで警告を解消することができます。
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="ReadonlyLocalVariables" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+ <PackageReference Include="Microsoft.Net.Compilers.Toolset" Version="4.3.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers;</IncludeAssets>
+ </PackageReference>
</ItemGroup>
</Project>
さいごに
初めての(Roslynアナライザ作成&&初めてのNuGetパッケージ公開)ということで賢明な読者の皆様からすると奇妙奇天烈なことになっているかと思います。
改善点などお知らせ頂けると大変うれしいです。
また,上にも書きましたが仕様についてもアドバイスをいただけるとありがたいです。
-
「変」数とは何だろうとか気になってしまいますが気にしないことにします。 (コンパイル時に値が決定されないという意味で定数ではなく変数,といったところでしょうか) ↩