Shadowing Inherited Generic Interface Members в .NET: хорошо, плохо или безобразно? - PullRequest
9 голосов
/ 12 августа 2011

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

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 строго типа.

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

Ответы [ 2 ]

8 голосов
/ 12 августа 2011

Я бы сказал, что у вас там довольно сложный сценарий, и я, как правило, пытаюсь упростить ситуацию - но если это работает для вас, я думаю, что можно добавить больше информации, например этот. (Это кажется разумным, пока вы не доберетесь до битов 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) тоже будет работать.)

  • Начиная с C # 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
    
0 голосов
/ 12 августа 2011

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

...