相等运算符 - 测试两个对象是否相等

==(相等)!=(不等) 运算符检查其操作数是否相等。 当内容相等时,值类型相等。 当两个变量引用同一存储时,引用类型相等。

C# 语言参考记录了 C# 语言的最新发布版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。

本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。

小窍门

若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。

在针对is进行测试时,可以使用==模式匹配运算符作为测试的替代方法。 运算符 is 对所有值和引用类型使用默认相等语义。

相等运算符 ==

如果操作数相等,等于运算符 == 返回 true,否则返回 false

值类型的相等性

如果内置值类型的值相等,则其操作数相等:

int a = 1 + 2 + 3;
int b = 6;
Console.WriteLine(a == b);  // output: True

char c1 = 'a';
char c2 = 'A';
Console.WriteLine(c1 == c2);  // output: False
Console.WriteLine(c1 == char.ToLower(c2));  // output: True

注意

==对于 <><=>=运算符,如果任何作数不是数字(Double.NaNSingle.NaN),则运算结果为false。 该条件表示 NaN 该值不大于、小于或等于任何其他 double (或 float)值,包括 NaN。 有关更多信息和示例,请参阅 Double.NaNSingle.NaN 参考文章。

如果基本整数类型的相应值相等,则相同枚举类型的两个操作数相等。

用户定义的 struct 类型默认情况下不支持 == 运算符。 要支持 == 运算符,用户定义的结构必须重载它。

C# 元组 具有对 == 运算符的 != 内置支持。 有关详细信息,请参阅元组类型一文的元组相等部分。

引用类型的相等性

默认情况下,引用类型作数(不包括记录)在引用同一对象时相等:

public class ReferenceTypesEquality
{
    public class MyClass
    {
        private int id;

        public MyClass(int id) => this.id = id;
    }

    public static void Main()
    {
        var a = new MyClass(1);
        var b = new MyClass(1);
        var c = a;
        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a == c);  // output: True
    }
}

如前面的示例所示,用户定义引用类型默认支持 == 运算符。 但是,引用类型可重载 == 运算符。 如果引用类型重载 == 运算符,使用 Object.ReferenceEquals 方法来检查该类型的两个引用是否引用同一对象。

记录类型相等性

记录类型支持 ==!= 运算符,这些运算符默认提供值相等性语义。 也就是说,当两个记录操作数都是 null 所有字段的对应值或自动实现的属性相等时,两个记录操作数是相等的。

public class RecordTypesEquality
{
    public record Point(int X, int Y, string Name);
    public record TaggedNumber(int Number, List<string> Tags);

    public static void Main()
    {
        var p1 = new Point(2, 3, "A");
        var p2 = new Point(1, 3, "B");
        var p3 = new Point(2, 3, "A");

        Console.WriteLine(p1 == p2);  // output: False
        Console.WriteLine(p1 == p3);  // output: True

        var n1 = new TaggedNumber(2, new List<string>() { "A" });
        var n2 = new TaggedNumber(2, new List<string>() { "A" });
        Console.WriteLine(n1 == n2);  // output: False
    }
}

如前面的示例所示,引用类型成员的相等性使用其特定的相等实现进行比较。

字符串相等性

如果两个字符串均为 或者两个字符串实例具有相等长度且在每个字符位置有相同字符,则这两个null操作数相等:

string s1 = "hello!";
string s2 = "HeLLo!";
Console.WriteLine(s1 == s2.ToLower());  // output: True

string s3 = "Hello!";
Console.WriteLine(s1 == s3);  // output: False

字符串相等比较是区分大小写的序号比较。 有关字符串比较的详细信息,请参阅如何在 C# 中比较字符串

委托相等

当两个委托作数及其调用列表的长度相同且每个位置都有相等条目时,同一运行时类型的两个null作数相等:

Action a = () => Console.WriteLine("a");

Action b = a + a;
Action c = a + a;
Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
Console.WriteLine(b == c);  // output: True

重要

调用列表中的等项包括调用中的所有固定参数,包括接收方。 接收方是调用条目时所表示 this 的对象实例。

var o1 = new object();
var o2 = new object();
var d1 = o1.ToString;
var d2 = o2.ToString;
Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
Console.WriteLine(d1 == d2);  // output: False (different receivers)

有关详细信息,请参阅 C# 语言规范中的委托相等运算符部分。

来自计算语义上相同的 lambda 表达式 的委托不相等,如以下示例所示:

Action a = () => Console.WriteLine("a");
Action b = () => Console.WriteLine("a");

Console.WriteLine(a == b);  // output: False
Console.WriteLine(a + b == a + b);  // output: True
Console.WriteLine(b + a == a + b);  // output: False

不相等运算符 !=

如果作数不相等,!=则不相等运算符true返回false。 对于内置类型的操作数,表达式 x != y 生成与表达式 !(x == y) 相同的结果。 有关类型相等性的更多信息,请参阅相等运算符部分。

以下示例演示如何使用 != 运算符:

int a = 1 + 1 + 2 + 3;
int b = 6;
Console.WriteLine(a != b);  // output: True

string s1 = "Hello";
string s2 = "Hello";
Console.WriteLine(s1 != s2);  // output: False

object o1 = 1;
object o2 = 1;
Console.WriteLine(o1 != o2);  // output: True

类层次结构中的相等性

记录在没有手动工作的情况下正确处理继承。 编译器生成的相等性检查运行时类型和所有声明的属性,因此它会自动满足对称性和可传递性要求。 当值相等性是目标时,首选 record 于手动未密封层次结构。

重要

尽可能使用 record — 编译器会为你生成所有必需的相等成员。 仅当类型必须派生自非记录类或具有阻止 record的其他约束时,才需要手动实现。

当某个类型无法定义为 record 类型时,自行实现相等性

下面是无法记录的值类型的最小手动实现:

class Color : IEquatable<Color>
{
    public Color(int r, int g, int b)
    {
        R = r;
        G = g;
        B = b;
    }

    public int R { get; }
    public int G { get; }
    public int B { get; }

    public bool Equals(Color? other) =>
        other is not null && R == other.R && G == other.G && B == other.B;

    public override bool Equals(object? obj) => obj is Color other && Equals(other);
    public override int GetHashCode() => HashCode.Combine(R, G, B);
}

实现提供三个必需成员: Equals(T?) 作为核心比较, override Equals(object?) 用于对象级调用, override GetHashCode() 因此基于哈希的集合正常工作。 HashCode.Combine 是一个库帮助程序,它从所使用的 Equals相同值生成一个哈希。 实现 IEquatable<T>Equals(T?) 重载)是可选的,但在调用方已有具体类型时,可以避免装箱。

==此外,定义语言!=并要求它们成对;警告 CS0660CS0661 提醒你保持所有四个成员一致。

如果上述三个成员已到位, Equals 则反映值相等,但仍 == 会测试标识,因为 == 尚未声明任何运算符:

var red1 = new Color(255, 0, 0);
var red2 = new Color(255, 0, 0);

Console.WriteLine(red1.Equals(red2)); // => True
Console.WriteLine(red1 == red2);      // => False  (no == overload; identity check)

正确的实现还必须满足等效协定(假定xy,且z为非 null):

  1. 反反x.Equals(x) 返回 true
  2. 对称x.Equals(y) 返回的值与 y.Equals(x).
  3. 可传递:如果 x.Equals(y)y.Equals(z) 都是两者 true,则必须 x.Equals(z)true
  4. 一致:只要两个对象都不会更改,连续调用将 x.Equals(y) 返回相同的值。
  5. Null 行为x.Equals(null) 返回 false; x.Equals(y) 在非 null x调用时不得引发。

未密封类层次结构中的值相等性要求比密封类中的值相等性更符合对称和可传递规则。 危险在于 IEquatable<T>.Equals(T? other) 调度遵循变量声明 的类型 (在变量声明中写入的类型),而不是其运行时类型。 如果Shape声明类型为非virtualEquals(Shape? other)变量(在Shape运行时Shape.Equals调用时保留),Circle则以无提示方式忽略Circle特定字段。 通过变量访问时,具有不同弧度的两 CircleShape 对象可以比较为相等。

正确的模式需要两个合作要求:使类型化 Equals 方法 virtual 使每个派生类都可以扩展比较,并在基类实现中添加一个 GetType() == other.GetType() 防护,以便从不考虑不同运行时类型的对象相等。

基类实现

// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}

要点:

  • virtual typed Equals:每个派生类都重写此方法,以增强与其自己的字段的比较。
  • GetType() guardGetType() == other.GetType() 防止同 Circle 一颜色相等 Shape ,并防止不同派生类型的对象彼此相等。
  • GetHashCode 包括 GetType():由于两个对象仅在运行时类型匹配时相等, GetHashCode 因此必须对运行时类型和数据字段进行哈希处理。 在此处省略 GetType() 会导致错误的行为和 Dictionary<TKey,TValue>HashSet<T>
  • == 委托给 Equals:保持运算符和方法相等一致。

派生类实现

一个派生类,该类添加字段将重写类型 Equals化、强制转换为其自己的类型、调用 base.Equals,然后比较其自己的字段:

class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}

base.Equals(c) GetType()强制保护并检查共享字段。 当参数是Shape任何其他派生类型时,通过other is Circle c强制转换失败。

通过基类型变量使用

Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)

密封类更简单

不能将类 sealed 子类化,因此编译时和运行时类型始终同意。 不需要防护 GetType()virtual 调度。 当IEquatable<T>类型不能为记录时,“实现相等”中所示的模式是正确的,并且对于密封类来说已完成。

运算符可重载性

可以在用户定义的类型中重载==!=运算符。 如果重载这两个运算符之一,则还必须重载另一个运算符。

不能在记录类型中显式重载 ==!= 运算符。 若要更改记录类型的==!=运算符的行为T,请使用以下签名实现IEquatable<T>.Equals方法:

public virtual bool Equals(T? other);

C# 语言规范

有关详细信息,请参阅 C# 语言规范中的关系和类型测试运算符部分。

有关记录类型的相等性的详细信息,请参阅 C# 语言规范“相等成员”部分。

另请参阅