はじめに
classとstructの違いをきちんと意識せずに使ってたらエラーが出たよ、というだけの話です。常識と言えば常識な感じのネタなので、こんなの常識、と思う方はスルーしてください😅
- こんなの何に使うの?
使いたい時もあるんです。 - 構造体はイミュータブルが常識では?
それは知ってる。 - ただの配列のほうがいいんじゃ?
話せば長くなるので。
ことの発端
パフォーマンスが気になる用途で、構造体のList<>を使おうとした時でした。
// 再現例
List<Point> points = new List<Point>();
points.Add( new Point( 0, 0 ) );
points[ 0 ].X++; // エラーCS1612
このコードは、コンパイルエラーになります。
エラーCS1612 変数ではないため、'List.this[int]' の戻り値を変更できません
何が起きてる?
簡単なコードで再現してみましょう。
// あくまで再現例
class Points {
private Point[] points = new Point[10];
public Point this[int index] {
get {
return points[index];
}
set {
points[index] = value;
}
}
}
Points points = new Points();
points[0].X = 1; // エラーCS1612
Pointは構造体(struct)なので、値型です。インデクサのgetは、int型2個分、合わせて8バイト分のメモリ領域を、points配列の中からコピーして直に戻してきます。
コピーしてきた戻り値を変更しても無駄なのは、お気づきの通り。それを指摘しているのが、この、エラーCS1612です。
さてどうする - その1
パフォーマンスを犠牲にして、新しいPointを作って差し替えます。これなら構造体もイミュータブルでいられます。素晴らしいね!
points[0] = new Point( points[0].X + 1, points[0].Y );
さてどうする - その2
パフォーマンスを犠牲にして、構造体をあきらめてクラスにします。クラスなら参照型なので、自由に変更できます。
class MyPoint {
public int X;
public int Y;
}
結局どうした?
パフォーマンスを犠牲にしたくなかったので、独自のListモドキを実装しました。その中で使ったのが参照戻り値というやつ。
// あくまで再現例
class Points {
private Point[] points = new Point[10];
public ref Point this[int index] {
get {
return ref points[index];
}
}
}
Points points = new Points();
points[0].X = 1; // エラーにならない!
points[1] = new Point( 1, 1 ); // まるごともいけるよ!
こうすることで、getインデクサは、points配列の中の一つの要素の参照(ポインタ) を返すようになります。これによって、参照型の場合と同じように、メンバ変数を自由に変更できるようになります。
またこの場合、setインデクサは必要ありません。代入式の左辺に書かれた場合は、getインデクサが返す参照先のメモリ領域に、直にコピーされます。
今回はインデクサ絡みの問題でこの問題に気付いたのですが、構造体を返すプロパティでもよくあるケース、みたいですね。