.Net 相等性:集合类 Contains 方法 深入详解
2009-09-05 17:39 鹤冲天 阅读(12091) 评论(13) 编辑 收藏 举报
这些方法归根结底都可追溯到以下三个接口上(不考虑非泛型版的):
一般集合类的Contains都源自ICollection<T>,字典类的ContainsKey都源自IDictionary<TKey, TValue>。另外System.Linq.Enumerable类(.Net3.0)扩展了IEnumerable<T>接口:
Contains或ContainsKey要将输入值与集合中原有的值进行相等比较,Contains涉及到.Net中的相等性。.Net表示相等有多种方法,先看Object类:
这其中有四个相等的方法:
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),它将采用一个默认实现,先看该类:
2 {
3 public int Id { get; set; }
4 public string Name { get; set; }
5 }
我们写段代码来测试下Equals的默认实现是什么?
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#》):
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更具有实际意义,如果非要比较引用相等,用“==”比较即可。
再来看一些与相等性有关的接口:
前两个比较相同,后两个不但可以比较相等还可比较谁大谁小(用于集合排序)。这次只讨论前两个。两个接口的声明如下:
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类实现了,如下:
把刚才的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实现:
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>很像的,它们是什么关系呢。我把和它和它的派生类都找了出来,连根拔起,如下:
EqualityComparer<T>是个抽象类,真正发挥作用是它的派生类。EqualityComparer<T>有个属性Defalut,实现如下:
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方法,这是个工厂方法:
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)的具体实现:
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:
2 {
3 return this.items.Contains(item);
4 }
还要顺藤摸瓜找下去,不过这次简单多了。items属性的类型是IList<T>,如下:
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,都是扩展方法:
2 public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value,
3 IEqualityComparer<TSource> comparer);
我们看第一个的实现:
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扩展,实现如下:
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):
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(0, null) { }
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 相等性的测试题目,看你基础牢不牢》吧!看完这篇文章,再做起来就比较自信了,答案就不必给出了,调试运行下就出来了。
-------------------
思想火花,照亮世界