zoukankan      html  css  js  c++  java
  • .Net 相等性:集合类 Contains 方法 深入详解

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

  • 相关阅读:
    WinForm 自定义查询
    维护应用程序状态(三):使用用户配置文件
    解决无法获取 GridView 中BoundField 隐藏列值问题
    Nhibernate Unknown entity class 的解决办法
    GridView 18般绝技(转)
    IsPostBack介绍
    "ESLG.CommonUtility.NHibernateHelper"的类型初始值设定项引发异常
    Visible和style.display的不同
    维护应用程序状态(二):使用会话状态
    AS3与PHP通信的五种方法(基于HTTP协议)
  • 原文地址:https://www.cnblogs.com/ldp615/p/1560791.html
Copyright © 2011-2022 走看看