【Unity C#】 IReadOnlyListの紹介
概要
今回の主役は IReadOnlyList
です!
List
でもなく、IReadOnlyCollection
でもありません!
個人的には パフォーマンス と ポリモーフィズムを併せ持った良いinterfaceだと思っています。
しかし、アロケーションの発生を避けたいときにはなかなか注意が必要 な存在だったりします。。。
そんなIReadOnlyList
を紹介していきたいと思います。
ちなみにアロケーションについては別記事にする予定です。
IReadOnlyList について
注意事項
- ダウンキャスト (
readonlyList as List<>
など)は禁止という前提で説明- ダウンキャストを許した場合は読込専用が簡単に崩壊するため
追記
2022/02/25
- 「Unityでの使用例」で共変性を利用したアップキャストになるようにコード例を修正
2022/02/26
IReadOnlyListは読込専用のList
- 名前の通り 読込専用のList 向けinterface
- 標準配列(
Array
)やList
などが継承している
- 標準配列(
- 各要素の変更が不可になっており
list[0] = default
などはコンパイルエラーとなる - 主な利用用途は変更不可のListとして提供したいときなど
[Tooltip("ラベル一覧")] [SerializeField] private List<string> _labelList; // 読込専用としてラベル一覧をクラス外に提供 public IReadOnlyList<string> LabelList => _labelList;
C#標準の一次元配列はIReadOnlyListにキャスト可能
string[] strList = new string[] { "foo", "bar", "baz" }; IReadOnlyList<string> readonlyStrList = strList;
- 公式ドキュメントにも 「一次元配列は
IList<T>
とIEnumerable<T>
を実装している」と明記されているSingle-dimensional arrays also implement IList<T> and IEnumerable<T>.
IReadOnlyCollectionとの違いは index指定で要素にアクセスが可能なこと
list[i]
がList
同様に使用可能- そもそも
T this[int index] { get; }
の定義はIReadOnlyList
由来 IReadOnlyCollection
と異なりindexさえわかればO(1)
の計算量でアクセス可能
※個人的にパフォーマンス的観点で有効性を感じるところ
IReadOnlyListのTに値型を宣言すれば要素含め読込専用となる
[SerializeField] private List<Vector3> _vectorList; public IReadOnlyList<Vector3> VectorList => _vectorList;
- 上記のようにTが値型で
IReadOnlyList
に変換した場合は完全な読込専用として提供される
IReadOnlyListは参照型の要素を読込専用にはしない
IReadOnlyList
はあくまでList自体の変更を不可にしたもので、要素本体はT型に従う- 要素の方が
GameObject
の場合は引き続きnameやtransformを変更することは可能
- 要素の方が
IReadOnlyList<GameObject> readonlyObjectList = new List<GameObject> { ... }; readonlyObjectList[0].name = "Renamed_" + readonlyObjectList[0].name;
但し、後述する共変性を利用することで参照型の要素本体も読込専用としての提供も可能
IReadOnlyList<out T>のため共変性持つ
public interface IReadOnlyList<out T> : System.Collections.Generic.IEnumerable<out T>, System.Collections.Generic.IReadOnlyCollection<out T>
- 上記のように
out T
で宣言されているため、IReadOnlyList
は共変性がある
例えば以下のような参照型クラスFoo
、interface INameHolder
があったとする。
public interface INameHolder { string Name { get; } } public class Foo : INameHolder { public string Name { get; set; } = "Default Name"; }
上記Foo
使ったList<Foo>
は共変性を使って以下のような変換が可能。
IReadOnlyList<INameHolder> readonlyNameHolderList = new List<Foo> { ... };
// コンパイルエラー(INameHolderはget_Nameのみ提供のため) readonlyNameHolderList[1].Name = "Melon";
readonlyの継承型として提供することで参照型要素もreadonlyとして提供可能。
Unityでの使用例
MonoBehaviourやScriptableObjectを依存逆転の法則を当てはめて提供
- Unityとは疎結合にしたまま、Unityの機能を利用した読込専用データ群を配布に利用するなど
※以下はあくまで実装イメージ
[CreateAssetMenu(fileName = nameof(ScriptableObjectNameHolder), menuName = "ScriptableObjects/" + nameof(ScriptableObjectNameHolder), order = 1)] public sealed class ScriptableObjectNameHolder : ScriptableObject, INameHolder { [SerializeField] private string _name; // ScriptableObject側で定義された名前を読込専用として提供 public string Name => _name; }
以下の様に List<ScriptableObjectNameHolder>
から IReadOnlyList<INameHolder>
に変換し、Unityとは疎結合なインスタンス公開が可能。
[SerializeField] private List<ScriptableObjectNameHolder> _nameHolders; // 共変性を利用してUnityの存在を隠蔽し、読み込み専用として公開 public IReadOnlyList<INameHolder> NameHolders => _nameHolders;
インスタンスの公開はExtenjectやVContainerなどを利用することが多いですが詳細は割愛。
関連
本題記事
雑感
久々の投稿です。
最近、環境が変わって色々ドタバタしていました。
世間ではFacebookのMetaへ社名変更、新しいQuestが発表、メタバースへの注目など、
XR界隈で目まぐるしい変化が来そうなときに、
「なんでC#の話してるの?」って言われそうですねw。
IReadOnlyList
はUnityでの開発でも結構役立つことが多いと感じています。
DI, SOLID原則, テスタビリティ, etc...
ちなみに今回の記事は前座で本題は次回の「IReadOnlyListのアロケーション(関連にリンク)」だったりします!
なんでアロケーションなんかを調べたのかは次回にお話できればと思います。
それでは~