【问题标题】:Overriding Equals method in Structs在 Structs 中覆盖 Equals 方法
【发布时间】:2011-02-02 07:35:42
【问题描述】:

我已经为结构寻找了最重要的指导方针,但我能找到的只是类。

起初我以为我不必检查传递的对象是否为空,因为结构是值类型,不能为空。但是现在我想起来了,因为等于签名是

public bool Equals(object obj)

似乎没有什么可以阻止我的结构的用户尝试将其与任意引用类型进行比较。

我的第二点涉及我(认为我)在比较结构中的私有字段之前必须进行的转换。我应该如何将对象转换为我的结构类型? C# 的 as 关键字似乎只适用于引用类型。

【问题讨论】:

  • 请注意,我们鼓励您避免在 .Net 中使用可变结构。它的设置你应该大部分时间坚持引用类型(类),并且很少使用结构。
  • 我同意这一点。使用不可变结构没有子类型。那么对于给定的接收器(左侧值),Equals 和 == 应该是相同的,其中唯一的区别是 Equals 需要一个“是”检查,然后,为简单起见,分派到 ==。因此,两个合同都得到了履行,意外情况也得到了缓解。
  • 是的,这个结构是不可变的。我只比较一个整数。
  • 另一个旁注;确保覆盖 GetHashCode() 以匹配逻辑。

标签: c# struct equals overriding


【解决方案1】:

感谢pattern matching in C# 7.0,有一种更简单的方法可以完成接受的答案:

struct MyStruct 
{
    public override bool Equals(object obj) 
    {
        if (!(obj is MyStruct mys)) // type pattern here
            return false;

        return this.field1 == mys.field1 && this.field2 == mys.field2 // mys is already known here without explicit casting
    }
}

您还可以将其缩短为 表达式主体函数

struct MyStruct 
{
    public override bool Equals(object obj) => 
        obj is MyStruct mys
            && mys.field1 == this.field1
            && mys.field2 == this.field2;
}

【讨论】:

  • 或者更简洁:obj is MyStruct mys && this.field1 == mys.field1 && ...
  • 好观察!我编辑了我的答案以反映这一点。
【解决方案2】:

如果有人想知道将结构装箱到 Nullable 对象中的性能损失(以避免来自 is 和强制转换的双重类型检查), 的开销是不可忽略的。

tl;dr:在这种情况下使用 is & cast。

struct Foo : IEquatable<Foo>
{
    public int a, b;

    public Foo(int a, int b)
    {
        this.a = a;
        this.b = b;
    }

    public override bool Equals(object obj)
    {
#if BOXING
        var obj_ = obj as Foo?;
        return obj_ != null && Equals(obj_.Value);
#elif DOUBLECHECK
        return obj is Foo && Equals((Foo)obj);
#elif MAGIC
        ?
#endif
    }

    public bool Equals(Foo other)
    {
        return a == other.a && b == other.b;
    }
}

class Program
{
    static void Main(string[] args)
    {
        RunBenchmark(new Foo(42, 43), new Foo(42, 43));
        RunBenchmark(new Foo(42, 43), new Foo(43, 44));
    }

    static void RunBenchmark(object x, object y)
    {
        var sw = Stopwatch.StartNew();
        for (var i = 0; i < 100000000; i++) x.Equals(y);
        sw.Stop();
        Console.WriteLine(sw.ElapsedMilliseconds);
    }
}

结果:

BOXING
EQ  8012    7973    7981    8000
NEQ 7929    7715    7906    7888

DOUBLECHECK
EQ  3654    3650    3638    3605
NEQ 3310    3301    3319    3297

警告:尽管我确实验证了基准代码本身并没有以奇怪的方式优化,但该测试可能在许多方面存在缺陷。

查看IL,double-check方法编译得更干净一些。

拳击伊利:

.method public hidebysig virtual 
    instance bool Equals (
        object obj
    ) cil managed 
{
    // Method begins at RVA 0x2060
    // Code size 37 (0x25)
    .maxstack 2
    .locals init (
        [0] valuetype [mscorlib]System.Nullable`1<valuetype StructIEqualsImpl.Foo> obj_
    )

    IL_0000: ldarg.1
    IL_0001: isinst valuetype [mscorlib]System.Nullable`1<valuetype StructIEqualsImpl.Foo>
    IL_0006: unbox.any valuetype [mscorlib]System.Nullable`1<valuetype StructIEqualsImpl.Foo>
    IL_000b: stloc.0
    IL_000c: ldloca.s obj_
    IL_000e: call instance bool valuetype [mscorlib]System.Nullable`1<valuetype StructIEqualsImpl.Foo>::get_HasValue()
    IL_0013: brfalse.s IL_0023

    IL_0015: ldarg.0
    IL_0016: ldloca.s obj_
    IL_0018: call instance !0 valuetype [mscorlib]System.Nullable`1<valuetype StructIEqualsImpl.Foo>::get_Value()
    IL_001d: call instance bool StructIEqualsImpl.Foo::Equals(valuetype StructIEqualsImpl.Foo)
    IL_0022: ret

    IL_0023: ldc.i4.0
    IL_0024: ret
} // end of method Foo::Equals

仔细检查 IL:

.method public hidebysig virtual 
    instance bool Equals (
        object obj
    ) cil managed 
{
    // Method begins at RVA 0x2060
    // Code size 23 (0x17)
    .maxstack 8

    IL_0000: ldarg.1
    IL_0001: isinst StructIEqualsImpl.Foo
    IL_0006: brfalse.s IL_0015

    IL_0008: ldarg.0
    IL_0009: ldarg.1
    IL_000a: unbox.any StructIEqualsImpl.Foo
    IL_000f: call instance bool StructIEqualsImpl.Foo::Equals(valuetype StructIEqualsImpl.Foo)
    IL_0014: ret

    IL_0015: ldc.i4.0
    IL_0016: ret
} // end of method Foo::Equals

感谢 Roman Reiner 发现了一个让我看起来不好看的错误。

【讨论】:

  • 你的测试有缺陷的!您的基准正在调用 Foo.Equals(Foo) 方法。 Foo.Equals(object) 永远不会被执行。
  • @RomanReiner:哦,真丢脸。我显然是想把物体扔掉,然后就忘了;后果很严重(实际结果非常不同)——好吧,如果有人重视微基准测试的话。非常感谢!
【解决方案3】:
struct MyStruct 
{
   public override bool Equals(object obj) 
   {
       if (!(obj is MyStruct))
          return false;

       MyStruct mys = (MyStruct) obj;
       // compare elements here

   }

}

【讨论】:

  • 您可以在此处添加override 关键字以清楚起见吗?在 Java 中总是好的做法,在 C# 中应该是一样的。
  • @JohanS 在 C# 中,这不仅仅是一种好习惯,如果您省略 override,该方法实际上会做一些完全不同的事情。
【解决方案4】:

我想,如果使用 .NET 4.5,可以使用documentation 中所述的默认实现:

当您定义自己的类型时,该类型会继承其基本类型的 Equals 方法定义的功能。

ValueType.Equals:值相等;直接逐字节比较或使用反射逐字段比较。

【讨论】:

  • 它实际上早于 4.5,我不知道它是什么时候添加的,但它肯定在 4 中可用。虽然 MSDN 上的一条评论似乎表明它对于浮点类型可能不准确。
  • @Pharap:在定义Equals 时,Microsoft 未能明确说明返回true 的东西应该有多“平等”;在某些情况下,测试 equivalence 的浮点值很有用(正零和负零在数值上相等并不意味着它们是等价的,因为如果 x 和 y 是等价的,那应该意味着1/x==1/y,但对于正零和负零来说不是这样)。某些结构中的浮点值经过等效性测试,但我不知道请求此类测试的一般方法。
  • 相同的文档指出:“对于值类型,您应该始终覆盖 Equals,因为依赖反射的相等性测试性能不佳。”所以这不是一个好主意。
【解决方案5】:

添加到现有答案。

如果您附加一个 ?在结构名称之后(这适用于每个值对象)

int?

也可以通过调用(MyStructName)variableName来完成投射

【讨论】:

  • 可以,但可为空值会带来非常高的性能损失,这将超过使用“as”而不是“is”所获得的任何好处。
  • @DanStory 我不会这么快。如果你想看看this,我很想知道我是否遗漏了什么。 tl;博士:is+cast 确实编译得更好一些,但似乎没有像 as+boxing 那样的“非常高的性能损失”。事实上,我无法让 is+cast 可靠地运行得更快(有时 as+boxing 方法会领先)。
  • @DanStory 我对之前的评论绝对是错误的。惩罚 很高(与微基准测试中的替代方法相比)。编辑了相同的链接答案。
【解决方案6】:

使用is 运算符:

public bool Equals(object obj)
{
  if (obj is MyStruct)
  {
    var o = (MyStruct)obj;
    ...
  }
}

【讨论】:

    猜你喜欢
    • 2014-05-27
    • 1970-01-01
    • 2020-01-30
    • 2015-02-25
    • 2011-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多