すぎしーのXRと3DCG

主にXR, Unity, 3DCG系の記事を投稿していきます。

【Unity C#】 IReadOnlyListの紹介

概要

今回の主役は IReadOnlyList です!

docs.microsoft.com

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>.

docs.microsoft.com

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;
}

f:id:tsgcpp:20220225231223p:plain

以下の様に List<ScriptableObjectNameHolder> から IReadOnlyList<INameHolder> に変換し、Unityとは疎結合インスタンス公開が可能。

    [SerializeField] private List<ScriptableObjectNameHolder> _nameHolders;

    // 共変性を利用してUnityの存在を隠蔽し、読み込み専用として公開
    public IReadOnlyList<INameHolder> NameHolders => _nameHolders;

インスタンスの公開はExtenjectやVContainerなどを利用することが多いですが詳細は割愛。

関連

本題記事

tsgcpp.hateblo.jp

雑感

久々の投稿です。
最近、環境が変わって色々ドタバタしていました。

世間ではFacebookのMetaへ社名変更、新しいQuestが発表、メタバースへの注目など、
XR界隈で目まぐるしい変化が来そうなときに、
「なんでC#の話してるの?」って言われそうですねw。

IReadOnlyListはUnityでの開発でも結構役立つことが多いと感じています。
DI, SOLID原則, テスタビリティ, etc...

ちなみに今回の記事は前座で本題は次回の「IReadOnlyListのアロケーション(関連にリンク)」だったりします!
なんでアロケーションなんかを調べたのかは次回にお話できればと思います。

それでは~