C# - Ограничения и ограничения типов, любой обходной путь для обеспечения безопасности типов?

У меня есть довольно распространенный сценарий об ограничениях Generic Type Constraint, который потребовал бы определения другого Generic.

Это уже обсуждалось (сам Эрик Липперт и другие), но до сих пор я не видел общего руководства или, скажем, практического правила, которое можно применять при столкновении со следующим сценарием:

public abstract class Class<TProperty> : Class
     where TProperty : Property<>
// Sadly the line above cannot work, although the compiler could actually infer
// the Generic Type, since defining two class definitions like:
// Considering A & B two other well-defined classes
// Class<TA> where TA : A and 
// Class<TB> where TB : B is not allowed and well-understandable
{
    protected Class(TProperty property)
    {
        if (property != null)
        {
            this._property = property;
        }
        else
        {
            throw new ArgumentNullException(@"property");
        }
    }

    private readonly TProperty _property;
    public TProperty Property
    {
        get
        {
            return this._property;
        }
    }
}
public abstract class Property<TParentClass>
// Same remark goes here
    where TParentClass : Class<>
{
    protected Property(TParentClass parent)
    {
        if (parent != null)
        {
            this._parent = parent;
        }
        else
        {
            throw new ArgumentNullException(@"parent");
        }
    }

    private readonly TParentClass _parent;
    internal TParentClass Parent
    {
        get
        {
            return this._parent;
        }
    }
}

Это хорошо, у нас все еще есть некоторые обходные пути, используя интерфейсы или создавая новые базовые классы следующим образом:

public abstract class Class
{
}
public abstract class Class<TProperty> : Class
    where TProperty : Property
{
    protected Class(TProperty property)
    {
        if (property != null)
        {
            this._property = property;
        }
        else
        {
            throw new ArgumentNullException(@"property");
        }
    }

    private readonly TProperty _property;
    public TProperty Property
    {
        get
        {
            return this._property;
        }
    }
}

public abstract class Property
{
}
public abstract class Property<TParentClass>
    where TParentClass : Class
{
    protected Property(TParentClass parent)
    {
        if (parent != null)
        {
            this._parent = parent;
        }
        else
        {
            throw new ArgumentNullException(@"parent");
        }
    }

    private readonly TParentClass _parent;
    internal TParentClass Parent
    {
        get
        {
            return this._parent;
        }
    }
}

Это нормально, но что произойдет, если я захочу добавить новый законный уровень наследования?

public abstract class InheritedClass<TInheritedProperty> : Class<TInheritedProperty>
// Damn it! I wanted to be more specific but I cannot have <> (and also <,>, <,,>, etc.)
// Cannot do that without declaring another public interface... sad
// Or another non generic base class
    where TInheritedProperty : Property
{
    // But this remark cannot work here... I would have needed a "real" type not InheritedProperty<>...
    // Yeah this is it: starting to bake the noodles
    protected InheritedClass(TInheritedProperty property)
        : base(property)
    {
    }
}

public abstract class InheritedProperty<TInheritedClass> : Property<TInheritedClass>
// Same goes here
    where TInheritedClass : Class
{
    protected InheritedProperty(TInheritedClass parent)
        : base(parent)
    {
    }
}

или даже хуже (с кодом, который явно не может быть скомпилирован) и действительно глупым без реальной безопасности типов:

public abstract class InheritedClass2<TInheritedProperty, TInheritedPropertyClass> : Class<TInheritedProperty>
    where TInheritedProperty : InheritedProperty2<TInheritedPropertyClass, TInheritedProperty>
{
    protected InheritedClass2(TInheritedProperty property)
        : base(property)
    {
    }
}

public abstract class InheritedProperty2<TInheritedClass, TInheritedClassProperty> : Property<TInheritedClass>
    where TInheritedClass : InheritedClass2<TInheritedClassProperty, TInheritedClass>
{
    protected InheritedProperty2(TInheritedClass parent)
        : base(parent)
    {
    }
}

В этот момент люди обычно говорят "нет", дизайн не должен быть таким сложным... пересмотреть ваши бизнес-требования и просто массово использовать интерфейс с композицией, наследование предназначено не только для того, чтобы сэкономить вам на написании дополнительного кода, и эти классы должны сформировать какой-то вид из семей, хорошо, они действительно образуют вид - семья.

Что ж, достаточно справедливо, но это не решает ситуацию, которая, как я должен признаться, слишком преувеличена, но есть случаи, когда имеет смысл иметь наследование с ограничениями и когда эти ограничения также имеют ограничения.

Да, они могут свести вас с ума (например, рекурсивные ограничения) и заставить вас выдернуть волосы... но все же есть ситуации, когда это может быть удобно, особенно в отношении безопасности типов.

В любом случае, что касается этих ограничений, какое самое подходящее общее руководство следует соблюдать, чтобы вернуться на путь, какие-либо другие решения, кроме простого использования интерфейсов или выбора подмножества типов в конструкторе?

2 ответа

Будет ли это работать для вас?

public abstract class Class<TProperty, T>
   where TProperty : Property<T>
{

}

Это не так мощно, как конструкторы типов, потому что T в Property<T> не может меняться, но не похоже, что вы ищете такую ​​гибкость в любом случае.

Я сталкивался с подобными проблемами при попытке ультра-шаблонировать довольно сложную модель приложения в C# раньше. Я пришел к выводу, что компилятор может сделать только так много, хотя.

Я бы беспокоился об установке довольно сложных ограничений типов, если вам конкретно нужно что-то сделать с аргументом типа. Скажем, например, вам нужно гарантировать, что это IEnumerable потому что ваш Class будет перебирать его. В случае с этими примерами вам вообще не нужно ограничение типа.

Если вы обнаружите, что отчаянно хотите сохранить ограничение типов, вы можете попытаться использовать контраст и ко-дисперсию для ограничения дочерних типов, например, так:

class Foo<TProperty> : Class
    where TProperty : IProperty<object>
{
    // Same as your implementation
}

interface IProperty<out T>
{
    T Property;
}

class Property<T> : IProperty
{
    // Same as your implementation
}

РЕДАКТИРОВАТЬ: Добавлен интерфейс IProperty, потому что противоречие и ко-вариация могут быть определены только на интерфейс класса, а не его реализация

Другие вопросы по тегам