派生类是否应该隐藏从 Comparer 继承的"默认"和"创建静态成员<T>"?



我正在按照 MSDN 的建议,通过从 Comparer<T> 类派生来编写IComparer<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并从Comparer<T>Create静态成员。这是不可取的,因为上述成员的实现与我的派生类无关,并且可能导致误导行为:

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

不要从Comparer<T>继承。这是代码共享对继承的经典误用。继承应该用于实现利斯科夫替代原则(LSP)。继承代码重用是一种黑客攻击,因为当你发现它时,它会在你的公共 API 图面中公开"垃圾"。

这不是 LSP 违规,因为没有违反基本类型合同。然而,这是对继承的滥用。问题在于,内部结构正在以API用户可能错误依赖的方式公开。它还阻碍了将来的实现更改,因为删除基类可能会中断用户。

是否可以容忍这种脏污取决于公共 API 图面的质量标准。如果您不关心这一点,请继续遵守 DRY,同时不遵守 LSP。如果有十亿行代码依赖于你的类,你当然不想公开一个脏的基类。这里的问题变成了封装(使用者不需要知道比较器的实现)和创建类时节省工作之间的权衡。

你提出了 DRY 原则。我不确定这是违反 DRY 的实例。Dry 尝试防止重复的代码变得不一致,并尝试防止重复的维护工作。由于此处的重复代码永远不会更改(空排序是合同性的),我认为这不是有意义的 DRY 违规行为。相反,它只是在创建实现时节省工作。

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

正在考虑实现我自己的比较器库

这将是同一问题的情况。也许您可以改为创建一个静态帮助程序方法来实现样板 null 和类型处理。该静态帮助程序不会向 API 用户公开。这就是"组合重于继承"。

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

我不太关心静态方法现在可以通过不同的类型名称使用的事实。这些方法并不是真正继承的。它们仅作为 C# 功能提供。我相信这是为了版本控制弹性。从不建议这样做,并且各种工具会围绕此生成警告。我不会太担心这一点。例如,每个Stream都"继承"某些静态成员,例如Stream.NullStream.Synchronized或任何它们的名称。没有人认为这是一个问题。

在我看来,什么都不做,即你自己的选择:

将继承的静态成员保留在原位,并假设 消费者将知道不要使用它们

你的类也继承自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());

你永远无法避免这一点,因为object是你编写的任何类型的基类。

您必须假定使用代码的开发人员了解static成员(属性、方法等)在 C# 中的工作方式,包括在继承上下文中也是如此。

好的开发人员工具(IDE)应该抱怨int.ReferenceEqualsMyComparer.ReferenceEqualsMyComparer.Default等,因为这些是编写调用的误导性方式。

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

与usr不同(见其他答案),我认为Comparer<>是一个很好的基类。

最新更新