コレクション【C#】

C#文法:コレクション:D08_AsReadOnly

今回は AsReadOnly について見ていきます。フォームに「AsReadOnly」というボタンを作り、ダブルクリックしてクリックイベントを生成しておきます。

図1 AsReadOnlyボタンを作成する

図1 AsReadOnlyボタンを作成する

前回の振り返り

前回は ReadOnly 系のコレクションインタフェースを取り上げました。戻り値を IReadOnlyCollection<T> や IReadOnlyList<T> にしておけば、インデックス指定や Count の機能は使えるけれど、編集はできない、という内容でした。

Products クラスには、前回作成した GetDataCount メソッドと GetDataList メソッドがあります。どちらも内部の _values をそのまま返しています。

Products.cs:前回作成した2つのメソッド

public static IReadOnlyCollection<string> GetDataCount()
{
    return _values;
}

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

図2 前回作成したGetDataCountとGetDataList

図2 前回作成したGetDataCountとGetDataList

キャストの問題

ところが、ReadOnly 系のインタフェースで返していても、実は「キャストの問題」というものがあります。キャストをすれば、無理やり値を変えることができてしまうのです。

実際に試してみます。まず、Products の GetDataList メソッドを呼び出します。戻ってくるのは IReadOnlyList<string> です。

Form1.cs:GetDataListを呼び出す

private void button21_Click(object sender, EventArgs e)
{
    var a = Products.GetDataList(); //IReadOnlyList<string>が返ってくる
}

図3 GetDataListでIReadOnlyList<string>を受け取る

図3 GetDataListでIReadOnlyList<string>を受け取る

この戻ってきた a を、List<string> にキャストして変数 b に入れます。すると、b は List<string> ですから、Add メソッドが使えます。つまり、編集ができてしまいます。

図4 List<string>にキャストするとAddが呼べる

図4 List<string>にキャストするとAddが呼べる

さらに、キャストしたものとは別に、GetData メソッドで純粋にデータを取り直して、変数 p に入れておきます。Add の影響が元のデータに出ているかどうかを確認するためです。

Form1.cs:キャストして無理やり追加する

private void button21_Click(object sender, EventArgs e)
{
    var a = Products.GetDataList();
    var b = (List<string>)a; //List<string>にキャストする
    b.Add("BBBB");           //編集できてしまう

    var p = Products.GetData();
}

図5 キャストしてAddするコードの完成形

図5 キャストしてAddするコードの完成形

実行して確認する

1行目にブレークポイントを置いて実行し、ステップ実行で値の変化を見ていきます。まず GetDataList で取得した直後の a は、Count = 2、つまり2件です。

図6 取得直後は2件

図6 取得直後は2件

続いて、b にキャストしたものを入れて、b に Add します。すると b は Count = 3、3件になります。

図7 Addした後、bは3件になっている

図7 Addした後、bは3件になっている

そして、GetData で純粋に取り直した p を見てみると、こちらも3件になっています。中身を展開すると、「ボール」「スパイク」に続いて、追加した「BBBB」が入っています。

図8 取り直したpにも「BBBB」が追加されている

図8 取り直したpにも「BBBB」が追加されている

a も b も p も、すべて Products クラスの中にある同じ _values を指しています。キャストは型の見え方を変えているだけで、別のリストを作っているわけではありません。そのため、b に Add した結果が元のデータに影響してしまうのです。

このように、ReadOnly 系のインタフェースで返却しても、無理やりキャストすれば変更できてしまいます。

ただし、基本的にはこれは書くほうが悪いコードです。キャストして値を変えるというのは、悪意があるというか、無理やりやっていることなので、そこまで気にしなくてもよいでしょう。

AsReadOnly で対策する

それでも、あえて対策をするのであれば、AsReadOnly メソッドを使います。Products クラスで _values を返している箇所に、AsReadOnly() を付けます。IReadOnlyCollection<T> を返すほうも、IReadOnlyList<T> を返すほうも、書き方は同じです。

Products.cs:AsReadOnlyを付けて返す

public static IReadOnlyCollection<string> GetDataCount()
{
    return _values.AsReadOnly();
}

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

図9 _valuesにAsReadOnly()を付ける

図9 _valuesにAsReadOnly()を付ける

AsReadOnly としておくと、キャストができなくなります。呼び出し側のコードは変えずに、そのまま実行してみます。

図10 キャストのタイミングでInvalidCastExceptionが発生する

図10 キャストのタイミングでInvalidCastExceptionが発生する

キャストをする行で、System.InvalidCastException が発生しました。メッセージは「ReadOnlyCollection<string> 型のオブジェクトを List<string> 型にキャストできません」という内容です。

Form1.cs:AsReadOnlyにした後の動き

var a = Products.GetDataList();
var b = (List<string>)a; //ここでInvalidCastExceptionが発生する
b.Add("BBBB");           //ここまで到達しない

注意点は、これは実行したときにしかわからない、ということです。コンパイルの時点でははじけません。

何が起きているのか

AsReadOnly にマウスカーソルを合わせてみると、戻り値が ReadOnlyCollection<string> になっていることがわかります。ReadOnlyCollection<T> は、インタフェースではなく具象クラスです。

図11 AsReadOnlyの戻り値はReadOnlyCollection<string>

図11 AsReadOnlyの戻り値はReadOnlyCollection<string>

これまでは、List<string> である _values を、IReadOnlyList<string> という型にして戻していました。型の見え方が変わっているだけで、実物は List<string> のままです。だから List<string> へのキャストが成功していました。

それに対して AsReadOnly を付けると、元のリストを包んだ別のラッパークラスに変わります。呼び出し側で List<string> にキャストしようとしても、実物はもう List<string> ではなく ReadOnlyCollection<string> なので、キャストに失敗する、というわけです。

(参考)AsReadOnlyの有無による実物の違い

List<string> values = new List<string> { "ボール", "スパイク" };

//そのまま返す場合:実物はList<string>のまま
IReadOnlyList<string> x = values;
var isList1 = x is List<string>;               // true(キャストできる)

//AsReadOnlyで返す場合:実物はReadOnlyCollection<string>
IReadOnlyList<string> y = values.AsReadOnly();
var isList2 = y is List<string>;               // false(キャストできない)
var isRoc = y is ReadOnlyCollection<string>;   // true

なお、ReadOnlyCollection<T> は System.Collections.ObjectModel 名前空間にあるクラスで、IReadOnlyCollection<T> と IReadOnlyList<T> の両方を実装しています。そのため、どちらのメソッドの戻り値にもそのまま使えます。

まとめ

AsReadOnly を使えば、キャストによる書き換えを防ぐことができます。ただ、そこまでしなくてもよい、というのが基本的な考え方です。絶対に書き換えられては困るもの、といった場合に、このように防いでもよいでしょう。

また、これは実行時エラーなので、テストで動かして初めて気づく、という書き方になります。本来はコンパイルではじいてくれるのが一番よいのですが、AsReadOnly はそういうものです。その点を理解したうえで、必要であれば使ってみてください。

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新機能」を観る