.Net 相等性:集合类 Contains 方法详解

来源:互联网 发布:淘宝卖家处理退货申请 编辑:程序博客网 时间:2024/05/16 20:53

.Net 相等性:集合类 Contains 方法详解(一)

 这些方法归根结底都可追溯到以下三个接口上(不考虑非泛型版的):

 一般集合类的Contains都源自ICollection<T>,字典类的ContainsKey都源自IDictionary<TKey, TValue>。另外System.Linq.Enumerable类(.Net3.0)扩展了IEnumerable<T>接口:

 Contains或ContainsKey要将输入值与集合中原有的值进行相等比较,Contains涉及到.Net中的相等性。.Net表示相等有多种方法,先看Object类:

 

 这其中有四个相等的方法:

1     public virtual bool Equals(object obj)
2     public static bool Equals(object objA, object objB)
3     public static bool ReferenceEquals(object objA, object objB)
4     public static bool operator == (object objA, object objB)

 第四个是==的运算符重载,系统默认实现。这四个相等性在值类型和引用类型含义不同,要把这四个相等性的问题说明清楚也不是件容易事,大家可以去看下《Effective c#》一书,其中有对此的详细阐述,我就不要详细重复了,简单说一下在引用类型中的含义吧:

 1.在引用类型中,ReferenceEquals与==含义相同,都表示引用相等(ReferenceEqual)。

 2.Equals(object objA, object objB)内部最终调用Equals(object obj)方法。

 3.引用类型不要去重载==运算符,这样会破坏它本来的含义。 

 总结起来,对引用类型可简化为两个方法,就上面的方法1和方法3,方法3不用操心,它只表示引用相等,不能修改。所以我们只关心方法1,它被标记为virtual,我们可以对它进行重写(override)。

 如果定义一个新的类(没有从其它类继承),没有重写Equals(object obj),它将采用一个默认实现,先看该类:

1     class People
2     {
3         public int Id { get; set; }
4         public string Name { get; set; }
5     }

 我们写段代码来测试下Equals的默认实现是什么?

1     People p1 = new People { Id = 1, Name = "harry" };
2     People p2 = new People { Id = 1, Name = "harry" };

4     bool b1 = p1 == p1;
5     bool b2 = p1.Equals(p1);
6     bool b3 = p1 == p2;
7     bool b4 = p1.Equals(p2);

 我们实例化了两个People,具有相同的属性。b1、b2肯定为true,自己和自己比较嘛!再来看b3,这里使用“==”进行比较,前面我们说过“==”是“引用相等”,p1、p2是两个实例,具有不同的引用,所以b3值是false。最后看b4,b4使用了Equals(object obj),也就是前面说的方法一,People类没有重写这个方法,于是就使用了Object类中的默认实现。这个默认实现就是引用相等,即ReferenceEqual。所以b4也是false。

 

 这个默认实现与我们的实际应用含义不相同,两个实例属性全部相同,为什么还不Equal呢。因此对于引用类型,我们应当重写其Equals方法,让它更具有实际意义。下面是一个参考实现(改编自《Effective c#》):

 1      public override bool Equals(object obj)
 2      {
 3          if (obj == null) return false;
 4          if (object.ReferenceEquals(this, obj)) return true;
 5          //
 6          if (this.GetType() != obj.GetType()) return false;
 7          //
 8          return CompareMembers(obj as People);
 9      }
10 
11      private bool CompareMembers(People other)
12      {
13          return Id.Equals(other.Id) && Name.Equals(other.Name);
14      }

 注意第六行,我们判断两个类的类型是否相同,类型不同我们认定“不相等”。(People类以后可能会有派生类,派生类即使所有属性与父类相同,也认为是不相等,因为类型不同。)

 重写Equals后,再来测下上面的b4吧,这次为true了。重写后Equals更具有实际意义,如果非要比较引用相等,用“==”比较即可。

 再来看一些与相等性有关的接口:

 

 前两个比较相同,后两个不但可以比较相等还可比较谁大谁小(用于集合排序)。这次只讨论前两个。两个接口的声明如下:

1     public interface IEquatable<T>
2     {
3         bool Equals(T other);
4     }
5     public interface IEqualityComparer<T>
6     {
7         bool Equals(T x, T y);
8         int GetHashCode(T obj);
9     }

 IEquatable<T>接口比较简单只有一个方法Equals,我们先给People类实现了,如下: 

Code
 1     class People  : IEquatable<People>
 2     {
 3         public int Id { get; set; }
 4         public string Name { get; set; }
 5 
 6 
 7         public override bool Equals(object obj)
 8         {
 9             if (obj == null) return false;
10             if (object.ReferenceEquals(this, obj)) return true;
11             if (this.GetType() != obj.GetType()) return false;
12             return Equals(obj as People);
13         }
14 
15         public bool Equals(People other)
16         {
17             if (other == null) return false;
18             if(this == other) return true;
19             return Id.Equals(other.Id) && Name.Equals(other.Name);
20         }
21     }

 把刚才的CompareMembers方法改成了Equals。而且是从私有方法变成了公有方法,所以又加上了两行代码(注意还没有对this.Name进行空值判断)。这样一来,前面测试中的计算b4值时调用的不再是Equals(object obj)了,而是调用了Equals(People other),效率会提高一些。

 接下来看第二个接口 IEqualityComparer<T>,这个接口用在何处呢?请看下图:

 

 如上这个方法是System.Linq.Enumerabler的一个扩展方法,可以传入一个IComparer<T>作为参数。这个重载 我们直接使用的比较少,大多数情况下我们使用是Collection的Contains<T>(T item)(这个方法扩展后面还会提到)。但IEqualityComparer<T>这个接口很重要,也本文的重点。

 现在有一个问题,泛型集合类的Contains方法是调用的两个Equals之中的哪个呢(如People类中,两个Equals分别在7行、15行),又与这些接口什么关系呢?

 我们先看使用最频繁的泛型集合类List<T>,来看它的Contains实现:

 1     public bool Contains(T item)
 2     {
 3         if (item == null)
 4         {
 5             for (int j = 0; j < this._size; j++)
 6                 if (this._items[j] == null) return true;
 7             return false;
 8         }
 9         EqualityComparer<T> comparer = EqualityComparer<T>.Default;
10         for (int i = 0; i < this._size; i++)
11             if (comparer.Equals(this._items[i], item))return true;
12         return false;
13     }

 3~8行,如果传入是item是null,也进行了处理,遍历内部集合_items(其实是个数组,定义为T[] _items),看是否也有空值。

 重点在第9行,comparer = EqualityComparer<TSource>.Default(这句代码后面会多次出现)。这里出现了一个EqualityComparer<T>类,和我们前面提到的接口IEqualityComparer<T>很像的,它们是什么关系呢。我把和它和它的派生类都找了出来,连根拔起,如下: 

原创粉丝点击