ジェネリッククラス内の静的フィールドの挙動について

今メインで作ってるほげもげの進捗があんま良くないので、たまには少し小ネタでも。ジェネリッククラス内(静的クラスでも普通のクラスでもどっちでもいいです)の静的フィールドは、それぞれ独立して、各型に唯一のフィールドとして存在できます。違う型では共有されず、同じ型ないでは共有される、という挙動です。あまり良い例でもないですが、例えばこんな感じ。

public static class InstanceGenerator<T>
{
    static readonly Func<T> generator;

    static InstanceGenerator()
    {
        var newExpr = Expression.Lambda<Func<T>>(
            Expression.New(typeof(T).GetConstructor(Type.EmptyTypes)));
        generator = newExpr.Compile();
    }

    public static T CreateNew()
    {
        return generator.Invoke();
    }
}

class Program
{
    static void Main(string[] args)
    {
        var p1 = InstanceGenerator<Program>.CreateNew();
        var p2 = InstanceGenerator<Program>.CreateNew();
        var s = InstanceGenerator<StringBuilder>.CreateNew();
    }
}

さすがにこれだとnew Program()って書けよって話なので、クソの役にもたたなすぎる例なんです が、いちおう、Compileという重たい処理をキャッシュできますね、みたいな感じ。(このクラスはクソの役にも立たないけど)(こういうジェネリッククラスの挙動は)便利便利。で、それはいいんですけど、もしフィールドがジェネリックじゃない場合はどーなるでしょう?こんな風に、非許可の型チェックを入れてみたりします。

// 静的クラスでもふつーのクラスでもどっちでもいーですよ
public class InstanceGenerator<T>
{
    static readonly Func<T> generator;
    static readonly HashSet<Type> disallowType = new HashSet<Type>
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };

    static InstanceGenerator()
    {
        var newExpr = Expression.Lambda<Func<T>>(Expression.New(typeof(T).GetConstructor(Type.EmptyTypes)));
        generator = newExpr.Compile();
    }

    public InstanceGenerator()
    {
        if (disallowType.Contains(typeof(T))) throw new Exception("その型は許可されてません!");
    }

    public T CreateNew()
    {
        return generator.Invoke();
    }
}

class Program
{
    static void Main(string[] args)
    {
        // ok
        var pg = new InstanceGenerator<Program>().CreateNew();

        // exception
        var sb = new InstanceGenerator<StringBuilder>().CreateNew();

    }
}

HashSet<Type>は特に<T>とは関係のないフィールド。かつ「意図としては」readonlyで全ジェネリッククラスで共有して欲しい。で、実際どーなってるかというと……確認しませう。

public class InstanceGenerator<T>
{
    static readonly Func<T> generator;

    // 呼ばれたのをチェックするために遅延実行のシーケンスをかませる
    static readonly HashSet<Type> disallowType = new HashSet<Type>
        (Enumerable.Range(1, 3).Select(x => { Console.WriteLine(x); return typeof(int); }))
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };

    // 以下同じなので略
}

class Program
{
    static void Main(string[] args)
    {
        // 1,2,3,1,2,3と出力されて、(当然)二回HashSetが初期化されてるのが分かる
        var pg = new InstanceGenerator<Program>().CreateNew();
        var sb = new InstanceGenerator<StringBuilder>().CreateNew();

    }
}

といった感じに、非ジェネリックフィールドも当たり前のように共有されることなく、各ジェネリッククラスで独立して存在します。当たり前っちゃあ当たり前です(readonlyじゃないstatic fieldだって存在できるし、readonlyだってimmutableとは限らないので、そんな利用者都合の区別をコンパイラがつけられはしない!)

けれど多くの静的フィールドを使うシチュエーションにとっては、あんま都合よくないかな、と。大したことナイといえばないですが、正規表現のCompileしたのとか別個で持ってたくないし、その他色々色々。気になるっちゃあ気になります。さて、どうすればいいか、っていうと

internal class InstanceGenerator
{
    protected static readonly HashSet<Type> disallowType = new HashSet<Type>
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };
}

// 静的クラスじゃなければ継承するとか
public class InstanceGenerator<T> : InstanceGenerator
{
    // 以下略
}

// 静的クラスの時は適当に誤魔化すしかない
internal static class _InstanceGenerator
{
    public static readonly HashSet<Type> disallowType = new HashSet<Type>
    {
        typeof(StringBuilder),
        typeof(ArrayList)
    };
}

public static class InstanceGenerator<T>
{
    // 中略

    // なんかひどぅぃ
    public static T CreateNew()
    {
        if (_InstanceGenerator.disallowType.Contains(typeof(T))) throw new Exception("その型は許可されてません!");
        return generator.Invoke();
    }
}

独立した外の型として定義せざるをえないので、適当に誤魔化すしかないですね!あとはふつーにゆーてぃりてぃクラスとして独立させるとか設計で回避、的なアレ。

ちなみに

例がクソややこしく感じた人には一番シンプルなものを。

public static class MyClass<T>
{
    public static object X = new object();
}

class Program
{
    static void Main(string[] args)
    {
        var b = Object.ReferenceEquals(MyClass<int>.X, MyClass<string>.X);
        Console.WriteLine(b); // false

        MyClass<int>.X = 1000; // 違うもクソも外からセットできるし
        Console.WriteLine(MyClass<int>.X); // 1000
        Console.WriteLine(MyClass<string>.X); // System.Object
    }
}

ようはこれだけじゃないですかーやだー無駄にこねくりまわした例は余計わかりづらいー。

仕様

言語仕様的には「4.4.2 オープン型とクローズ型」の最後の部分の話です。

すべての型は、"オープン型" か "クローズ型" のいずれかに分類されます。オープン型は、型パラメーターと一緒に使用する型です。より具体的には、次のとおりです。

  • 型パラメーターはオープン型を定義します。
  • 配列型は、要素の型がオープン型の場合のみ、オープン型です。
  • 構築された型は、1 つ以上の型引数がオープン型の場合のみ、オープン型です。構築された入れ子になった型は、1 つ以上の型引数または外側の型の型引数がオープン型の場合のみ、オープン型です。

クローズ型とは、オープン型でない型です。

実行時、ジェネリック型宣言内のすべてのコードは、ジェネリック宣言に型引数を適用することによって作成されたクローズ構築型のコンテキストで実行されます。ジェネリック型内の各型パラメーターは、特定の実行時の型にバインドされます。すべてのステートメントおよび式の実行時の処理ではクローズ型が発生し、オープン型は、コンパイル時の処理でのみ発生します。

クローズ構築型には独自の静的変数セットがあり、このセットは他のクローズ構築型と共有されません。オープン型は実行時には存在しないため、オープン型に関連付けられた静的変数はありません。2 つのクローズ構築型は、同じ非バインド ジェネリック型から構築された場合は同じ型になり、対応する型引数も同じ型になります。

あとは「10.5.1 静的フィールドとインスタンスフィールド」でも触れられています。

静的フィールドは特定のインスタンスの一部ではなく、クローズ型 (4.4.2 を参照) のすべてのインスタンス間で共有されます。クローズ クラス型のインスタンスがいくつ作成される場合でも、関連付けられたアプリケーション ドメインに対する静的フィールドのコピーは 1 つだけです。

この辺りのは言い方がややこしいんで言語仕様とにらめっこしてるだけだとあんま頭に入ってこない系ですにぇ。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive