Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Shadowing Inherited Generic Interface Members in .NET: good, bad or ugly?

I know that shadowing members in class implementations can lead to situations where the "wrong" member can get called depending on how I have cast my instances, but with interfaces I don't see that this can be a problem and I find myself writing interfaces like this quite often:

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>
{ }

I have places in my code that only know about INode so children should also be of type INode.

In other places I want to know about the specific types - in the implementation of my example IAlpha & IBeta interfaces I want the children to be typed the same as their parent.

So I implement a NodeBase class like so:

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>(); }
    }
}

No shadowing in the actual implementation, only in the interfaces.

Specific instances of IAlpha & IBeta look like this:

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>(); }
    }
}

Again, no shadowing in the implementations.

I can now access these types like so:

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>;

Each of these variables now have the correct, strongly-type Children collection.

So, my questions are simple. Is the shadowing of interface members using this kind of pattern good, bad or ugly? And why?

like image 879
Enigmativity Avatar asked Aug 12 '11 05:08

Enigmativity


1 Answers

I would say you've got yourself a pretty complicated scenario there, and I generally try to keep things simpler than that - but if it works for you, I think it's okay to add more information like this. (It seems reasonable until you get to the IAlpha and IBeta bit; without those interfaces, Alpha and Beta don't need any implementation at all, and callers can just use INode<IAlpha> and INode<IBeta> instead.

In particular, note that IEnumerable<T> effectively does the same thing - not hiding one generic with another, admittedly, but hiding a non-generic with a generic.

Four other points:

  • Your call to AsEnumerable in NodeBase is pointless; callers can still cast to List<T>. If you want to prevent that, you can do something like Select(x => x). (In theory Skip(0) might work, but it could be optimized away; LINQ to Objects isn't terribly well documented in terms of which operators are guaranteed to hide the original implementation. Select is guaranteed not to. Realistically, Take(int.MaxValue) would work too.)

  • As of C# 4, your two "leaf" classes can be simplified due to covariance:

    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; } }
    }
    
  • As of C# 4, your NodeBase implementation of INode.Children can be simplified if you're willing to restrict N to be a reference type:

    public abstract class NodeBase<N> : INode<N>
        where N : class, INode<N> // Note the class constraint
    {
        ...
    
        IEnumerable<INode> INode.Children
        {
            get { return this.Children; }
        }
    }
    
  • As of C# 4, you can declare INode<N> to be covariant in N:

    public interface INode<out N> : INode
    
like image 170
Jon Skeet Avatar answered Sep 28 '22 23:09

Jon Skeet