代码改变世界

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

2009-09-05 17:39 鹤冲天 阅读(...) 评论(...) 编辑 收藏
 .Net中具有Contains方法(或ContainsXXXX方法)的类很多,大多为集合类,请看下图:

 

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

 一般集合类的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 { getset; }
4         public string Name { getset; }
5     }

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

1     People p1 = new People { Id = 1, Name = "鹤冲天" };
2     People p2 = new People { Id = 1, Name = "鹤冲天" };
3 
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 == nullreturn 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

 把刚才的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] == nullreturn 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>很像的,它们是什么关系呢。我把和它和它的派生类都找了出来,连根拔起,如下: 

 

 EqualityComparer<T>是个抽象类,真正发挥作用是它的派生类。EqualityComparer<T>有个属性Defalut,实现如下:

 1     public static EqualityComparer<T> Default
 2     {
 3         get
 4         {
 5             EqualityComparer<T> defaultComparer = EqualityComparer<T>.defaultComparer;
 6             if (defaultComparer == null)
 7             {
 8                 defaultComparer = EqualityComparer<T>.CreateComparer();
 9                 EqualityComparer<T>.defaultComparer = defaultComparer;
10             }
11             return defaultComparer;
12         }
13     }

 这种写法经常见,我们顺藤摸瓜找下去,来看CreateComparer方法,这是个工厂方法:

 1     private static EqualityComparer<T> CreateComparer()
 2     {
 3         Type c = typeof(T);
 4         if (c == typeof(byte))
 5         {
 6             return (EqualityComparer<T>)new ByteEqualityComparer();
 7         }
 8         if (typeof(IEquatable<T>).IsAssignableFrom(c))
 9         {
10             return (EqualityComparer<T>)typeof(GenericEqualityComparer<int>)
11                 .TypeHandle.CreateInstanceForAnotherGenericParameter(c);
12         }
13         if (c.IsGenericType && (c.GetGenericTypeDefinition() == typeof(Nullable<>)))
14         {
15             Type type2 = c.GetGenericArguments()[0];
16             if (typeof(IEquatable<>).MakeGenericType(new Type[] { type2 }).IsAssignableFrom(type2))
17             {
18                 return (EqualityComparer<T>)typeof(NullableEqualityComparer<int>)
19                     .TypeHandle.CreateInstanceForAnotherGenericParameter(type2);
20             }
21         }
22         return new ObjectEqualityComparer<T>();
23     }

 先整体上对代码说一下:

 行4~7是对byte类型进行的处理,ByteEqualityComparer实现很简单,两个byte一比较就是了。

 行8~12是对实现了IEquatable<T>接口的类型进行处理,行8要好好理解IsAssignableFrom,意思就是:类型T实现了IEquatable<T>接口。

 行13~21是对可空类型进行处理,先将类型从Nullable<>中剥离出来,再来看它有没有实现IEquatable接口。

 行22,如果类型(或包在Nullable<>中的类型)没有实现IEquatable<T>接口,就返回ObjectEqualityComparer<T>的一个实例。

 Type.TypeHandle类型是 RuntimeTypeHandle 结构,CreateInstanceForAnotherGenericParameter是RuntimeTypeHandle 的内部方法,可以理解为创建一个泛型类的实现,这个泛型类的参数就是输入的参数。这点了解一下就可以了。

 总结这段CreateComparer()方法,它会根据要比较的值的性质(是否是值类型byte,有没有实现IEquatable<T>接口,是否为可空类型)生成四种IEqualityComparer<T>:

 1.byte类型,返回一个ByteEquityComparer;

 2.实现IEquatable<T>接口的类型,返回一个GenericEqualityComparer<T>;

 3.可空类型,如果内部类型V实现了IEquatable<V>接口,返回一个NullableEqualityComparer<V>;

 4.其它类型统统返回 ObjectequalityComparer<T>。 
 

 这四种类型请参见前面贴出的类图,下面是四个 IEqualityComparer<T>.Equal(T x, T y)的具体实现:

 1     //ByteEqualityComparer
 2     public override bool Equals(byte x, byte y)
 3     {
 4         return (x == y);
 5     }
 6     //GenericEqualityComparer<T>
 7     public override bool Equals(T x, T y)
 8     {
 9         if (x != null)
10         {
11             return ((y != null&& x.Equals(y));
12         }
13         if (y != null)
14         {
15             return false;
16         }
17         return true;
18     }
19     //NullableEqualityComparer<T>
20     public override bool Equals(T? x, T? y)
21     {
22         if (x.HasValue)
23         {
24             return (y.HasValue && x.value.Equals(y.value));
25         }
26         if (y.HasValue)
27         {
28             return false;
29         }
30         return true;
31     }
32     //ObjectEqualityComparer<T>
33     public override bool Equals(T x, T y)
34     {
35         if (x != null)
36         {
37             return ((y != null&& x.Equals(y));
38         }
39         if (y != null)
40         {
41             return false;
42         }
43         return true;
44     }

 ByteEqualityComparer的实现不用多说。

 GenericEqualityComparer<T>、 NullableEqualityComparer<T>的实现中的Equals是IEquatable<T>.Equals<T>(T obj)。

 ObjectEqualityComparer<T>实现中调用Equals的是Object.Equals(object obj)。

 晕没有,我都有点了。先想清楚再向下看。 

 

 刚才说了这么多,都是List<T>的,我们再来看Collection<T>的Contains:

1     public bool Contains(T item)
2     {
3         return this.items.Contains(item);
4     }

 还要顺藤摸瓜找下去,不过这次简单多了。items属性的类型是IList<T>,如下:

 1     public class Collection<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
 2     {
 3         private IList<T> items;
 4 
 5         public Collection()
 6         {
 7             this.items = new List<T>();
 8         }
 9         public Collection(IList<T> list)
10         {
11             if (list == null)
12                 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.list);
13             this.items = list;
14         }
15         
16     }

 还好,Collection默认构造函数采用的是List<T>,不用分析了。

 

 接下来我们看 System.Linq.Enumerable,它有两个Contains,都是扩展方法:

1     public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value);
2     public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value,
3         IEqualityComparer<TSource> comparer);

 我们看第一个的实现: 

1     public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value)
2     {
3         ICollection<TSource> is2 = source as ICollection<TSource>;
4         if (is2 != null)
5             return is2.Contains(value);
6         return source.Contains<TSource>(value, null);
7     }

 行4,如果是ICollection<T>,调用ICollection<T>的Contains。如果是List<T>或Collection<T>则调用它们相应的Contains与前面一致,不用分析了。 

 否则,还得顺藤摸瓜(有点烦了吧),会调用第二个Contains扩展,实现如下:

 1     public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value,
 2         IEqualityComparer<TSource> comparer)
 3     {
 4         if (comparer == null)
 5         {
 6             comparer = EqualityComparer<TSource>.Default;
 7         }
 8         if (source == null)
 9         {
10             throw Error.ArgumentNull("source");
11         }
12         foreach (TSource local in source)
13         {
14             if (comparer.Equals(local, value))
15             {
16                 return true;
17             }
18         }
19         return false;
20     }

 第7行,comparer = EqualityComparer<TSource>.Default,熟悉吧,前面刚分析过,回头找吧!

 小结:List<T>、Collection<T>、Enumerable.Contains<T>,归根结底内部实现是一致的。

 

 还剩下最下一个Dictionary<T,K>.ContainsKey(K key):  

 1     public class Dictionary<TKey, TValue> : IDictionary<TKey, TValue>,
 2         ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>,
 3         IDictionary, ICollection, IEnumerable, ISerializable, IDeserializationCallback
 4     {
 5         private IEqualityComparer<TKey> comparer;
 6 
 7         public Dictionary() : this(0null) { }
 8         public Dictionary(IDictionary<TKey, TValue> dictionary) : this(dictionary, null) { }
 9         public Dictionary(IEqualityComparer<TKey> comparer) : this(0, comparer) { }
10         public Dictionary(int capacity) : this(capacity, null) { }
11         public Dictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
12             : this((dictionary != null? dictionary.Count : 0, comparer)
13         {
14             if (dictionary == null)
15                 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary);
16             foreach (KeyValuePair<TKey, TValue> pair in dictionary)
17                 this.Add(pair.Key, pair.Value);
18         }
19         public Dictionary(int capacity, IEqualityComparer<TKey> comparer)
20         {
21             if (capacity < 0)
22                 ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
23             if (capacity > 0)
24                 this.Initialize(capacity);
25             if (comparer == null)
26                 comparer = EqualityComparer<TKey>.Default;
27             this.comparer = comparer;
28         }
29         public bool ContainsKey(TKey key)
30         {
31             return (this.FindEntry(key) >= 0);
32         }
33          private int FindEntry(TKey key)
34         {
35             if (key == null)
36                 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
37             if (this.buckets != null)
38             {
39                 int num = this.comparer.GetHashCode(key) & 0x7fffffff;
40                 for (int i = this.buckets[num % this.buckets.Length]; i >= 0; i = this.entries[i].next)
41                     if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
42                         return i;
43             }
44             return -1;
45         }
46     }

 Containskey调用FindEntry,FindEntry中(行39)使用了字段comparer(行5中定义),再来找何处给comparer赋的值。看行11构造函数,可以通过参数传入一个。不传或传空值时怎么处理?行19的构造函数中进行了处理,代码在25~26行,这comparer = EqualityComparer<TKey>.Default,这次忘不不了吧! 

 Dictionary<T,K>.ContainsKey也和前的处理一样。 

 

 好了,费了这么大工夫,把.Net掘地三尺,总算弄明白了Contains、ContainsKey是怎么实现的,是调用的IEquatable<T>.Equals(T obj),还是Object.Equals(object obj)。在分析的过程中我们也看得出.Net的源码是多么的严谨,真要仔细学习一番。

 说明一下,EqualityComparer<T>抽象类是公有(Public),但前面提到的它的四个派生类都是 internal,我们是没法直接使用的。但我们可以使用EqualityComparer<T>.Default进行相等性判断,它可是考虑了各种情况(是否实现了IEquatable<T>,是否为可空类型等等)。

 最后建议大家,创建类的时候一定要实现IEquatable<T>接口,并重写Object.Equals(object obj)方法,以免引起不必要的麻烦。

 

 

 还记得昨天我给出的《.Net 相等性的测试题目,看你基础牢不牢》吧!看完这篇文章,再做起来就比较自信了,答案就不必给出了,调试运行下就出来了。