.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 {
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更具有实际意义,如果非要比较引用相等,用“==”比较即可。
再来看一些与相等性有关的接口:
前两个比较相同,后两个不但可以比较相等还可比较谁大谁小(用于集合排序)。这次只讨论前两个。两个接口的声明如下:
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>很像的,它们是什么关系呢。我把和它和它的派生类都找了出来,连根拔起,如下:
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 相等性的测试题目,看你基础牢不牢》吧!看完这篇文章,再做起来就比较自信了,答案就不必给出了,调试运行下就出来了。