在.NET中隐藏继承的通用接口成员:好、坏还是难看

本文关键字:成员 接口 隐藏 NET 继承 | 更新日期: 2023-09-27 18:05:13

我知道在类实现中隐藏成员可能会导致调用"错误"成员的情况,这取决于我如何转换实例,但对于接口,我不认为这会是一个问题,我发现自己经常编写这样的接口:

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

在实际实现中没有阴影,只有在接口中。

CCD_ 6&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集合。

所以,我的问题很简单。使用这种模式的接口成员的阴影是好的、坏的还是难看的?为什么?

在.NET中隐藏继承的通用接口成员:好、坏还是难看

我想说你有一个非常复杂的场景,我通常会尝试让事情变得更简单,但如果它对你有效,我认为可以添加更多这样的信息。(在使用IAlphaIBeta位之前,这似乎是合理的;如果没有这些接口,AlphaBeta根本不需要任何实现,调用方只需使用INode<IAlpha>INode<IBeta>即可。

特别要注意的是,IEnumerable<T>有效地做了同样的事情——诚然,不是用另一个泛型隐藏一个泛型,而是用泛型隐藏非泛型。

其他四点:

  • 您在NodeBase中呼叫AsEnumerable毫无意义;呼叫者仍然可以转换为CCD_ 18。如果你想防止这种情况发生,你可以做一些类似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开始,如果您愿意将N限制为引用类型:,则INode.ChildrenNodeBase实现可以简化

    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开始,您可以在N:中声明INode<N>是协变的

    public interface INode<out N> : INode
    

为什么不简单地使用类型参数(泛型类型的参数(来确定子类型。那么INode仍然会有相同的信号,但你根本不需要阴影而且在实现中确实存在阴影,将转换为INode将导致与您在后中描述的问题相同的问题