C#相等性比较

本文阐述C#中相等性比较,其中主要集中在下面两个方面算法

==和!=运算符,何时它们能够用于相等性比较,何时它们不适用,若是不使用,那么它们的替代方式是什么?shell

何时,须要自定一个类型的相等性比较逻辑安全

在阐述相等性比较,以及如何自定义相等性比较逻辑以前,咱们首先了解一下值类型比较和引用类型比较app

值类型比较对比引用类型比较

C#中的相等性比较有两种:ide

  • 值类型相等,两个值在某种场景下相等
  • 引用类型相等,两个引用指向同一个对象

默认状况下,函数

  • 值类型使用值类型相等
  • 引用类型使用引用相等

实际上,值类型只能使用值相等(除非值类型进行了装箱操做)。看一个简单的例子(比较两个数字),运行的结果为True性能

int x = 5, y = 5;
Console.WriteLine(x == y);

默认地,引用类型使用引用相等。好比下面的例子:返回Falseui

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

若是x和y指向同一个对象,那么将返回True:this

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

相等性的标准

下面三个标准用于实现相等性比较:spa

  • ==和!=运算符
  • object中的虚方法Equals
  • IEquatable<T>接口

下面咱们来分别阐述

1. ==和!=运算符

使用==和!=的缘由是它们是运算符,它们经过静态函数实现相等性比较。所以,当你使用==或!=时,C#在编译时就决定了所比较的类型,并且不会执行任何虚方法(Object.Equals)。这是你们所指望的相等行比较。好比在第一个数字比较的例子中,编译器在编译时就决定了执行==运算的类型是int类型,由于x和y都是int类型。

而第二个例子,编译器决定执行==运算的类型是object类型,由于object是类(引用类型),所以对象的==运算符采起引用相等去比较x和y。那么结果就返回False,这是由于x和y指向堆上不一样的对象(被装箱的int)

 

2. Object.Equals虚方法

为了正确地比较第二个例子中的x和y,咱们可使用Equals虚方法。System.Object中定义了Equals虚方法,它适用于全部类型

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

Equals在程序运行时决定比较的类型--根据对象的实际类型进行比较。在上面的例子中,会调用Int32的Euqals方法,该方法使用值相等进行比较,因此上面的例子返回True。若是x和y是引用类型,那么调用引用相等进行比较;若是x和y是结构类型,那么Equals会调用结构每一个成员对应类型的Equals方法进行比较。

看到这里,你可能会想,为何C#的设计者不把==设计成virtaul,从而使其与Equals同样,以免上诉缺陷。这是由于:

  • 若是第一个运算对象是null,Equals方法会抛出NullReferenceException异常;而静态的运算符则不会
  • 由于==运算符在编译时决定了比较类型(静态解析比较类型),那么它的执行就很是快。这也就使得编写大量运算代码去执行相等性比较时对性能不会带来太大的影响
  • 有时候,==和Equals适用于不一样的场景的相等性比较。(后续的内容会涉及)

简而言之,复杂的设计反映了复杂的场景:相等的概念涉及到许多场景。

 

而Euqals方法,适用于比较两个未知类型的对象,下面的这个方法就适用于比较任何类型的两个对象:

public static bool AreEqual(object obj1, object obj2)
{
    return obj1.Equals(obj2);
}

可是,该函数不能处理第一个参数是null的情形,若是第一个函数是null,你会获得NullReferenceException异常。所以咱们须要对该函数进行修改:

public static bool AreEqual(object obj1, object obj2)
{
    if (obj1 == null)
        return obj2 == null;
    return obj1.Equals(obj2);
}

 

object的静态Equals方法

object类还定义了一个静态Equals方法,它的做用与AreEquals方法同样。

public static bool Equals(Object objA, Object objB)
{
    if (objA==objB) {
        return true;
    }
    if (objA==null || objB==null) {
        return false;
    }
    return objA.Equals(objB);
}

这样就能够对编译时不知道类型的null对象进行安全地比较。

object x = 5, y = 5;
Console.WriteLine(object.Equals(x, y)); // -> True
x = null;
Console.WriteLine(object.Equals(x, y)); // -> False
y = null;
Console.WriteLine(object.Equals(x, y)); // -> True

Console.WriteLine(x.Equals(y)); // -> NullReferebceException, because x is null

请注意,当编写Generic类型时,下面的代码将不能经过编译(除非把==或!=运算符替换成Object.Equals方法的调用):

public class Test<T> : IEqualityComparer<T>
{
    T _value;

    public void SetValue(T newValue)
    {
        // Operator '!=' cannot be applied to operands of type 'T' and 'T'
        // it should be : if(!object.Equals(newValue, _value))
        if (newValue != _value)
            _value = newValue;
    }
}

object的静态ReferenceEquals方法

有时候,你须要强行比较两个引用是否相等。这个时候,你就须要使用object.ReferenceEquals:

internal class Widget 
{
    public string UID { get; set; }
    public override bool Equals(object obj)
    {
        if (obj == null)
            return this == null;

        if (!(obj is Widget))
            return false;
        Widget w = obj as Widget;
        return this.UID == w.UID;
    }

    public override int GetHashCode()
            {
                return this.UID.GetHashCode();
            }

    public static bool operator  == (Widget w1, Widget w2)
            {
                return w1.Equals(w2);
            }

    public static bool operator !=(Widget w1, Widget w2)
            {
                return !w1.Equals(w2);
            }
}

static void Main(string[] args)
{

    Widget w1 = new Widget();
    Widget w2 = new Widget();
    Console.WriteLine(w1==w2); // -> True
    Console.WriteLine(w1.Equals(w2)); // -> True
    Console.WriteLine(object.ReferenceEquals(w1, w2)); // -> False 

    Console.ReadLine();
}
Basic ReferenceEquals

 

之因此调用ReferenceEquals方法,这是由于自定义类Widget重写了object类的虚方法Equals;此外,该类还重写了操做符==和!=,所以执行==时操做也返回True。因此,调用ReferenceEquals能够确保返回引用是否相等。

3. IEquatable<T>接口

调用object.Equals方法实际上对进行比较的值类型进行了装箱操做。在对性能有较高要求的场景,那么就不适合使用这种方式。从C#2.0开始,经过引入IEquatable<T>接口来解决这个问题

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

当实现IEquatable接口之口,调用接口方法就等同于调用objet的虚方法Equals,可是接口方法执行地更快(不须要类型转换)。大多数.NET基本类型都实现了IEquatable<T>接口,你还能够为Generic类型添加IEquatable<T>限制

internal class Test<T> where T : IEquatable<T>
{
    public bool IsEqual(T t1, T t2)
    {
        return t1.Equals(t2);
    }
}

若是,咱们移除IEquatable<T>限制,Test<T>类仍能够经过编译,可是t1.Equals(t2)将使用object.Equals方法。

4. 当Equals结果与==的结果不一致

在前面的内容中,咱们已经提到有时候,==或equals适用于不一样的场景。好比:

double x = double.NaN;
Console.WriteLine(x == x); // False
Console.WriteLine(x.Equals(x)); // True

这是由于double类型的==运算符强制NaN不等于其余任何值,即便另一个NaN。从数学的角度来说,两个确实不相等。而Equals方法,由于具备对称性,因此x.Equals(x)总返回True。

集合与字典正是依赖于Equals的对称性,不然就不能找到已经保存在集合或字典中的元素。

对于值类型而言,Equals和==不多出现不一样的相等性。而在引用类型中,则比较常见。通常地,引用类型的建立者重写Equals方法执行值相等比较,而保留==执行引用相等比较。好比StringBuilder类就是这样的:

StringBuilder buffer1 = new StringBuilder("123");
StringBuilder buffer2 = new StringBuilder("123");
Console.WriteLine(buffer1 == buffer2); // False
Console.WriteLine(buffer1.Equals(buffer2)); // True

 

比较自定义类型

回顾一下默认的比较行为

  • 值类型使用值相等
  • 引用类型使用引用相等

进一步,

  • 结构类型的equals方法会根据每一个字段的类型进行相等行比较

有时候,在建立类型时,须要重写上述行为,通常在下面两种情形下须要重写:

  • 更改相等的意义
  • 提升结构类型的比较速度

 

1)更改相等的意义

当默认的==和Equals不适用(不符合天然规则,或悖离了使用者的指望)于自定义类型时,就须要更改相等的意义。好比DateTimeOffset结构,其有两个私有成员:一个DateTime类型的UTC,以及int类型的offset。若是是你在建立DateTimeOffset类型,那么你极可能只要UTC字段相等便可,而不去比较Offset字段。另一个例子就是支持NaN的数字类型,好比float和double,若是你来建立这两个类型,你可能会但愿NaN也是能够进行比较的。

而对于Class类型,不少时候,使用值比较更有意义。尤为是一些包含较少数据的类,好比System.Uri或System.String

2)提升结构类型的比较速度

结构类型的默认比较算法相对较慢。经过重写Equals方法能够提升5%的性能。而重载==运算和实现IEquatable<T>能够在不装箱操做的状况下实现相等性比较,这使得提升5%性能变得可能。

对于自定义相等比较,有一个特殊的情形,更改结构类型的hashing算法后,hashtable能够得到更好的性能。这是由于hashing算法和相等性比较都发生在栈上。

3)如何重写相等

总地来讲,有下面三种方式:

  • 重写GetHashcode()和Equals()
  • 【可选】重载!=和==
  • 【可选】实现IEquatable<T>

I)重写GetHashCode

object对象的虚方法GetHashCode,也就仅仅对于Hashtable类型和Dictionary<TKey,TValue>类型有益。

这两个类型都是哈希表集合,集合中的每一个元素都是一个键值用于存储元素和获取元素。哈希表使用了一个特定的策略以有效地基于元素的键值分配元素。这就要求每一个键值都有一个Int32数(或哈希码)。哈希码不只对于每一个键值是惟一的,并且还必须有较好的性能。哈希表认为object类定义的GetHashCode方法已经足够了,所以这两个类型都省略了获取哈希码的方法。

不管值类型仍是引用类型,都默认实现了GetHashCode方法,因此你不用重写这个方法,除非你须要重写Equals方法。(所以,若是你重写了GetHashCode方法,那么你确定是须要重写Equals方法)。

是否须要重写GetHashCode方法,能够参考下面的规则:

  • 若是Equals方法返回True是,两个比较的对象必须返回相同的哈希码
  • 不容许抛出异常
  • 除非对象变化了,那么重复的对一个对象调用GetHashCode方法应返回相同的哈希码

为了提升哈希表的性能,GetHashCode须要重写以防止不一样的值返回相同的哈希码。也这就说明了为何须要对结构类型须要重写Equals和GetHashCode方法,所以这样重写比默认的哈希算法更有效率。结构类型的GetHashCode方法的默认实现是在运行时才发生,并且极可能基于结构的每一个成员而实现。

// char type
public override int GetHashCode() {
    return (int)m_value | ((int)m_value << 16);
}

// int32
public override int GetHashCode() {
    return m_value;
}

而类(class)类型,GetHashCode方法的默认实现基于内部对象标识,这个标识在CLR中对于每一个对象实例都是惟一的。

public virtual int GetHashCode()
{
    return RuntimeHelpers.GetHashCode(this);
}

II)重写Equals

object.Equal的规定(公理)以下:

  • 一个对象不能和null相等(除非对象是nullable类型)
  • 相等性是对称的(一个对象等于自身)
  • 相等性是可交换的(若是a等于b,那么b也等于a)
  • 相等性是可传递的(若是a等于b,b等于c,那么a等于c)
  • 相等性是可重复的,而且是可靠的(不会抛出异常)

III)重载==和!=

除了可重写Equals,还能够重载等于和不等于运算符。

对于结构类型,基本都重载了等于和不等于运算符,若是不重载它们,那么对于结构类型,等于和不等于将返回错误的结果;

而对于类(class)类型,有两种处理方式:

  • 不重载==和!=,由于它们会执行引用相等
  • 重载==和!=,使其与Equals一致

第一种实现适用于大多数自定义类型,特别是可变(mutable)类型。它确保了自定义类型符合==和!=就应该执行引用相等性的比较,从而不会误导这些自定义的使用者。再次回顾一下前面举过的StringBuilder例子

StringBuilder buffer1 = new StringBuilder("123");
StringBuilder buffer2 = new StringBuilder("123");
Console.WriteLine(buffer1 == buffer2); // False, Reference equality
Console.WriteLine(buffer1.Equals(buffer2)); // True, Value equality

而第二种实现适用于使用者永远都不但愿自定义类型执行引用相等。通常地这些都类型都是不可变(immutable)类型,好比string类型和System.Uri类型,固然也包含一些引用类型。

III)实现IEquatable<T>

为了保持完整性,建议在重写Equals方法时,同时实现IEquatable<T>接口。接口方法的结果应当与自定义重写后Equals方法的结果一致。若是你已经重写了Equals方法,那么实现IEquatable<T>不须要额外的实现代码(直接调用Equlas方法便可)

internal class Staff : IEquatable<Staff>
{
    public string FirstName { get; set; }

    // implements IEquatable<Staff>
    public bool Equals(Staff other)
    {
        return this.FirstName.Equals(other.FirstName);
    }

    // override Equals
    public override bool Equals(object obj)
    {
        if (obj == null)
            return this == null;

        if (!(obj is Staff))
            return false;

        Staff s = obj as Staff;

        return this.FirstName == s.FirstName;
    }

    // override GetHashCode
    public override int GetHashCode()
    {
        return this.FirstName.GetHashCode();
    }            
}

IV)可插入的相等比较器

若是你但愿一个类型在某一个特定的场景下使用不一样的比较,那么你可使用可插件式的IEqualityComparer。它特别适用于集合类。(后续有内容介绍)

 

相等性比较总结

C#类库中,为相等性比较设计了三个接口:IEquatable<T>,IEqualityComparer,以及IEqualityComparer<T>。

IEqualityComparer与IEqualityComparer<T>的差异很简单,一个是非Generic的,须要把T转换成Object,而后调用Object的Equals方法;然后者直接调用T类型实例的Equals方法。

那么IEquatable<T>和IEqualityComparer<T>有什么差异,分别适用于什么场景呢?

1. IEquatable<T>用于比较与本身类型相同的另外一个对象是否相等;而IEqualityComparer<T>则用于比较两个相同类型的实例是否相等。

2. 若是两个实例是否相等只有一种可能,或者有几个是否相等的比较但只有其中一个更有意义,那么应该选择IEquatable<T>,T类型本身实现IEquatable<T>接口。所以IEquatable<T>的实例本身就知道该如何比较本身和另一个实例。与之相反,若是须要比较的实例之间存在多个相等性比较,那么IEqualityComparer<T>更适合这种状况;这个接口不会由T类型实现,相反须要一个外部的类实现IEqualityComparer<T>接口。由于,当比较两个类型实例是否相等时,由于T类型内部不知道如何比较,那么你就须要显示地指定一个IEqualityComparer<T>实例用于执行相等性比较从而知足特定的需求。

3. 例子

internal class Staff : IEquatable<Staff>
        {
            public string FirstName { get; set; }
            public string Title { get; set; }
            public string Dept { get; set; }

            public override string ToString()
            {
                return string.Format(
                    "FirstName:{0}, Title:{1}, Dept:{2}",
                    FirstName, Title, Dept);
            }

            // implements IEquatable<Staff>
            public bool Equals(Staff other)
            {
                return this.FirstName.Equals(other.FirstName);
            }


            // override Object.GetHashCode
            public override int GetHashCode()
            {
                return this.FirstName.GetHashCode();
            }

        }

internal class StaffTitleComparer : IEqualityComparer<Staff>
        {
            public bool Equals(Staff x, Staff y)
            {
                return x.Title == y.Title;
            }

            public int GetHashCode(Staff obj)
            {
                return obj.Title.GetHashCode();
            }

        }

internal class StaffDeptComparer : IEqualityComparer<Staff>
{
    public bool Equals(Staff x, Staff y)
            {
                return x.Dept == y.Dept;
            }

    public int GetHashCode(Staff obj)
    {
        return obj.Dept.GetHashCode();
    }

}


static void Main(string[] args)
        {

            IList<Staff> staffs = new List<Staff> 
            {
                new Staff{FirstName="AAA", Title="Manager", Dept="Sale"},  
                new Staff{FirstName="BBB", Title="Accountant", Dept="Finance"},
                new Staff{FirstName="BBB", Title="Accountant", Dept="Finance"},
                new Staff{FirstName="AAA", Title="Sales", Dept="Sale"},
                new Staff{FirstName="ABA", Title="Manager", Dept="HR"}
            };
            Print("All Staffs", staffs);
            Print("No duplicated first name", staffs.Distinct());
            Print("No duplicated title", staffs.Distinct(new StaffTitleComparer()));
            Print("No duplicated department", staffs.Distinct(new StaffDeptComparer()));

            Console.ReadLine();
        }

private static void Print(string group, IEnumerable<Staff> staffs)
        {
            Console.WriteLine(group);
            foreach (Staff s in staffs)
                Console.WriteLine(s.ToString());

            Console.WriteLine();
        }
Overall

 

--update--

最后一个例子,还能够经过扩展IEnumeable<T>来实现DistinctBy:

public static class IEnurambleExtension
{
    public static IEnumerable<TSource> DistinctBy<TSource, TKey>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    { 
        HashSet<TKey> keys = new HashSet<TKey>();

        foreach (TSource element in source)
            if (keys.Add(keySelector(element))) 
                yield return element;
    }
}

能够这样使用

staffs.DistinctBy(s => s),注意,staff类须要实现IEquatable<T>(或重写Equals和GetHashCode)

staffs.DistinctBy(s => s.Dept),这就省去了编写StaffDeptComparer类

进一步,若是staff的某个字段是一个类,那么这个类一样须要实现IEquatable<T>(或重写Equals和GetHashCode)

 参考资料

1. C# 5.0 in a Nutshell;

2. MSDN, IEquatable<T>, http://msdn.microsoft.com/en-us/library/ms131187.aspx;

3. MSDN IEqualityComparer, http://msdn.microsoft.com/en-us/library/ms132151.aspx;

4. Stackoverflow, http://stackoverflow.com/questions/9316918/what-is-the-difference-between-iequalitycomparert-and-iequatablet

相关文章
相关标签/搜索