セルフリファレンス汎用モード解析

5693 ワード

ある人は私にこのような質問をしたことがあります:どのようにサブクラスに無パラメトリック構造関数を提供させるか.当時与えられた答えは,サブクラスにこのようなインタフェースを実現させることである.
    public interface IMustHaveParameterLessConstructor<T>
        where T : IMustHaveParameterLessConstructor<T>, new()
    {
    }

このような汎用パラメータで自身を引用する技法には,「Self−referencing Generics」モードという名前がある.この技法はC++で20年以上使われていますが、Curiously Recurring Templateと呼ばれています.
この技法は多くの有用な機能を実現するために用いることができる.例えばすべてのサブクラスに対してSingletonモードを実現する
     public class Singleton<T>
        where T : new()
    {
        private static readonly T instance = new T();
        public static T Instance
        {
            get { return instance; }
        }
    }

    public class Model : Singleton<Model>
    {
    }

この技法の劣勢についてお話しします.
まず、コードの可読性に影響します.例えば、この程度の場合です.
    public interface IComponent<T>
    { }

    public interface IComponentProvider<TComponent>
        where TComponent : IComponent<IComponentProvider<TComponent>>
    { }

    public interface IComponentProviderWorkaround<TComponent, TSelf>
        where TComponent : IComponent<TSelf>
        where TSelf : IComponentProviderWorkaround<TComponent, TSelf>
    { }

これが自業自得だ.人が読むと悪口を言いたくなる.
次に,この技法は実際には逆向きである.クラスが階層を1つ以上継承すると、問題が発生します. 
Eric Lippertはその博文「Curiouser and curiouser」で継承関係の論理的合理性の観点から分析した.本質的には,自己引用汎型はリス置換の原則に違反している.次の節では、いくつかのポイントを選びました.
 It seems like an abuse of a mechanism rather than the modeling of a concept from the program's "business domain"
……
My advice is to think very hard before you implement this sort of curious pattern in C#; do the benefits to the customer really outweigh the costs associated with the mental burden you're placing on the code maintainers?
Eric文の例は穏やかで,少なくともコンパイルエラーや警告は起こらなかった.そこで一部の人に無視された.
では、コンパイルエラーの例を書きます. 
    public interface SoapArgs<out T>
        where T : SoapArgs<T>
    {
    }

    public class GenericSoapArgs<T> : SoapArgs<GenericSoapArgs<T>>
    {
    }

    public class DerivedGenericSoapArgs<T> : GenericSoapArgs<T>
    {
    }

この3つのクラス(またはインタフェース)の関係は一目瞭然ですよね.SoapArgsを送信する関数もあります. 
    public class SoapSender
    {
        public virtual void SendSoapArgs<T>(T args)
            where T : SoapArgs<T>
        {
        }
    }

簡単ですよね?論理的には、この関数は前の2つのクラスのインスタンスを受け入れることができますよね?しかし、実際には、次の2行目のコードでコンパイルエラーが発生します.
new SoapSender().SendSoapArgs(new GenericSoapArgs<int>());
new SoapSender().SendSoapArgs(new DerivedGenericSoapArgs<int>());

エラーメッセージは次のとおりです.
The type 'DerivedGenericSoapArgs' cannot be used as type parameter 'T' in the generic type or method 'SoapSender.SendSoapArgs(T)'. There is no implicit reference conversion from 'DerivedGenericSoapArgs' to 'SoapArgs< DerivedGenericSoapArgs>'.
解決策は簡単ですが、親が実現しているにもかかわらず、DerivedGenericSoapArgs自身でSoapArgsインタフェースを再実現すればいいのです.