Затенение унаследованных членов универсального интерфейса в .NET: хорошо, плохо или безобразно?

Я знаю, что затенение членов в реализациях классов может привести к ситуациям, когда «неправильный» член может быть вызван в зависимости от того, как я привел свои экземпляры, но с интерфейсами я не вижу, что это может быть проблемой, и я обнаружил, что пишу интерфейсы так довольно часто:

public interface INode
{
    IEnumerable<INode> Children { get; }
}

public interface INode<N> : INode
    where N : INode<N>
{
    new IEnumerable<N> Children { get; }
}

public interface IAlpha : INode<IAlpha>
{ }

public interface IBeta : INode<IBeta>
{ }

В моем коде есть места, которые знают только о INode, поэтому дочерние элементы также должны относиться к типу INode.

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

Итак, я реализую класс NodeBase следующим образом:

public abstract class NodeBase<N> : INode<N>
    where N : INode<N>
{
    protected readonly List<N> _children = new List<N>();

    public IEnumerable<N> Children
    {
        get { return _children.AsEnumerable(); }
    }

    IEnumerable<INode> INode.Children
    {
        get { return this.Children.Cast<INode>(); }
    }
}

Никакого затенения в реальной реализации, только в интерфейсах.

Конкретные экземпляры IAlpha и IBeta выглядят так:

public class Alpha : NodeBase<Alpha>, IAlpha
{
    IEnumerable<IAlpha> INode<IAlpha>.Children
    {
        get { return this.Children.Cast<IAlpha>(); }
    }
}

public class Beta : NodeBase<Beta>, IBeta
{
    IEnumerable<IBeta> INode<IBeta>.Children
    {
        get { return this.Children.Cast<IBeta>(); }
    }
}

Опять же, никакого затенения в реализациях.

Теперь я могу получить доступ к этим типам следующим образом:

var alpha = new Alpha();
var beta = new Beta();

var alphaAsIAlpha = alpha as IAlpha;
var betaAsIBeta = beta as IBeta;

var alphaAsINode = alpha as INode;
var betaAsINode = beta as INode;

var alphaAsINodeAlpha = alpha as INode<Alpha>;
var betaAsINodeBeta = beta as INode<Beta>;

var alphaAsINodeIAlpha = alpha as INode<IAlpha>;
var betaAsINodeIBeta = beta as INode<IBeta>;

var alphaAsNodeBaseAlpha = alpha as NodeBase<Alpha>;
var betaAsNodeBaseBeta = beta as NodeBase<Beta>;

Каждая из этих переменных теперь имеет правильную коллекцию строгого типа Children.

Итак, мои вопросы просты. Является ли затенение элементов интерфейса с использованием такого шаблона хорошим, плохим или уродливым? И почему?


person Enigmativity    schedule 12.08.2011    source источник
comment
Почему бы просто не использовать аргумент типа для определения типа дочернего элемента. Тогда INode‹IAlpha› по-прежнему будет иметь ту же сематику, но вам вообще не нужно будет затенять   -  person Rune FS    schedule 12.08.2011
comment
@Rune - Что вы подразумеваете под аргументом типа?   -  person Enigmativity    schedule 12.08.2011
comment
@Enigmativity: я отредактировал свой ответ, чтобы упростить реализацию INode.Children в NodeBase - я понял, чего мне не хватало раньше.   -  person Jon Skeet    schedule 12.08.2011
comment
@Джон - Спасибо. Очень признателен.   -  person Enigmativity    schedule 12.08.2011


Ответы (2)


Я бы сказал, что у вас есть довольно сложный сценарий, и я обычно стараюсь сделать все проще, но если это сработает для вас, я думаю, что можно добавить больше информации, подобной этой. (Это кажется разумным, пока вы не доберетесь до битов IAlpha и IBeta; без этих интерфейсов Alpha и Beta вообще не нуждаются в какой-либо реализации, а вызывающие могут просто использовать вместо них INode<IAlpha> и INode<IBeta>.

В частности, обратите внимание, что IEnumerable<T> фактически делает то же самое - не скрывает один общий код за другим, по общему признанию, а скрывает не-общий вариант за общим.

Еще четыре пункта:

  • Ваш звонок AsEnumerable в NodeBase бессмыслен; вызывающие абоненты по-прежнему могут приводить к List<T>. Если вы хотите предотвратить это, вы можете сделать что-то вроде Select(x => x). (Теоретически Skip(0) может работать, но его можно оптимизировать; LINQ to Objects не очень хорошо документирован с точки зрения того, какие операторы гарантированно скрывают исходную реализацию. Select гарантированно не будет. На самом деле, Take(int.MaxValue) тоже сработает.)

  • Начиная с С# 4, ваши два "листовых" класса могут быть упрощены из-за ковариации:

    public class Alpha : NodeBase<Alpha>, IAlpha
    {
        IEnumerable<IAlpha> INode<IAlpha>.Children { get { return Children; } }
    }
    
    public class Beta : NodeBase<Beta>, IBeta
    {
        IEnumerable<IBeta> INode<IBeta>.Children { get { return Children; } }
    }
    
  • Начиная с C# 4, ваша NodeBase реализация INode.Children может быть упрощена, если вы хотите ограничить N ссылочным типом:

    public abstract class NodeBase<N> : INode<N>
        where N : class, INode<N> // Note the class constraint
    {
        ...
    
        IEnumerable<INode> INode.Children
        {
            get { return this.Children; }
        }
    }
    
  • Начиная с C# 4, вы можете объявить INode<N> ковариантным в N:

    public interface INode<out N> : INode
    
person Jon Skeet    schedule 12.08.2011
comment
Спасибо за еще один отличный ответ. Пункт о том, что AsEnumerable можно преобразовать обратно в List<T>, сбил меня с толку. В Rx метод AsObservable скрывает наблюдаемый источник. Я просто предположил, что AsEnumerable тоже. Может быть, ребята из Rx просто умны. - person Enigmativity; 12.08.2011

Почему бы просто не использовать аргумент типа (то есть аргумент универсального типа) для определения типа дочернего элемента. Тогда у INode по-прежнему будет та же сематика, но вам вообще не понадобится затенение. И у вас действительно есть затенение в реализации, приведение к INode приведет к тем же проблемам, которые вы описываете в своем посте.

person Rune FS    schedule 12.08.2011