源码分析之Dictionary

        接下来我们一步步来熟悉 Dictionary的底层结构实现,下面的MyDictionary等同于源码中的Dictionary看待。

       首先我们定义一个类 MyDictionary,类中定义一个结构Entry,用来存储我们的key和value,除此之外,我们还要定义一个int变量用于存储key的hashCode,因为我们是根据hash值来查找的,然后我们再定义一个next变量 用于内部指向同一个桶下的下一个Entry(发生桶碰撞时,采用链式把冲突的链起来,即拉链法)

public class MyDictionary<TKey, TValue>
{
   .......
   private struct Entry
   {
        public int hashCode;    //  31 位 hash code, -1 表示未用
        public int next;        // 下一个Entry的索引位置 -1 表示链最后一个
        public TKey key;           // Key of entry
        public TValue value;         // Value of entry
   }
  .....
}

接下来接下来我们一个个加 其他字段,先简单说下这个hashCode到时候是怎么赋值的

 我们源代码中是

 int  hashCode =comparer.GetHashCode(key) & 0x7FFFFFFF;

而 comparer是我们定义的一个类型为 IEqualityComparer<TKey> 的私有变量,而这个私有变量的赋值是在我们MyDictionary构造函数中赋值的

构造函数有好多重载方法,主要参数有两个,一个是初始容量,另一个就是这个IEqualityComparer<TKey> comparer

假设我们定义一个 var  dic=Dictionary<int,string>();

public MyDictionary(): this(0, null)    //走这里
{ 
}

public MyDictionary(int capacity): this(capacity, null) 
{
}

public MyDictionary(IEqualityComparer<TKey> comparer): this(0, comparer) 
{
}

public MyDictionary(int capacity, IEqualityComparer<TKey> comparer)   //然后到这里
{
   if (capacity < 0)
   {
      throw new Exception("capacity异常");
   } 
   if (capacity > 0) Initialize(capacity); //初始化容器
this.comparer = comparer ?? EqualityComparer<TKey>.Default; //然后到这里 }

可以看到,我们没有指定它,这个comparer 用的是    EqualityComparer<TKey>.Default;

Dictioanry内部的比较都是通过这个实例来进行的。我们继续看它源码级别的定义

  public abstract class EqualityComparer<T> : IEqualityComparer, IEqualityComparer<T>
  {
       static readonly EqualityComparer<T> defaultComparer = CreateComparer();

        public static EqualityComparer<T> Default
        {
                get
                {                
                    return defaultComparer;
                }
        }

         private static EqualityComparer<T> CreateComparer()   //走到这里
            {                      
                RuntimeType t = (RuntimeType)typeof(T);
                // Specialize type byte for performance reasons
                if (t == typeof(byte))
                {
                    return (EqualityComparer<T>)(object)(new ByteEqualityComparer());
                }
                // If T implements IEquatable<T> return a GenericEqualityComparer<T>
                if (typeof(IEquatable<T>).IsAssignableFrom(t))
                {
                    return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<int>), t);
                }
               if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))
                {
                    RuntimeType u = (RuntimeType)t.GetGenericArguments()[0];
                    if (typeof(IEquatable<>).MakeGenericType(u).IsAssignableFrom(u))
                    {
                        return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(NullableEqualityComparer<int>), u);
                    }
                }

                // See the METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST and METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST_LONG cases in getILIntrinsicImplementation
                if (t.IsEnum)
                {
                    TypeCode underlyingTypeCode = Type.GetTypeCode(Enum.GetUnderlyingType(t));

              switch (underlyingTypeCode)
                {
                  case TypeCode.Int16: // short
                          return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(ShortEnumEqualityComparer<short>), t);
                  case TypeCode.SByte:
                          return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(SByteEnumEqualityComparer<sbyte>), t);
                  case TypeCode.Int32:
                  case TypeCode.UInt32:
                  case TypeCode.Byte:
                  case TypeCode.UInt16: //ushort
                          return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(EnumEqualityComparer<int>), t);
                  case TypeCode.Int64:
                  case TypeCode.UInt64:
                          return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(LongEnumEqualityComparer<long>), t);
}

                 }
                // Otherwise return an ObjectEqualityComparer<T>
                return new ObjectEqualityComparer<T>();

}

这个大致可以概括为:在CreateComparer我们可以看到如果我们的类型不是byte、没实现IEquatable<T>接口、不是Nullable<T>、不是enum的话,会默认给我们创建一个ObjectEqualityComparer<T>()

   ObjectEqualityComparer<T>();  这个的源码定义为:

internal class ObjectEqualityComparer<T> : EqualityComparer<T>
{

    public override bool Equals(T x, T y)
    {
        if (x != null)
        {
            if (y != null) return x.Equals(y);
            return false;
        }
        if (y != null) return false;
        return true;
    }

   
    public override int GetHashCode(T obj)
    {
        if (obj == null) return 0;
        return obj.GetHashCode();
    }

  
    // Equals method for the comparer itself. 
    public override bool Equals(Object obj)
    {
        ObjectEqualityComparer<T> comparer = obj as ObjectEqualityComparer<T>;
        return comparer != null;
    }

    public override int GetHashCode()
    {
        return this.GetType().Name.GetHashCode();
    }
}

要注意的是  这个  ObjectEqualityComparer 下的Equal方法 对于像值类型是有装箱操作的

我们使用Dictionary的时候一般的习惯应该就上面那样用,这种使用方法在我们使用内置的类型当key的时候没有问题,但是如果我们需要将一个自定义的值类型(struct)当作key的时候就需要注意了。这里有一个很容易忽略的问题,会导致使用Dictionary的时候带来大量不必要的性能开销。

我们先做一个实验来比较一下值类型和类作为key的性能有多大的差距。实验代码如下,这段代码中我插入1000个到10000个数据来得到所需要的时间。

ublic class/struct CustomKey
{
    public int Field1;
    public int Field2;
    public override int GetHashCode()
    {
        return Field1.GetHashCode() ^
                Field2.GetHashCode();
    }
 
    public override bool Equals(object obj)
    {
        CustomKey key = (CustomKey)obj;
        return this.Field1 == key.Field1 &&
                this.Field2 == key.Field2;
    }
}
 
Dictionary<CustomKey, int> dict = new Dictionary<CustomKey, int>();
int tryCount = 50;
double totalTime = 0.0;
for (int count = 1000; count < 10000; count += 1000)
{
    for (int j = 0; j < tryCount; j++)
    {
        Stopwatch watcher = Stopwatch.StartNew();
        for (int i = 0; i < count; i++)
        {
            CustomKey key = new CustomKey() { Field1 = i * 2, Field2 = i * 2 + 1 };
            dict.Add(key, i);
        }
        watcher.Stop();
        dict.Clear();
        totalTime += watcher.ElapsedMilliseconds;
    }
    Console.WriteLine("{0},{1}", count, totalTime / tryCount);
}

结果是这样子的:

Class vs struct

原因就在于:ObjectEqualityComparer的默认实现中会存在着很多的装箱操作,它是用来将值类型装箱成引用类型的。这个操作是很耗时的,因为它需要创建一个object并将值类型中的值拷贝到新创建的对象中。

猜你喜欢

转载自www.cnblogs.com/wwkk/p/10337911.html