vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】readonly と ReadOnlyCollection

はじめに

この記事は C# その2 Advent Calendar 2018 三日目の記事です。

前回書いた通り、最近は dotnet cli などのコードをたどっています。

その中で IReadOnlyCollection などを使って処理の結果などを返しているのが目につきました。

これまで自分ではあまり使ったことがなかったのですが、気になったので調べてみることにした、というお話です。

readonly と IReadOnlyCollection

まずは readonly キーワードと IReadOnlyCollection の違いについて調べてみます。

private readonly List< string> _texts1 = new List< string>();
private IReadOnlyCollection< string> _texts2 = new List< string>();

readonly は List のインスタンス自身が ReadOnly になるため、コンストラクタなどで初期化した後、 null や new List< string>() などを入れようとするとエラーが発生します。

IReadOnlyCollection は List (配列) 内の要素が ReadOnly となり、 Add や Clear 、インデックスによる要素アクセスといった要素を変更するメソッドが提供されません。

要するに get-only プロパティと同じようなもの、という感じでしょうか。

また、上記の初期化の様子からもわかるかもしれませんが、 List や配列は IReadOnlyCollection を継承しているため、 IReadOnlyCollection としてふるまうことができます。

注意点としては下記が挙げられます。

  1. readonly キーワードと違い、 List のインスタンス自身は書き換えてしまえること
  2. ①List を作る → ② ①を元に IReadOnlyCollection を作る とした場合、①を変更すると ②も変更されてしまうこと
List< string> originalValues = new List< string> {
    "Hello",
};
IReadOnlyCollection< string> readonlyValues = originalValues;

foreach (string v in readonlyValues) {
    Console.WriteLine(v); // Hello と出力される.
}

originalValues[0] = "World";

foreach (string v in readonlyValues) {
    Console.WriteLine(v); // World と出力される.
}

ここから、元の List (配列) から IReadOnlyCollection を作る場合、というのは、あくまでメソッドの引数などに使用し、このメソッドでは要素を変更しませんよ、という形で使うのが良さそうです。

あと、当然ながら IReadOnlyCollection にも readonly キーワードは使用できるため、注意点 1. に対応することはできます。

private readonly IReadOnlyCollection< string> _texts = new List< string>();

慣れてないと???と思ってしまいそうですが。

IReadOnlyCollection と ReadOnlyCollection

名前から言えば、 ReadOnlyCollection は IReadOnlyCollection に対する具象クラス( concrete class )?という気がします。

実際のところ違いは下記のようなものが挙げられます。

  1. List や配列は継承していない
    ( new ReadOnlyCollection(List や配列のインスタンス) で生成する)
  2. Contains や CopyTo 、 IndexOf など IReadOnlyCollection が持たないメソッドを持つ
  3. List や配列など、 IList を継承しているクラスのインスタンスからのみ生成できる
    ( ReadOnlyCollection は IReadOnlyCollection からは生成できない)

3.から、戻り値は List ではなく IEnumerable で使う、といったように、一般的には具象クラスでなく抽象クラスや interface で返しましょう、という話をよく聞きます。

が、 ReadOnlyCollection と IReadOnlyCollection に関しては、例えば ReadOnlyCollection のメソッドが必要な場合などはちゃんと ReadOnlyCollection として返す必要がありそうです。

その他の注意点としては下記が挙げられます。

  1. readonly キーワードと違い、 List のインスタンス自身は書き換えてしまえること
  2. ①List を作る → ② ①を元に IReadOnlyCollection を作る とした場合、①を変更すると ②も変更されてしまうこと

IReadOnlyCollection と一緒やん!(; ・`д・´)

ということで気を付けて使いましょう。

各要素のパブリック変数

もう一つ注意点として、 readonly 、 IReadOnlyCollection 、 ReadOnlyCollection ともに各要素が持つパブリック変数については ReadOnly にならない、ということが挙げられます。

SampleValue.cs

namespace ConsoleApp1 {
    public class SampleValue {
        public string Name { get; set; }
    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ConsoleApp1 {
    class Program {
        static void Main(string[] args) {
            ReadOnlyCollection readonlyValues = new ReadOnlyCollection< SampleValue> (
                new List< SampleValue> {
                    new SampleValue {
                        Name = "Hello",
                    }   
                });
            
            foreach (var v in readonlyValues) {
                Console.WriteLine(v.Name); // Hello と表示される.
            }

            foreach (var v in readonlyValues) {
                v.Name = "World";
            }
            foreach (var v in readonlyValues) {
                Console.WriteLine(v.Name); // World と表示される.
            }
        }
    }
}

これを防ぎたい場合は SampleValue 内の変数を get-only プロパティにするなどの対策が必要です。

おわりに

ReadOnly という字面だけ見て判断してしまうと、躓いてしまいそうなことがわかりました。

とはいえちゃんと活用すれば、IReadOnlyCollection や ReadOnlyCollection はとても便利だと思うので、必要に応じて積極的に使用していきたいと思います。

参照