C# - 值类型等于方法 - 为什么编译器使用反射?

问题描述 投票:0回答:3

我刚刚遇到了一些对我来说非常奇怪的事情:当你在值类型上使用 Equals() 方法时(当然,如果这个方法没有被重写),你会得到一些非常非常慢的东西——字段被比较一到一个使用反射!如:

public struct MyStruct{
   int i;
}

   (...)

   MyStruct s, t;
   s.i = 0;
   t.i = 1;
   if ( s.Equals( t ))   /*  s.i will be compared to t.i via reflection here. */
      (...)

我的问题:为什么 C# 编译器不生成一个简单的方法来比较值类型?类似于(在 MyStruct 的定义中):

   public override bool Equals( Object o ){
      if ( this.i == o.i )
         return true;
      else
         return false;
   }

编译器在编译时就知道 MyStruct 的字段是什么,为什么要等到运行时才枚举 MyStruct 字段?

对我来说很奇怪。

谢谢:)

ADDED:抱歉,我刚刚意识到,当然,

Equals
不是一个语言关键字,而是一个运行时方法......编译器完全不知道这个方法。所以在这里使用反射是有意义的。

c# struct
3个回答
11
投票

以下是来自mscorlib反编译的ValueType.Equals方法:

public override bool Equals(object obj)
{
    if (obj == null)
    {
        return false;
    }
    RuntimeType type = (RuntimeType) base.GetType();
    RuntimeType type2 = (RuntimeType) obj.GetType();
    if (type2 != type)
    {
        return false;
    }
    object a = this;
    if (CanCompareBits(this))
    {
        return FastEqualsCheck(a, obj);
    }
    FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    for (int i = 0; i < fields.Length; i++)
    {
        object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(a, false);
        object obj4 = ((RtFieldInfo) fields[i]).InternalGetValue(obj, false);
        if (obj3 == null)
        {
            if (obj4 != null)
            {
                return false;
            }
        }
        else if (!obj3.Equals(obj4))
        {
            return false;
        }
    }
    return true;
}

如果可能,将进行按位比较(注意 CanCompareBits 和 FastEqualsCheck,两者都定义为 InternalCall。JIT 可能会在这里注入适当的代码。至于为什么这么慢,我无法告诉你。


11
投票

当不需要时,它不使用反射。它只是一点一点地比较值,以防struct

(如果可以的话)。但是,如果任何 
struct
 成员(或成员的成员、任何后代)重写 
object.Equals
 并提供自己的实现,显然,它不能依赖于逐位比较来计算返回值。 

速度慢的原因是

Equals

的参数是
object
类型,并且值类型必须装箱才能被视为
object
。装箱涉及在堆上分配内存以及将值类型复制到该位置的内存。 

您可以手动为

Equals

 方法提供重载,该方法将您自己的 
struct
 作为参数以防止装箱:

public bool Equals(MyStruct obj) { return obj.i == i; }
    

3
投票
编译器生成函数的想法是合理的。

考虑到效果,我认为语言设计团队做得对。 C++ 中编译器生成的方法对于初学者来说很难理解。让我们看看在 C# 中使用自动生成的 struct.Equals 会发生什么:

现在,.Equals() 的概念很简单:

    每个结构体都从 ValueType 继承 Equals。
  • 如果被覆盖,则应用自定义 Equals 方法。
如果编译器总是创建 Equals 方法,我们可以:

    每个结构体都从 Object 继承 Equals。 (ValueType 将不再实现自己的版本)
  • Object.Equals 现在总是(!)被覆盖,无论是由编译器生成的 Equals 方法还是由用户实现
现在我们的结构体有一个代码阅读器看不到的自动生成的重写方法!那么您如何知道基本方法 Object.Equals 不适用于您的结构呢?通过学习编译器自动生成方法的所有案例。而这正是学习C++的负担之一。

将高效的结构 Equals 留给用户并保持概念简单,需要一个标准的默认 Equals 方法,这是一个很好的决定。

也就是说,性能关键结构应该覆盖 Equals。下面的代码显示了

360653 毫秒 在 .Net 4.5.1 上测量

这种性能增益肯定是由于避免了虚拟 Equals,但无论如何,如果调用虚拟 Object.Equals,增益会低得多。然而,性能关键情况不会调用 Object.Equals,因此此处的增益将适用。

using System; using System.Diagnostics; struct A { public int X; public int Y; } struct B : IEquatable<B> { public bool Equals(B other) { return this.X == other.X && this.Y == other.Y; } public override bool Equals(object obj) { return obj is B && Equals((B)obj); } public int X; public int Y; } class Program { static void Main(string[] args) { var N = 100000000; A a = new A(); a.X = 73; a.Y = 42; A aa = new A(); a.X = 173; a.Y = 142; var sw = Stopwatch.StartNew(); for (int i = 0; i < N; i++) { if (a.Equals(aa)) { Console.WriteLine("never ever"); } } sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); B b = new B(); b.X = 73; b.Y = 42; B bb = new B(); b.X = 173; b.Y = 142; sw = Stopwatch.StartNew(); for (int i = 0; i < N; i++) { if (b.Equals(bb)) { Console.WriteLine("never ever"); } } sw.Stop(); Console.WriteLine(sw.ElapsedMilliseconds); } }

另请参阅

http://blog.martindoms.com/2011/01/03/c-tip-override-equals-on-value-types-for-better-performance/

© www.soinside.com 2019 - 2024. All rights reserved.