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

 .Net中具有Contains方法(或ContainsXXXX方法)的类很多,大多为集合类,请看下图:
.Net 相等性:集合类 Contains 方法 深入详解_第1张图片

 

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

.Net 相等性:集合类 Contains 方法 深入详解_第2张图片

 一般集合类的Contains都源自ICollection<T>,字典类的ContainsKey都源自IDictionary<TKey, TValue>。另外System.Linq.Enumerable类(.Net3.0)扩展了IEnumerable<T>接口:
.Net 相等性:集合类 Contains 方法 深入详解_第3张图片

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

.Net 相等性:集合类 Contains 方法 深入详解_第4张图片 

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

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  =   " 鹤冲天 "  };
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  ==   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更具有实际意义,如果非要比较引用相等,用“==”比较即可。

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

.Net 相等性:集合类 Contains 方法 深入详解_第5张图片 

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

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

.Net 相等性:集合类 Contains 方法 深入详解_第6张图片 

 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 ( 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 相等性的测试题目,看你基础牢不牢》吧!看完这篇文章,再做起来就比较自信了,答案就不必给出了,调试运行下就出来了。

你可能感兴趣的:(contains)