書き換えられる(mutable)構造体を作ると事故る問題を解決するためにアナライザー作りました。

mutable 構造体

一般論としては、構造体を mutable に作ると事故ります。 要するに、「書き換えたつもりが、実は書き換えてたのはコピーであって元の値は書き換わってない」的なやつ。 なので、たいていの場合は「構造体は immutable(書き換え不能)に作れ」という指針になります。

その一方で、まれに、ヒープ確保を避けるために mutable な構造体を作りたい場合があります。 フィールドとしてクラスに埋め込んで使ったり、ローカル変数に確保してref引数でメソッドに渡す想定で作ります。

例えば、corefxlab が作りかけてるResizableArrayとか、neueccさんが作ってるUtf8Json中のJsonWriterとか、 書き込み先の配列(満杯になったら確保しなおす)と現在の書き込み位置だけを持つ小さい型なんですが、 構造体で作られています。 自分が昔作ったのだと、Lazy型のためにアロケーションが発生するのがいやすぎて、これの構造体版を作ったこととか。

もちろん、コピーが発生したら事故ります。 「JsonWriterWriteしたつもりが何も書き込まれていない(コピーに対して書き込んでた)」とかやらかしがちです。

コピーの方を禁止する

mutable 構造体で問題が起きるのはコピーが発生するせいです。 なので、コピーの方を禁止すれば、構造体が mutable でも問題は起こしません。

ということで作ったのがこちら。

本当はずっと昔から作ろうとは思ってたんですが。 「Analyzer with Code Fix のプロジェクト テンプレートが SDK-based csproj になったら本気出す」って思ってたらつい最近になってようやく… 先週のブログに書いたAnalyzer 用の自作プロジェクト テンプレートを作った動機もこれ用。

どういうコードが禁止されるかは、テスト用のコードを見てみてください。サブフォルダーの Source フォルダー以下にある csx の、❌ コメントを入れている行を禁止。 以下のような感じでコンパイル エラーが出ます。

non-copyable error

おまけ: 言語機能化の提案

コピー禁止って、必要となる場面がそんなに多くないわりに、解析するの結構大変なんですよね… 自分が作った NonCopyableAnalyzer も完璧なものではないです。 例えば、ジェネリクスが絡むと誤判定あり。

static void Main()
{
    var x = new NonCopyableStruct();
    var illegal = x; // ちゃんと禁止
    var misjudged = Copy(x); // 本来禁止すべき。でも現実装だと通っちゃう
}

public static T Copy<T>(in T x) => x;

「Non-cobyable 構造体」を言語機能として入れてほしいっていう要望とかも出てたりはするんですけども。 言語に組み込むには上記のような誤判定がつらいかなぁという感じ…

こういう、「コピー禁止」があると、昔ブログに書いた非ガベコレな高効率メモリ管理とかも実現できたりするんですが、 これは誤判定がちょっとでも残るとやばいやつなので、相当難しそう…