为什么没有 Nullable<T>.Equals(T value) 方法?

8

让我们从一段非常简单的代码开始:

decimal d = 2;

Console.WriteLine("d == 2 = {0}", d == 2);
Console.WriteLine("d == (decimal)2 = {0}", d == (decimal)2);
Console.WriteLine("d.Equals(2) = {0}", d.Equals(2));
Console.WriteLine("d.Equals((decimal)2) = {0}", d.Equals((decimal)2));

结果为 4xtrue。现在,让我们将变量 d 的类型更改为 decimal?:
decimal? d = 2;

这次的结果将是 True, True, False, True。这种情况的解释非常简单。Equals 方法是针对 Nullable<T> 类型实现的,实现方式如下:
public override bool Equals(object other)
{
    if (!this.HasValue)
    {
        return (other == null);
    }
    if (other == null)
    {
        return false;
    }
    return this.value.Equals(other);
}

如果this有一个值而other参数不为空,那么将调用Decimal.Equals(object value)方法。该方法的工作方式是,如果value参数不是decimal类型,则结果将始终为false
我认为当前的实现方法不够直观,我想知道为什么Nullable<T>没有提供带泛型的equals方法,比如:
public bool Equals(T other)
{
    if (!this.HasValue)
        return false;

    return this.value.Equals(other);
}

这是故意的还是疏忽了呢?

评论1:

简要说明一下。我建议 Nullable<T> 应该有两个 Equals 方法,即:public override bool Equals(object other)public bool Equals(T other)


我不明白他们如何提供那个。T 不一定是 IEquatable<T> - Mike Zboray
难道不应该是 public bool Equals(T? other) 方法吗?否则,对于声明 decimal? d1, d2;d1.Equals(d2) 将是无效的。 - user743382
@hvd 我认为他想要一个额外的重载。所以你的例子会进入Equal(object)重载。但是除非你也在T上有一个,否则通用的Equals没有意义。 - Mike Zboray
@mikez T 可以隐式转换为 T?,因此仍然可以在没有特定重载的情况下调用 T。而且如果没有 T? 的重载,使用 int? 参数调用 decimal?.Equals 仍将存在与本问题相同的问题,因为它将解析为 decimal?.Equals(object),而不会将包含的 int 值转换为 decimal 类型。 - user743382
还有一个来自2011年的连接条目,但状态已关闭!http://connect.microsoft.com/VisualStudio/feedback/details/679706/nullable-t-should-implement-iequatable-nullable-t - Jehof
2个回答

3

您可以使用2m代替(decimal)2(以下将使用此方法)。

使用==运算符时,不会发生装箱。 C#编译器将选择最佳匹配的预定义重载(即在C#语言规范中定义的重载;这不一定是真实的.NET方法)operator ==

有多个重载:

operator ==(int x, int y);
operator ==(decimal x, decimal y);

没有像 operator ==(decimal x, int y); 这样的“混合”重载。因为从 intdecimal 存在隐式转换,所以当你使用 == 时,你的字面量 2 会被隐式转换为 2m

对于 Equals,在一些情况下会出现装箱。你不需要使用可空类型。以下是一些例子:

object.Equals(2, 2m);
object.Equals(2m, 2);
((object)2).Equals(2m);
((object)2m).Equals(2);
(2).Equals((object)2m);
(2m).Equals((object)2);
(2).Equals(2m);

返回false!类型为Int32的“Two”与类型为Decimal的“two”不相等。

只有当方法重载导致intdecimal之间的转换时,结果才为true。例如:

(2m).Equals(2);  // true

因此,虽然可以向Nullable<>添加额外的Equals重载,但您所描述的行为与Nullable<>并没有真正关系。

1
尽管我喜欢这些问题,但只有负责类型的设计团队才能真正回答这些问题。一个明显的解决方法是访问Value,即T,并使用该值的Equals方法。
我的最佳猜测是,它可能会强制所有的T都是IEquatable<T>,以便在给定类型上通用地访问Equals<T>。这对于核心值类型来说是可以工作的,但其他.NET结构体不一定实现了该接口,而enum类型也不是。
我想这可以通过类型检查/转换来完成,但与调用者简单地执行myNullable.GetValueOrDefault().Equals()相比,这需要大量的工作。
您可以创建一个扩展方法来执行此任务,为了使其调用所需的方法,您需要明确指定泛型参数(否则编译器将调用Equals(object))。
class Program
{
    static void Main(string[] args)
    {
        double? d = null;

        Console.WriteLine(d.Equals<double>(0.0));

        d = 0.0;

        Console.WriteLine(d.Equals<double>(0.0));

        Console.Read();
    }
}

public static class NullableExtensions
{
    public static bool Equals<T>(this T? left, T right) where T : struct, IEquatable<T>
    {
        if (!left.HasValue)
            return false;

        return right.Equals(left.Value);
    }
}

我曾经问过一个关于为什么需要这个调用的问题。

事实证明,将其强制转换为扩展方法的原因是编译器实现仅在不存在合适的方法时使用扩展方法,在这种情况下,Equals(object)被认为比Equals<T>(T)更合适,因为后者是扩展方法。这在规范中有说明。


1
你:我最好的猜测是,它可能会强制所有的T都成为IEquatable<T>,以便在给定类型上通用地访问Equals<T> 不完全正确。原帖建议的方法可以直接粘贴到Nullable<>源代码中,它可以工作。如果Nullable<>有两个重载都是实例方法,重载决策将不得不在两者之间选择。这将使情况与我的答案中的示例(2m).Equals(2)完全类似。这里有两个重载可供选择,而取decimal的那个更具体。 - Jeppe Stig Nielsen
@JeppeStigNielsen 他的代码不能正常工作,因为唯一可用的方法是 Equals(object),要让它选择特定于类型的版本,您需要以某种方式约束 T,或手动转换并手动调用相关的 Equals。我也说过“可能”,但正如你所说这并不严格正确,你可以测试一下该类型是否实现了 IEquatable<T> 并在没有泛型约束的情况下进行转换。 - Adam Houldsworth
1
我被误解了。在原始问题的额外重载中,额外的重载将由重载决议选择。然后2参数将被隐式转换为2m。你是对的,它最终会调用Equals(object)(实际上是ValueType.Equals(object)),但这没关系,因为2m2m作为对象是相等的。结论:是的,他的代码会“正常”运行。 - Jeppe Stig Nielsen
1
@JeppeStigNielsen 哦,我明白了,但是你不会因为知道 T 而获得任何调用的好处,所以仍然会发生装箱。他的代码将在重新实现 Nullable<T> 的能力下正常工作,而我的代码则作为解决方法运行。 - Adam Houldsworth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接