• 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)可靠性,不抛出异常

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

    ##### 愿你一寸一寸地攻城略地,一点一点地焕然一新 #####
  • 相关阅读:
    SPSS分类分析:决策树
    单例设计模式
    设计模式
    java.lang.NoClassDefFoundError: org/apache/zookeeper/proto/SetWatches
    AngularJs中Uncaught Error: [$injector:modulerr] http://errors.angularjs.org/1.3.15/
    如何在linux环境下配置环境变量
    MQ的面试题
    VUE框架
    SQL中and和or的区别是?
    Git的工作流程
  • 原文地址:https://www.cnblogs.com/johnyang/p/15368737.html
Copyright © 2020-2023  润新知