コレクション【C#】

C#文法:コレクション:D09_ToListでコピーする

今回は ToList を取り上げます。ToList は何かというと、List のコピーを作るためのメソッドです。まずはフォームに「ToList」というボタンを作り、ダブルクリックしてクリックイベントを生成しておきます。

図1 「ToList」ボタンを追加する

図1 「ToList」ボタンを追加する

おさらい:IList<T> をそのまま返すと元ネタに影響が出る

これまでのレクチャーで、コレクションをそのまま返すと元ネタに影響が出てしまう、という話をしてきました。たとえば Products クラスの GetData メソッドは、内部で持っている _values を IList<string> としてそのまま返しています。

図2 IList<string>をそのまま返しているGetDataメソッド

図2 IList<string>をそのまま返しているGetDataメソッド

Products.cs:GetDataメソッド

public static IList<string> GetData()
{
    return _values;
}

この場合、クライアント側で Add されると元ネタに影響が出ます。もう一度やってみましょう。Products.GetData() で IList<string> が返ってくるので、そのまま Add ができます。そのあと、もう一度 GetData で取得し直します。

Form1.cs:取得したリストにAddしてから、もう一度取得する

private void button22_Click(object sender, EventArgs e)
{
    var a = Products.GetData();
    a.Add("CCC");

    var b = Products.GetData();
}

図3 GetDataで取得したリストにAddし、もう一度GetDataする

図3 GetDataで取得したリストにAddし、もう一度GetDataする

ブレークポイントを置いて実行し、ToList ボタンを押します。最初に取得した時点では、a は2件です。

図4 取得直後のaは2件

図4 取得直後のaは2件

Add を実行すると a は3件になります。そこから改めて GetData を呼び出すと、b も3件になっています。中身を見ると、追加した「CCC」が入っています。

図5 もう一度GetDataすると3件が返ってくる

図5 もう一度GetDataすると3件が返ってくる

このように、取ってきた List に対して編集をすると、元ネタに影響が出てしまいます。a と Products クラスの中の _values が、同じ List を指しているからです。これを ReadOnly 系で防ぎましょう、という話をこれまでしてきました。

コピーを返せば問題ない

しかし、そもそもコピーを返してしまえば問題はありません。IList<T> を返却するとしても、元ネタそのものではなく、そのコピーを返すようにします。それを行うのが ToList です。

Products クラスに、IList<string> を返す GetDataCopy メソッドを追加します。中身は _values.ToList() とします。

図6 ToListはIEnumerable<T>からList<T>を作成する拡張メソッド

図6 ToListはIEnumerable<T>からList<T>を作成する拡張メソッド

ツールチップを見ると、ToList は IEnumerable<T> に対する拡張メソッドで、IEnumerable<T> から List<T> を作成する、と説明されています。つまり ToList を呼び出すと、List を新しく作り直します。そのため、返したリストをどう編集されても元ネタには影響が出ません。

Products.cs:GetDataCopyメソッドを追加する

public static IReadOnlyList<string> GetDataList()
{
    return _values.AsReadOnly();
}

public static IList<string> GetDataCopy()
{
    return _values.ToList();
}

図7 ToListでコピーを返すGetDataCopyメソッド

図7 ToListでコピーを返すGetDataCopyメソッド

コピーを返した場合の動作を確認する

フォーム側の呼び出しを、Products.GetData() から Products.GetDataCopy() に変更して実行します。

Form1.cs:GetDataCopyに変更する

private void button22_Click(object sender, EventArgs e)
{
    var a = Products.GetDataCopy();
    a.Add("CCC");

    var b = Products.GetData();
}

図8 呼び出しをGetDataCopyに変更する

図8 呼び出しをGetDataCopyに変更する

最初は2件です。Add を実行すると、a は3件になりました。

図9 Addした結果、aは3件になる

図9 Addした結果、aは3件になる

ここで元ネタを取りに行きます。すると、b は「ボール」「スパイク」の2件のままです。

図10 元ネタのbは2件のまま

図10 元ネタのbは2件のまま

このようにコピーを返してしまえば、元ネタとは切り離された別の List を渡していることになるので、これはこれで安心です。

ToListのデメリット

ただし、デメリットとして多少の負荷がかかります。そのままの List を、型をインタフェースにして戻すだけの場合と比べると、ToList は全部の要素を新たに別のメモリへ入れ替えてから返却します。その分の負荷がかかるため、件数が多い場合や頻繁に呼び出される場合には、パフォーマンスに気をつける必要があります。

(参考)そのまま返す場合とコピーを返す場合の違い

// そのまま返す:型をインタフェースにして戻すだけ(負荷はほぼない)
public static IList<string> GetData()
{
    return _values;
}

// コピーを返す:全要素を別のメモリに入れ替えて返す(件数分の負荷がかかる)
public static IList<string> GetDataCopy()
{
    return _values.ToList();
}

クライアント側でToListする

ToList は、返す側で使うだけではありません。もらう側で使うこともできます。IEnumerable<string> を返す GetDataB メソッドの結果を、変数 c で受け取ります。

図11 GetDataBはIEnumerable<string>を返す

図11 GetDataBはIEnumerable<string>を返す

IEnumerable<T> は、ぐるぐると繰り返すことしかできません。しかし、これを ToList することで、クライアント側が自分でコピーを作ってしまうことができます。c を ToList して d を作り、その d に対して Add します。そして最後に、もう一度元ネタを取得して確認します。

Form1.cs:IEnumerable<T>でもらって自分でコピーを作る

private void button22_Click(object sender, EventArgs e)
{
    var a = Products.GetDataCopy();
    a.Add("CCC");

    var b = Products.GetData();

    var c = Products.GetDataB();
    var d = c.ToList();
    d.Add("ddd");

    var ee = Products.GetData();
}

図12 IEnumerable<T>で受け取ったcをToListしてdを作る

図12 IEnumerable<T>で受け取ったcをToListしてdを作る

実行して確認します。c は2件です。この c をコピーして d を作るので、d も2件です。

図13 cをToListしたdは2件

図13 cをToListしたdは2件

d に Add すると、d は3件になります。ここで元ネタを見に行っても、2件のままです。

図14 dにAddしても元ネタは2件のまま

図14 dにAddしても元ネタは2件のまま

IEnumerable<T> を返して、あとはクライアントに任せる

このことから、基本的には IEnumerable<T> を返すようにしておいて、もらった側で自由にコピーして好きに使ってください、という形にするのもよい方法です。それであれば、わざわざ ReadOnly 系にする必要もありません。

基本的には IEnumerable<T> で返して、加工はクライアント側で自由にやってもらう。その代わり元ネタには影響が出ないので、クライアントはクライアントで独自にやっているだけ、という返し方です。

図15 IEnumerable<T>を返すメソッドだけを用意しておけばよい

図15 IEnumerable<T>を返すメソッドだけを用意しておけばよい

この方針であれば、Products クラスは IEnumerable<T> を返すメソッドだけを持っておけば十分です。クライアント側は、加工したければ ToList して使うことができます。

Products.cs:IEnumerable<T>を返すメソッドだけあればよい

public static IEnumerable<string> GetDataB()
{
    return _values;
}

Form1.cs:加工したければクライアント側でToListする

var c = Products.GetDataB();   //繰り返ししかできない
var d = c.ToList();            //自分でコピーを作る
d.Add("ddd");                  //元ネタには影響しない

まとめ

取ってきたリストを編集されても元ネタに影響を出さないための方法として、これまで見てきた ReadOnly 系を使う方法に加えて、今回は ToList でコピーする方法を紹介しました。返す側で ToList してコピーを返すこともできますし、IEnumerable<T> で返しておいてクライアント側で ToList してもらうこともできます。

ReadOnly 系を使ったり、コピーでやったりと、用途に合わせて使い分けてみてください。

C#文法:コレクション

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る

A01_はじめに
A02_プロジェクトの作成
B01_配列とは
B02_配列の生成とアクセス
B03_生成と同時に値を設定する
B04_型推論による生成
B05_メソッドの引数などにする場合の注意点
B06_Length
B07_IndexOfでの検索
B08_FindIndexでの検索
B09_Find
B10_Exists
B11_FindAllとFindLast
B12_誤ったコピー
B13_Array.Copy
B14_範囲指定のコピー
B15_Resize
C01_ArrayList
C02_List
C03_List 動的な要素の変更
C04_Listのコンストラクタ
C05_Listのコンストラクタ_Capacity
C06_ListTからArrayクラスのメソッドが呼ばれている
D01_コレクションインタフェースとは
D02_異なるコレクションクラスに互換性を持たせる
D03_インタフェースの階層構造
D04_クライアントができる操作を制限させる
D05_クライアントができる操作を制限させる_後半
D06_Enumerableの拡張メソッドに関して
D07_ReadOnly系のコレクションインタフェース
D08_AsReadOnly
D09_ToListでコピーする
D10_ListTはprivateで使う

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る