zoukankan      html  css  js  c++  java
  • C# 相等比较

    C# 相等比较

    有两种类型的相等:

    • 值相等:即两个值是一样的
    • 引用相等:即引用是一样的,也就是同一个对象

    默认地,对于值类型来讲,相等指的就是值相等;对于引用类型,相等就是指的引用相等。

    int a = 5;
    int b = 5;
    Console.WriteLine(a == b);
    

    image-20211004211745162

    class Foo { public int x; }
    Foo f1 = new Foo { x = 5 };
    Foo f2 = new Foo { x = 5 };
    Console.WriteLine(f1 == f2);
    

    image-20211004212014041

    标准相等协议

    有三种标准相等协议:

    • ==!=运算符
    • object的虚函数Equals
    • `IEquatablee接口

    另外还有pluggable协议,IStructuralEquatable接口。

    ==和!=

    当使用==!=,C#在编译的过程就确定哪个类型来进行比较,不需要调用虚方法。

    下面例子,编译器用int类型的==:

    int x =5;
    int y=5;
    Console.WriteLine(x==y);//return True;
    

    下面例子,编译器用object类的==:

    object x=5;
    object y=5;
    Console.WriteLine(x==y);//return false;
    
    Foo f1 = new Foo { x = 5 };
                Foo f2 = null;
                Console.WriteLine(f2==f2);//true
                Console.WriteLine();
    

    虚方法 Object.Equals

    object x = 5;
    object y = 5;
    Console.WriteLine(x.Equals(y));
    

    image-20211004223833617

    虚方法默认为这样的:

    public virtual bool Equals(object obj)
    
    {
    
      if(obj==null) return false;
    
      if(GetType() != obj.GetType()) return false;
    if(obj==this)
    
      Return true;
    
    }
    

    由此可以看出,默认的实现其实比较的是两个对象的内存地址。值类型和string类型除外,因为所有值类型继承于System.ValueType()(System.ValueType()同样继承于Object,但是System.ValueType()本身却是引用类型),而System.ValueType()对Equals()和==操作符进行了重写,是逐字节比较的。而string类型是比较特殊的引用类型,所以strIng在很多地方都是特殊处理的,此处就不做深究了。

    int a = 5;
    int b = 5;
    Console.WriteLine(a.Equals(b));//true
    
    string a = "abc";
    string b = "abc";
    Console.WriteLine(a.Equals(b));//true
    
    string a = "abc";
    string b = "abc";
    Console.WriteLine(a==b);//true
    
    int a = 5;
    int b = 5;
                Console.WriteLine(a == b);//true
    

    Equals方法在运行时根据object的实际类型来调用,在上例中,它调用了Int32的Equals方法,所以是true.

    int x = 5;
    double y = 5;
    Console.WriteLine(x.Equals(y));//return false
    
    			object x = 3, y = 3;
                Console.WriteLine(x.Equals(y));
                x = null;
                Console.WriteLine(x.Equals(y));
                y = null;
                Console.WriteLine(x.Equals(y));
    

    image-20211004234125015

    虚函数,如果调用者本身就是null,那么将抛出异常

    调用Int32的Equals方法,然而x,y类型不一样,所以返回false,只有y也是Int类型,并且值与x一样的时候,才返回true

    那么,为什么C#的设计者不通过让==也变成虚方法,从而让其与Equals等价,从而避免了复杂性?

    其实它主要考虑了三个原因:

    • 如果第一个操作数是null,Equals方法失效,会抛出NullReferenceException;而==不会。
    • ==是静态的调用,所以它执行起来也相当快。
    • 有时候,==Equals可能对“相等”有不同的含义。

    下面方法比较了任何类型是否相等:

    public static bool AreEqual(object obj1,object obj2)
        => obj1==null?obj2==null:obj1.Equals(obj2);
    

    静态方法object.Equals

    object类提供了一个静态方法,其作用与上面的AreEqual作用一样,即可以比较任何类型是否相等,但需要装箱,包括是否是null,它就是Equals,接受2个参数:

    public static bool Equals(object obj1,object obj2)
    

    这就提供了一个null-safe的相等比较算法,当类型在编译时未确定,比如:

    object x=3,y=3;
    Console.WriteLine(object.Equals(x,y));//return true;
    x=null;
    Console.WriteLine(object.Equals(x,y));//return false;
    y=null;
    Console.WriteLine(object.Equals(x,y));//return true;
    

    而如果上面Equals全部用==代替:

    object x = 3, y = 3;
                Console.WriteLine(x==y);
                x = null;
                Console.WriteLine(x == y);
                y = null;
                Console.WriteLine(x == y);
    

    image-20211004230542725

    一个重要的应用就是当在写泛型类型的时候,就不能用==

    image-20211004231115508

    所以必须这样写:

    public class Test<T>{
        T _value;
        public void SetValue(T newValue)
        {
            if (!object.Equals(newValue,_value))
            {
                _value=newValue;
                OnValueChanged();
            }
        }
        protected virtual void OnValueChanged(){...}
    }
    

    ==运算符在这里是不允许的,因为它要在编译的时候就确定是哪个类型。

    另一种方法是用EqualityComparer<T>泛类,这避免了使用object.Equals而必需的装箱。

    静态方法 object.ReferenceEquals

    有时候,需要强行执行引用对比,这时候就要用到了object.ReferenceEquals.

    object x = 3, y = 3;
                Console.WriteLine(object.ReferenceEquals(x,y));
                x = null;
                Console.WriteLine(object.ReferenceEquals(x, y));
                y = null;
                Console.WriteLine(object.ReferenceEquals(x, y));
    

    image-20211004232027085

    class Widget{...}
    class Test
    {
        static void Main()
        {
            Widget w1=new Widget();
            Widget w2=new Widget();
            Console.WriteLine(object.ReferenceEquals(w1,w2));//return false
        }
    }
    

    对于Widget,有可能虚函数Equals已经被覆盖了,以致w1.Equals(w2)返回true,也有可能重载了==运算符,以致w1==w2返回true

    在这种情况下,object.ReferenceEquals保证了一般的引用相等的语义。

    另一种强制引用相等的办法是先把value转换为object类型,然后用==运算符

    IEquatable<T>

    object.Equals静态方法必须要装箱,这对于高性能敏感的程序是不利的,因为装箱是相对昂贵的,与实际的比较相比。解决办法就是IEquatable<T>

    public interface IEquatable<T>
    {
        bool Equals(T other);
    }
    

    它给出调用objectEquals虚方法相同效果的结果,但更快,你也可以用IEquatable<T>作为泛型的约束:

    class Test<T> where T:IEquatable<T>
    {
        public bool IsEqual(T a,T b)
        {
            reurn a.Equals(b);//不用装箱
        }
    }
    

    IsEqual被调用时,它调用了a.Equals,给出object的虚函数Equals的相同的效果,如果去掉约束,会发现仍然可以编译,但此时a.Equals调用的是object.Equals静态方法,也就是实际进行装箱操作了,所以就相对慢了些。

    当Equals和==是不同的含义

    有时候对于==Equals赋予不同的“相等”含义是非常有用的,比如:

    double x=double.NaN;
    Console.WriteLine(x==x);//false
    Console.WriteLine(x.Equals(x));//true
    

    double类型的==运算符强制任何一个NaN和任何一个数都不相等,对于另一个NaN也不相等,从数学的角度,这是非常自然的。而Equals,需要遵守一些规定,比如:

    x.Equals(x) must always return true.
    

    集合和字典就是依赖Equals的这种行为,否则,就找不到之前储存的Item了。

    对于值类型来讲,==Equals有不同的涵义实际上是比较少的,更多的场景是引用类型,用==来进行引用相等的判断,用Equals来进行值相等的判断。StringBuilder就是这样做的:

    var sb1 = new StringBuilder("foo");
                var sb2 = new StringBuilder("foo");
                Console.WriteLine(sb1 == sb2);//false,reference equal
                Console.WriteLine(sb1.Equals(sb2));//true
    

    image-20211005092038236

    Equality and Custom type

    值类型用值相等,引用类型用引用相等,结构的Equals方法默认用structural value equality(即比较结构中每个字段的值)。

    当写一个类型的时候,有两种情况可能要重写相等:

    • 改变相等的涵义

    当默认的==Equals对于所写的类型的涵义不是那么自然的时候,这时候,就有必要重新了。

    • 加速结构的相等的比较

    结构默认的structural equality比较算法是相对慢的,通过重写Equals可以提升它的速度,重载==IEquatable<T>允许非装箱相等比较,又可以再提速。

    重载引用类型的相等语义,意义不大,因为默认的引用相等已经非常快了。

    如果定义的类型重写了Equals方法,还应该重写GetHashCode方法,事实上,如果类型重写Equals的同时,没有重写GetHashCode,C#编译器就会生成一条警告。

    image-20211005130255089

    之所以还要定义GetHashCode,是由于在System.Collections.Hashtable类型,System.Collections.Generic.Dictionary类型及其他一些集合的实现中,要求两个对象必须具有相同哈希码才视为相等,所以重写Equals就必须重写GetHashCode,确保相等性算法和对象哈希码算法一致,否则就是哈希码就是类型实例默认的地址。

    IEqualityComparer,IEqualityComparer<T>接口则强行要求要同时实现Equals,GetHashCode方法,EqualityComparer抽象类则同时继承了这两个接口,只需要重新Equals(T x,T,y),GetHashCode(T obj)即可(https://www.cnblogs.com/johnyang/p/15417804.html),方便在需要判断是否两个对象相等的场景下,作为参数,或者调用者本身来使用。

    重载相等语义的步骤:

    • 重载GetHashCode()和Equals()
    • (可选)重载!=,==,应实现这些操作符的用法,在内部调用类型安全的Equals
    • (可选)运用IEquatable<T>,这个泛型接口允许定义类型安全的Equals方法,通常重载的Equals接受一个Object参数,以便于在内部调用类型安全的Equals方法。

    为什么在重写Equals()时,必须重写GetHashCode()的例子:

        class Foo:IEquatable<Foo>
        { public int x;
            public override bool Equals(object obj)//重写Equals算法,注意这里参数是object
            {
                if (obj == null)
                    return base.Equals(obj);//base.Equal是
                return Equals(obj as Foo);
            }
            public bool Equals(Foo other) //实现IEquatable接口,注意这里参数是Foo类型
            {
                if (other == null)
                    return base.Equals(other);
                return this.x == other.x;
            }
            //public override int GetHashCode()
            //{
            //    return this.x.GetHashCode();
            //}
    
        }
    
    
    public void Main()
    {
                var f1 = new Foo { x = 5 };
                var f2 = new Foo { x = 3 };
                var f3 = new Foo { x = 5 };
                var flist = new List<Foo>();
                flist.Add(f1);
                flist.Add(f3);
                flist.Add(f2);
                Console.WriteLine(f1.Equals(f3));
                Console.WriteLine(flist.Contains(f3));
                Console.WriteLine(flist.Distinct().Count());
                var dic = new Dictionary<Foo, string>();
                dic.Add(f1,"f1");
                dic.Add(f2, "f2");
                Console.WriteLine(dic[f3]);
    }
    

    image-20211005143919152

    在注释了GetHashCode重写代码后,我们运行上面的程序,就会发现,虽然Equals可以正常工作,但对于list的distinct的数量,显然错误,f1既然是和f2相等,那么数量应该是2,还有最后发现字典访问键f3也访问不了了,这当然是没有重写GetHashCode的后果,因为根据key取值的时候也是把key转换成HashCode而且验证Equals后再取值,也就是说,只要GetHashCode和Equlas中有一个方法没有重写,在验证时没有重写的那个方法会调用基类的默认实现,而这两个方法的默认实现都是根据内存地址判断的,也就是说,其实一个方法的返回值永远会是false。其结果就是,存储的时候你可能任性的存,在取值的时候就找不到北了!

    如果一个对象在被作为字典的键后,它的哈希码改变了,那么在字典中,将永远找不到这个值了,为了解决这个问题,所以可以基于不变的字段来进行哈希计算。

    现在,我们再去掉注释看看:

    image-20211005145005591

    正常工作了!

    而对GetHashCode重载的要求如下:

    • 对于Equals返回为true的两个值,GetHashCode也必须返回一样的值。

    • 不能抛出异常

    • 对于同一个对象,必须返回同一个值,除非对象改变

      对于class的GetHashCode默认返回的是internal object token,这对于每个实例来讲都是唯一的。


      假如因为某些原因要实现自己的哈希表集合,或者要在实现的代码中调用GetHashCode,记住千万不能对哈希码进行持久化,因为它很容易改变,一个类型的未来版本可能使用不同的算法计算哈希码。

      有公司不注意,在他们的网站上,用户选择用户名和密码进行注册,然后网站获取密码String,调用GetHashCode,将哈希码持久性存储到数据库,用户重新登陆网站,输入密码,网站再次调用GetHashCode,将哈希码与数据库中存储值对比,匹配就允许访问,不幸的是,升级到新CLR后,String的GetHashCode算法发生改变,结果就是所有用户无法登录


      重载Equals

      自己定义的重载Equals必须具备如下特征:

      (1)自反性,即x.Equals(x)是true

      (2)对称性,即x.Equals(y)y.Equals(x)返回值相同

      (3)可传递性,即x.Equals(y)返回true,y.Equals(z)也返回true,那么x.Equals(z)肯定也应该是true

      (4)可靠性,不抛出异常

      满足这几点,应用程序才会正常工作。

    ##### 愿你一寸一寸地攻城略地,一点一点地焕然一新 #####
  • 相关阅读:
    HDU 1800 Flying to the Mars 字典树,STL中的map ,哈希树
    字典树 HDU 1075 What Are You Talking About
    字典树 HDU 1251 统计难题
    最小生成树prim算法 POJ2031
    POJ 1287 Networking 最小生成树
    次小生成树 POJ 2728
    最短路N题Tram SPFA
    poj2236 并查集
    POJ 1611并查集
    Number Sequence
  • 原文地址:https://www.cnblogs.com/johnyang/p/15368737.html
Copyright © 2011-2022 走看看