为什么运算符"=="不能应用于结构体和默认(结构)?



在结构集合上使用FirstOrDefault()后,我看到了一些奇怪的行为。我已将其隔离到此复制箱中。此程序无法编译

using System;
using System.Linq;
namespace MyProgram {
    public class Program {
        static void Main() {
            var users = new User[] {
            new User() { UserGuid = Guid.NewGuid(), Username = "user01" },
            new User() { UserGuid = Guid.NewGuid(), Username = "user02" }
        };
            var user = users.FirstOrDefault(u => u.Username == "user01");
            Console.WriteLine(user == default(User) ? "not found" : "found");
        }
    }
    public struct User {
        public Guid UserGuid;
        public string Username;
    }
}

编译器错误相当神秘:

运算符"=="不能应用于类型为"MyProgram.User"和"MyProgram.User"的操作数

将结构更改为类工作正常 - 但我不知所措,为什么我不能将结构"实例"与默认值进行比较?

对于类,==运算符使用引用相等性。当然,结构是值类型,因此无法通过引用进行比较。结构没有默认的 == 实现,因为成员比较并不总是有效的比较,具体取决于类型。

您可以改用 Object.Equals 方法,该方法确实按成员比较:

Console.WriteLine(user.Equals(default(User)) ? "not found" : "found");

或者你可以只实现==来调用Object.Equals

public static bool operator ==(User lhs, User rhs)
{
    return lhs.Equals(rhs);
}

但是,结构Equals的默认实现使用反射,因此非常慢。最好自己实现Equals,以及==!=(也可能是GetHashCode):

public override bool Equals(Object obj)
{
    return obj is User && Equals((User)obj);
}
public bool Equals(User other)
{
    return UserGuid == other.UserGuid && Username == other.Username;
}
public static bool operator ==(User lhs, User rhs)
{
    return lhs.Equals(rhs);
}
public static bool operator !=(User lhs, User rhs)
{
    return !lhs.Equals(rhs);
}

你只需要实现它:

public static bool operator == (User u1, User u2) 
{
   return u1.Equals(u2);  // use ValueType.Equals() which compares field-by-field.
}

在 C# 中,== 标记用于表示两个不同的运算符(并非所有语言都对两个运算符使用相同的标记;VB.NET 使用标记 =Is )。 其中一个运算符是可重载相等性测试,并且仅在为两种操作数类型定义重载,或者为一个操作数类型和另一个操作数可隐式转换为的类型定义重载的情况下才可用。 另一个运算符表示引用相等性测试,在以下情况下可用:相等性测试运算符不可用,并且一个操作数是从另一个操作数派生的类类型,一个操作数是类类型,另一个操作数是接口类型,或者两个操作数都是接口类型。

第一个相等测试运算符不能与未为其提供显式覆盖的任何类型(类、接口或结构)一起使用。 但是,如果在第一个相等测试运算符不可用的情况下使用 == 标记,C# 将尝试使用第二个运算符 [请注意,VB.NET 等其他语言不会这样做;VB.NET,尝试使用 = 来比较两个未定义相等测试重载的东西将是一个错误, 即使可以使用Is运算符比较事物]。 第二个运算符可用于将任何引用类型与相同类型的另一个引用进行比较,但不能用于结构。 由于没有为结构定义任何类型的相等运算符,因此不允许进行比较。

如果有人想知道为什么==不简单地回退到 Equals(Object) 上,它适用于所有类型,原因是 == 的两个操作数都受到类型强制的影响,从而阻止其行为与Equals匹配。 例如,1.0f==1.0 和 1.0==1.0f,都将float操作数强制转换为 double,但给定一个表达式,例如 (1.0f).Equals(1.0) 第一个操作数不能被计算为除float之外的任何内容。 此外,如果==映射到Equals,那么C#有必要使用不同的标记来表示引用相等性测试[该语言无论如何都应该这样做,但显然不想这样做]。

使用:

public static bool IsDefault<TValue>(TValue value) => 
   EqualityComparer<TValue>.Default.Equals(value, default(TValue));

或在 C# 7.1+ 中:

public static bool IsDefault<TValue>(TValue value) =>
   EqualityComparer<TValue>.Default.Equals(value, default);

并考虑实施IEquatable<T>

解释
EquityComparer<T>.Default第一次尝试使用IEquatable<T>界面逐渐减少到object.Equals。这既解决了编译器问题,又避免了在实现IEquatable<T>的情况下进行代价高昂的基于反射的结构成员比较。

谨慎
避免使用默认的 object.Equals 方法实现 == 运算符,因为在后台它将使用反射并根据其调用方式对实例进行框控。使用时EqualityComparer<T>.Default.Equals请确保您的结构实现IEquatable<T>否则此方法也会导致引擎盖下的反射。


== 运算符是为对象和 .Net 类型实现的,因此自定义结构将没有默认的 == 运算符实现。

由于这种细微差别,在编译通用相等性测试时,例如:

bool IsDefault<TValue> : where TValue : struct => value == default(TValue)

编译器无法确定要生成的 IL 指令,因为在解析泛型类型之前无法确定正确的相等运算符实现;但是,在 C# 中,泛型是在运行时解析的。因此,即使您确实为自定义结构实现了 == 运算符,当涉及泛型时,您仍然可能遇到问题。

如果要执行此操作,可以重载==运算符

public static bool operator ==(User u1, User u2) 
   {
        return u1.Equals(u2)
   }

您还应该覆盖EqualsGetHashCode()

此外,如果您覆盖==,您可能还希望覆盖!=

public static bool operator !=(User u1, User u2) 
   {
        return !u1.Equals(u2)
   }

比较两种引用类型时,将检查引用是否指向同一类型。

但是,如果您正在处理值类型,则没有要比较的引用。

您必须自己实现运算符,并且(可能)检查值类型的字段是否匹配。