none
请教一下大家啊,结构体的一个奇怪现象? RRS feed

  • 问题

  • 结构体的Equals方法是采用值语义来比较的,也就是只要两个结构体的实例的值相等,两个结构体实例就相等。

    大家看这个Demo:

        public struct S
        {
            public double a;
            public double b;
        }

                S s1, s2;
                s1.a = s2.a = 1;
                s1.b = 0.0;
                s2.b = -0.0;
                bool r2 = s1.Equals(s2); // 这里居然是false
                Console.WriteLine(r2);

     

    而且更奇怪的是把struct定义的a字段改成float

        public struct S
        {
            public float a;
            public double b;
        }

    那么s1.Equals(s2)就返回true了,这个太奇怪了,a的类型不知道怎么会影响比较的结果!

    经测试,只要a和b是同样的类型,同时是float类型或同时是double类型,那结果就是false,只有a和b类型不同,结果才是true.

    这个问题研究好久了,实在是没有想出为什么是这样的现象,请教一下大家啊。

     


    周雪峰
    2010年7月19日 13:14
    版主

答案

  • 查了一下原因,情况是这样的。

    因为 struct 的 Equals 方法,是实际上调用 System.ValueType.Equals(Object) 方法。而这个方法做了这 4 步的计算:

    1、判断传入的参数 obj 是否为 null,如果 null,返回 false。
    2、判断传入参数的类型是不是与被比较的 this 类型相同,如果不同,返回 false。
    3、判断如果 this 和传入参数可以被二进制比较,则进行快速二进制比较。
    4、利用反射获得每一个公共字段的值,并调用其基础类型上的 Equals 方法进行逐个比较。

    您的问题是,因为 struct 类型是值类型,当其所有域的类型相同时,其内部数据类型会得到优化,以节约存储空间 (类似于 C++ 的 Align)。上面的代码使用了基元类型 (primitive type) 且所有域都是同一基元类型,此时第三步 CanCompareBits 返回 true,因此调用了 FastEqualsCheck 而没有进行第四步,所以,结果返回 false。

    这可能是 .NET CLR 的一个潜在 bug,不幸的是我并不能拿到 CanCompareBits 和 FastEqualsCheck 的源代码 (因为它们是用本地汇编写的)。但请将这个问题提交给 Microsoft,以便得到更好的解释。

    需要说明的是,如果将 foo2.Bar2 和 foo1.Bar2 都赋值为非 0 的 double 值,结果就会是 true。


    Mark Zhou
    2010年7月20日 8:36
  • 只能说  结构那个b运行转换操作造成两个数相等  你拿个最简单-0.0例子执行转换任何点数类型都是0

    如果结构里面类型都相同 在编译器上 不用运行转换操作 得到优化 执行更快

    如果是-0.1的值  那就正常了 

     


    77138191qq群 .net与asp.net
    2010年7月19日 23:48
    版主

全部回复

  • 只能说  结构那个b运行转换操作造成两个数相等  你拿个最简单-0.0例子执行转换任何点数类型都是0

    如果结构里面类型都相同 在编译器上 不用运行转换操作 得到优化 执行更快

    如果是-0.1的值  那就正常了 

     


    77138191qq群 .net与asp.net
    2010年7月19日 23:48
    版主
  • 查了一下原因,情况是这样的。

    因为 struct 的 Equals 方法,是实际上调用 System.ValueType.Equals(Object) 方法。而这个方法做了这 4 步的计算:

    1、判断传入的参数 obj 是否为 null,如果 null,返回 false。
    2、判断传入参数的类型是不是与被比较的 this 类型相同,如果不同,返回 false。
    3、判断如果 this 和传入参数可以被二进制比较,则进行快速二进制比较。
    4、利用反射获得每一个公共字段的值,并调用其基础类型上的 Equals 方法进行逐个比较。

    您的问题是,因为 struct 类型是值类型,当其所有域的类型相同时,其内部数据类型会得到优化,以节约存储空间 (类似于 C++ 的 Align)。上面的代码使用了基元类型 (primitive type) 且所有域都是同一基元类型,此时第三步 CanCompareBits 返回 true,因此调用了 FastEqualsCheck 而没有进行第四步,所以,结果返回 false。

    这可能是 .NET CLR 的一个潜在 bug,不幸的是我并不能拿到 CanCompareBits 和 FastEqualsCheck 的源代码 (因为它们是用本地汇编写的)。但请将这个问题提交给 Microsoft,以便得到更好的解释。

    需要说明的是,如果将 foo2.Bar2 和 foo1.Bar2 都赋值为非 0 的 double 值,结果就会是 true。


    Mark Zhou
    2010年7月20日 8:36
  • 忘记贴上我的测试代码了:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    namespace Demo
    {
        using System.Runtime.InteropServices;

        class Program
        {
            static void Main(string[] args)
            {
                Foo foo1, foo2;
                foo1.Bar1 = foo2.Bar1 = 1f;

                foo1.Bar2 = 0.0;
                foo2.Bar2 = -0.0;

                Console.WriteLine(foo1.Bar1 == foo2.Bar1 && foo1.Bar2 == foo2.Bar2);

                Console.WriteLine(foo1.Equals(foo2));
                Console.ReadKey();
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        struct Foo
        {
            public double Bar1;
            public double Bar2;
        }
    }

    还有 Equals 的源代码。

    .method public hidebysig virtual instance bool Equals(object obj) cil managed
    {
        .maxstack 3
        .locals init (
            [0] class System.RuntimeType 'type',
            [1] class System.RuntimeType type2,
            [2] object obj2,
            [3] object obj3,
            [4] object obj4,
            [5] class System.Reflection.FieldInfo[] infoArray,
            [6] int32 num)
        L_0000: ldarg.1
        L_0001: brtrue.s L_0005
        L_0003: ldc.i4.0
        L_0004: ret
        L_0005: ldarg.0
        L_0006: call instance class System.Type System.Object::GetType()
        L_000b: castclass System.RuntimeType
        L_0010: stloc.0
        L_0011: ldarg.1
        L_0012: callvirt instance class System.Type System.Object::GetType()
        L_0017: castclass System.RuntimeType
        L_001c: stloc.1
        L_001d: ldloc.1
        L_001e: ldloc.0
        L_001f: call bool System.RuntimeType::op_Inequality(class System.RuntimeType, class System.RuntimeType)
        L_0024: brfalse.s L_0028
        L_0026: ldc.i4.0
        L_0027: ret
        L_0028: ldarg.0
        L_0029: stloc.2
        L_002a: ldarg.0
        L_002b: call bool System.ValueType::CanCompareBits(object)
        L_0030: brfalse.s L_003a
        L_0032: ldloc.2
        L_0033: ldarg.1
        L_0034: call bool System.ValueType::FastEqualsCheck(object, object)
        L_0039: ret
        L_003a: ldloc.0
        L_003b: ldc.i4.s 0x34
        L_003d: callvirt instance class System.Reflection.FieldInfo[] System.Type::GetFields(valuetype System.Reflection.BindingFlags)
        L_0042: stloc.s infoArray
        L_0044: ldc.i4.0
        L_0045: stloc.s num
        L_0047: br.s L_0089
        L_0049: ldloc.s infoArray
        L_004b: ldloc.s num
        L_004d: ldelem.ref
        L_004e: castclass System.Reflection.RtFieldInfo
        L_0053: ldloc.2
        L_0054: ldc.i4.0
        L_0055: callvirt instance object System.Reflection.RtFieldInfo::InternalGetValue(object, bool)
        L_005a: stloc.3
        L_005b: ldloc.s infoArray
        L_005d: ldloc.s num
        L_005f: ldelem.ref
        L_0060: castclass System.Reflection.RtFieldInfo
        L_0065: ldarg.1
        L_0066: ldc.i4.0
        L_0067: callvirt instance object System.Reflection.RtFieldInfo::InternalGetValue(object, bool)
        L_006c: stloc.s obj4
        L_006e: ldloc.3
        L_006f: brtrue.s L_0077
        L_0071: ldloc.s obj4
        L_0073: brfalse.s L_0083
        L_0075: ldc.i4.0
        L_0076: ret
        L_0077: ldloc.3
        L_0078: ldloc.s obj4
        L_007a: callvirt instance bool System.Object::Equals(object)
        L_007f: brtrue.s L_0083
        L_0081: ldc.i4.0
        L_0082: ret
        L_0083: ldloc.s num
        L_0085: ldc.i4.1
        L_0086: add
        L_0087: stloc.s num
        L_0089: ldloc.s num
        L_008b: ldloc.s infoArray
        L_008d: ldlen
        L_008e: conv.i4
        L_008f: blt.s L_0049
        L_0091: ldc.i4.1
        L_0092: ret
    }

     


    Mark Zhou
    2010年7月20日 8:40
  • 把s2.b = -0.0;改成s2.b = 0.0;就是true。不明白。
            public struct S
            {
                public double a;
                public double b;
            }
            private void Form1_Load(object sender, EventArgs e)
            {
                S s1, s2;
                s1.a = s2.a = 1;
                s1.b = 0.0;
                s2.b = 0.0;
                bool r2 = s1.Equals(s2);
    2010年7月20日 11:33
  • 因為多加了那個負號,

    您認為那個負號會不會被存儲在記憶體中呢?

     



    • 已编辑 DK. Da 2012年6月10日 0:54
    2010年7月21日 6:24
  • 感谢mazhou提供的解释,同时感谢大家的热心回复。

    我总结一下这个问题吧!这个应该是.NET运行时的一处Bug,VulueType在内部会调用CanCompareBits方法(和遗憾,我也没有拿到CanCompareBits方法的源代码,正如mazhou,为了速度的提升,是本地的汇编写的)来判断是否进行二进制比较来提升速度,但是当我把两个字段都设置成double的时候,他错误的返回 true,这是对每个字段进行的是二进制比较,0.0和-0.0明显二进制不相等,于是发生了上面的问题。

    我的朋友在Mono(Linux上的.NET实现)上测试这段代码,并没有发现这个问题。只有微软实现的运行时才有这个问题。

     


    周雪峰
    2010年7月22日 12:07
    版主
  • @周雪峰:

    可以把这个问题提交到 Connect 么?这样相关的 Team (应该是 CLR Team) 可以采取进一步的措施。我也很好奇 CanCompareBits 怎么实现的。


    Mark Zhou
    2010年7月23日 9:40
  • 嗯,可以提交的。你提交应该更方便吧!
    周雪峰
    2010年7月23日 14:29
    版主
  • 我怎么访问不了这个链接啊!
    周雪峰
    2010年8月1日 10:24
    版主
  • 你得把这个connect添加到自己的配置中。。。
    Most questions i'm interested in might have two or more possible answers i know or i don't know. So please read question carefully before you try to answer, and explan your question detailedly before asking for help. 很多看起来简单的问题都存在多种可能性,如果您不能详细的解释,别人就不能正确判断出您所遭遇的实际状况,因而不能给出最适合的解决办法。在您没有给出详细信息的情况下,施助者只有张贴大量有可能有关的解决办法。而您可能没有耐心阅读所有这些东西,在这种情况下您就客观地形成了对施助者的伤害——除非“施助者”并没有用心去尝试帮助您。 同样地,当您尝试解答一个看起来“好像遇到过”的问题的时候,您也需要详细地阅读和理解这个问题。如果您不了解问题的细节,您可能会给出不相关的或者无助于解决当前问题的解答。
    2010年8月1日 18:11
  • 目前看来 CLR Team 好像不想修这个问题,他们跟我说正零和负零的浮点表示是不同的,做二进制比较时会明显不同。最明显的区别是负数的最高位是 1 而正数是 0。

    这个大家都知道,不过。。。看看其他人有没有不同的看法。

    我会继续关注这个问题。


    Mark Zhou
    2010年10月4日 10:02
  • 看到这个帖子,感觉很有趣。

    但我想说,我其实比较赞同CLR Team的意见。

    有时候做一些数值计算,中间过程可能会算出很高精度的值,就好象两个极限,一个从正无穷逼近零,一个从负无穷逼近零。

    这样,两个数值在普通用户眼里是没有区别的,而在一些科学应用中,符号的信息很重要。

    其实,微软也不是不解决,他们针对普通用户提出了采用override Equals的方法,而不去修改CLR。

    这样可以保证更大范围的受众。


    喜欢C#,喜欢看上去很酷、或者用起来很有用、或者很高效的代码
    2010年10月5日 2:16
  • 很受益!
    张威
    2010年12月30日 0:56
  • 这个问题已经从 Product Studio 上看到 Fixed by design。也就是不会修正这个问题。有任何其他问题请继续跟帖。
    Mark Zhou
    2010年12月30日 7:54