今回は AsReadOnly について見ていきます。フォームに「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
キャストの問題
ところが、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>を受け取る
この戻ってきた a を、List<string> にキャストして変数 b に入れます。すると、b は 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するコードの完成形
実行して確認する
1行目にブレークポイントを置いて実行し、ステップ実行で値の変化を見ていきます。まず GetDataList で取得した直後の a は、Count = 2、つまり2件です。
図6 取得直後は2件
続いて、b にキャストしたものを入れて、b に Add します。すると b は Count = 3、3件になります。
図7 Addした後、bは3件になっている
そして、GetData で純粋に取り直した p を見てみると、こちらも3件になっています。中身を展開すると、「ボール」「スパイク」に続いて、追加した「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()を付ける
AsReadOnly としておくと、キャストができなくなります。呼び出し側のコードは変えずに、そのまま実行してみます。
図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>
これまでは、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#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新機能」を観る










