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, потому что противоречие и ко-вариация могут быть определены только на интерфейс класса, а не его реализация