よくきたな。おれは毎日ものすごい量のコードをコミットしているが、publicにする予定はない。しかし、C#でお前が自作クラスをうまくリストに追加できないばあいについて書いておいた。
おれは逆噴射聡一郎先生ではなく、この文体はパロディだ。
同じ顔ぶれの男たち
おまえは真の男であるから、むろん、クラスDefeatedManを持つはずだ。
中にはIdとNameがある・・・。こんなだ。
public class DefeatedMan
{
public int Id { get; set; }
public string Name { get; set; }
public DefeatedMan()
{
}
}
もし望むならDefeatedTimeなどをついかしてもよい。
もちろんおまえは真の男であり、今まで打ちのめした腰抜けはひとりや二人や三人などではすまない。
クラスDefeatedManをあつかうときは、リストにして持つことになるだろう。配列ではないのは、おまえはあと何人の男と対峙するのか知らないし、予測もつかないからだ。
てはじめに1000人のDefeatedManをついかする。一つ一つやっつけていては日が暮れるためループ処理にまかせる。
List<DefeatedMan> defeatedMans = new List<DefeatedMan>();
DefeatedMan man = new DefeatedMan();
for (int i = 0; i < 1000; i++)
{
man.Id = i;
man.Name = $"John_{i}";
defeatedMans.Add(man);
}
これで万事うまくいったとおもうと大間違いだ。
満を持してVisualStudioのデバッグ機能で変数の中身をみてみろ。同じ顔ぶれがつらつらと入っているだけだ。
おれは配列の0個目の中身を改めているが、IDは999である。以下、すべての中身はJohn_999である。
なぜか? それは参照型だからだ。
参照型とゆうのは、つまり、同じメモリ上に配置されているとゆうことを意味する。
自作クラスDefeatedManは参照型だ。
DefeatedMan man = new DefeatedMan();
manは確かにDefeatedMan型の変数で、newされたが、ループの外で1回きりしかされていない。
お前は皿の上にアメリカンクラッカーを乗せ、思い思いにチーズや唐辛子やガーリック・パウダーなどをまぶしているが、次のクラッカーに手を伸ばすとき、いっこうに新しい皿を出さず、同じ皿でどうこうしている。ゆえに皿は一枚でしかなく、さいごには、Countはみせかけだけで、おまえがさいごに出したクラッカー1まいしかない。すべてのIDが999なのはそのためだ。おまえが相手取ってきた手下どもはまるでニンジャだ。実体は一人しかいない。
お前がやるべきことはただ一つ。ループの最中に、defeatedMansをただしくnewしてやることだ。
List<DefeatedMan> defeatedMans = new List<DefeatedMan>();
for (int i = 0; i < 1000; i++)
{
DefeatedMan man = new DefeatedMan();
man.Id = i;
man.Name = $"John_{i}";
defeatedMans.Add(man);
}
これでリストの要素は独立し、多種多様なdefeatedManの人生が生じる。
お前はこの操作により過去と決別し、お前自身の人生を歩むことができる。
おまえがしくじった理由にも100くらいは思い至る。おまえはおそらくメモリは節約しろといわれてきたし、そういった教えがおまえをこれまでも幾度となく助けてきたからだ。
newをするたびに新しい皿を出すのをきらったのかもしれない。だがお前のクラスは・・・参照型である。いちいちゆってきたりはしないため、強靭なWILLによってメモリを確保しろ。
とにかく、自作クラスやリストを=で代入するときは気をつけろ。お前は新しい皿をサーブしているか? それを常に意識し続けろ。自作クラスやListを使うときはとくに。
List<DefeatedMan> defeatedMansOld = new List<DefeatedMan>();
// TODO:defeatedMansOldに好きなだけ要素を追加しろ
List<DefeatedMan> defeatedMansNew = new List<DefeatedMan>();
// あほの例
defeatedMansNew = defeatedMansOld;
// GOODな例
foreach (DefeatedMan dm in defeatedMansOld)
{
// 各要素を新しいインスタンスとしてコピー
defeatedMansNew.Add(new DefeatedMan(dm));
}
コピー・アンド・ペーストでは人生はうまくはゆかない。特にリストは1要素ずつループさせて詰め込んだりなどしろ。それと自作クラスにリストが含まれるばあい、CloneやDeepCopyToなんかを実装しておけ。
using System.Collections.Generic;
public class DefeatedMan
{
public int Id { get; set; }
public string Name { get; set; }
public DefeatedMan()
{
}
public DefeatedMan Clone()
{
return new DefeatedMan
{
Id = this.Id,
Name = this.Name
};
}
}
public class DefeatedManManagement
{
public List<DefeatedMan> DefeatedMen { get; set; }
public DefeatedManManagement()
{
DefeatedMen = new List<DefeatedMan>();
}
// リスト全体のクローンを作成するメソッド
public List<DefeatedMan> CloneDefeatedMen()
{
List<DefeatedMan> clonedList = new List<DefeatedMan>();
foreach (var defeatedMan in DefeatedMen)
{
clonedList.Add(defeatedMan.Clone());
}
return clonedList;
}
(ほ足)Record型・・・
C#のバージョン9.0以降においては、Record型が導入された。これは間違いなくクラスらしいが、"値ベースの等価性" を使用するクラスだ。これを使うなら、お前がメモリとか皿とかをうだうだ気にする必要はない。
おれの開発環境は、おおむね.Net Frameworkであり、4.8ではせいぜいがC#のバージョン7.3のため、あまり使ってはこなかった。
このばあい、クラス自体は自作クラスであるため参照型だが(Microsoftの公式リファレンスに参照型を定義します、とある)、「不変性」があるため値は変わらない・・・とゆうことになる。
with型とゆうのがあるのも教えてもらったため、そのうち追記したい。9.0の機能らしい。
参考
C#でわかる値渡し、参照渡し
未確認飛行C 参照渡し
copy --- 浅いコピーおよび深いコピー操作
※これはpythonだが…。