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?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With