派生类是否应隐藏从Comparer <t>继承的Default和Create静态成员?

时间:2016-03-11 12:33:20

标签: c# .net icomparer

我通过从IComparer<T>类派生recommended by MSDN来编写Comparer<T>实现。例如:

public class MyComparer : Comparer<MyClass>
{
    private readonly Helper _helper;

    public MyComparer(Helper helper)
    {
         if (helper == null)
             throw new ArgumentNullException(nameof(helper));

        _helper = helper;
    }

    public override int Compare(MyClass x, MyClass y)
    {
        // perform comparison using _helper
    }
}

但是,在此方法下,MyComparer类继承了Default类中的CreateComparer<T>静态成员。这是不可取的,因为所述成员的实现与我的派生类无关,并可能导致误导行为:

// calls MyClass.CompareTo or throws InvalidOperationException
MyComparer.Default.Compare(new MyClass(), new MyClass());

由于所需的Helper参数,我的比较器不能有一个默认实例,也不能从Comparison<T>初始化它自己,所以我不能用有意义的实现隐藏继承的静态成员。

此类情况的推荐做法是什么?我正在考虑三种选择:

  • 手动实施IComparer<T>,而不是从Comparer<T>派生,以避免继承所述静态成员

  • 保留继承的静态成员,并假设消费者知道不使用它们

  • 使用抛出InvalidOperationException的新实现隐藏继承的静态成员:

    public static new Comparer<MyClass> Default
    {
        get { throw new InvalidOperationException(); }
    }
    
    public static new Comparer<MyClass> Create(Comparison<MyClass> comparison)
    {
        throw new InvalidOperationException();
    }
    

2 个答案:

答案 0 :(得分:7)

不要继承Comparer<T>。这是代码共享的经典滥用继承。应该使用继承来实现Liskov替换原则(LSP)。继承代码重用是一种破解,因为正如您所发现的那样,它会在您的公共API表面中暴露“垃圾”。

这不是LSP违规,因为没有基本类型合同被破坏。然而,这是对继承的滥用。问题是内部构件以API用户可能错误依赖的方式暴露。它还会阻碍将来的实现更改,因为删除基类可能会破坏用户。

您是否能够容忍肮脏程度取决于您对公共API表面的质量标准。如果您不关心这一点,那么继续坚持DRY而不遵守LSP。如果十亿行代码依赖于您的类,您当然不希望暴露脏基类。这里的问题成为封装(消费者不需要了解比较器的实现)和创建类时保存工作之间的权衡。

你提出了DRY原则。我不确定这是违反DRY的情况。干尝试以防止重复的代码变得不一致,并尝试防止重复的维护工作。由于这里的重复代码永远不会改变(空订购是契约的)我不认为这是一个有意义的DRY违规。相反,它只是在创建实现时保存工作。

实施IComparer<T>很容易,所以这样做。我认为不再需要实施IComparer了。默认实现并不多。如果您关心空输入,则必须在您自己的比较方法中复制该逻辑。您实现的代码重用几乎没有。

  

我正在考虑实现自己的ComparerBase

那就是同样问题的情况。也许您可以改为创建一个实现样板null和类型处理的静态帮助器方法。该静态帮助程序不会向API用户公开。这是“继承的构成”。

隐藏静态成员实在令人困惑。根据呼叫站点的细微变化,将调用不同的方法。此外,它们都没有用。

我不太关心静态方法现在可以通过不同的类型名称获得的事实。这些方法并没有真正继承。它们仅作为C#功能提供。我相信这是针对版本弹性的。从不推荐它,各种工具会围绕此产生警告。我不会太在意这件事。例如,每个Stream“都会继承”某些静态成员,例如Stream.NullStream.Synchronized或其他任何名称。没有人认为这是一个问题。

答案 1 :(得分:1)

在我看来,不做任何事情,即你自己的选择:

  

保留继承的静态成员,并假设   消费者会知道不使用它们

你的班级也继承自latin1_swedish_ci,因此拥有像

这样的东西
System.Object

你永远无法避免这种情况,因为// static method overload inherited from System.Object: MyComparer.Equals(new MyClass(), new MyClass()); // also inherited from System.Object: MyComparer.ReferenceEquals(new MyClass(), new MyClass()); 是你所写的任何类型的基类。

您必须假设使用您的代码的开发人员,在C#中了解object成员(属性,方法等)的工作方式,同样在继承的上下文中。

优秀的开发人员工具(IDE)应该抱怨staticint.ReferenceEqualsMyComparer.ReferenceEquals等等,因为这些都是误导性的写入调用方式。

隐藏MyComparer.Default成员几乎总是一个坏主意。根据我的经验,它让开发人员更加困惑。在可能的情况下,避免使用new修饰符(在类型成员上)。

与usr(请参阅其他答案)不同,我认为new是一个很好的基类。