C#の混在オブジェクトリストを扱う方法「List<T>で様々なクラスのオブジェクトを要素にしてみる」

コンピュータ

C# の List<T> は、通常は 同一型のオブジェクトを格納する ためのコレクションです。
そのため、Tinterface で定義するのが最も素直で安全な方法だと思います。

しかし今回は、

  • 既存のクラスが存在する

  • そのクラスに interface を追加できない

という前提を想定し、object 型を使って 複数のクラスのオブジェクトを混載する方法 を考えてみます。


基本方針

object 型を使う場合、元のクラスの型に キャストして使用する必要があります。
そのため、

  • 値(object)

  • 型識別用の情報

セットで管理する必要があります。

また、型ごとにキャスト処理が異なるため、
取り出す際には switch 文による 多岐分岐 が必要になります。

サンプルコード

// 型識別用列挙型(Typeプロパティ用)
enum ItemType
{
    ClassA,
    ClassB,
}

// ClassA,ClassBはlistの要素のValueとなるクラス(既存のクラスを想定)
class ClassA
{
    public int ValueA { get; set; }
}

class ClassB
{
    public string ValueB { get; set; } = "";
}

// listの要素用のクラス。Valueを受け入れるコンテナ
class Item
{
    public ItemType Type { get; }   // 参照のみ
    public object Value { get; }    // 参照のみ

    public Item(ItemType type, object value)
    {
        // 初期化で値をセット=> Value Objet => record化出来るかも。
        Type = type;
        Value = value;
    }
}

// 使う側のコード
class Program
{
    // エントリーポイント
    static void Main()
    {
        var items = new List<Item>();

        items.Add(new Item(
            ItemType.ClassA,
            new ClassA { ValueA = 10 }));

        items.Add(new Item(
            ItemType.ClassB,
            new ClassB { ValueB = "hello" }));
        
        // 数値の10と文字列のhelloがitemsに混載された。

        // 以下取り出して使うコード
        foreach (var item in items)
        {
            switch (item.Type)
            {
                case ItemType.ClassA:
                {
                    if (item.Value is not ClassA a)
                        throw new InvalidOperationException(
                            $"Type=A but Value={item.Value.GetType()}");    // キャスト失敗は例外でアプリ終了

                    // ここから型安全
                    Console.WriteLine(a.ValueA);
                    break;
                }

                case ItemType.ClassB:
                {
                    if (item.Value is not ClassB b)
                        throw new InvalidOperationException(
                            $"Type=B but Value={item.Value.GetType()}");    // キャスト失敗は例外でアプリ終了

                    Console.WriteLine(b.ValueB);
                    break;
                }

                default:
                    throw new NotSupportedException(
                        $"Unsupported Type: {item.Type}");
            }
        }

        // 結果
        // 10
        // hello

    }        
}

感想

コードとしてはやや回りくどく、
そもそも 通常のアプリケーション設計で混載が必要になる場面は少ない ようにも感じます。

ただし、JSON や XAML といったデータフォーマットは、
このような「型の異なる値が混在する構造」を
さらに複雑に組み合わせて表現しています。

そう考えると、

  • JSON を言語として自然に扱える JavaScript の柔軟性の高さ

  • 一方で、静的型付け言語で同じことをやろうとした場合の記述量の多さ

が、対照的に見えてきます。

今回のサンプルコードを見る限り、
C# でこの方法を多用するのは、あまり効率の良いやり方ではない
という印象も受けます。

ただし、

  • 既存クラスを変更できない

  • 境界データ(JSON / XAML / UI 要素など)を扱う

といった場面では、
現実的な選択肢のひとつになり得る方法だと思います。

コメント