zoukankan      html  css  js  c++  java
  • [玩来玩去]将自定义的值类型用作字典的键,要特别注意什么?

      前天关注了老赵的微信公众号:赵人希。昨天就推送了一个快速问答,下面把我的答题经历跟大家分享,希望对于菜鸟同胞们有所帮助启发。

      其实这个问题在他的博客里有专门的篇幅讲解,我猜到了,哈哈。但是大牛讲问题都是在一个高度上,水平差点的需要费点力气才能理解,而这个过程就是拉进我们与大牛距离的过程,用心总结下,才能不断强化自己靠近他们。

    一、通常的字典用法(熟练可直接略过)

      在我平时的编码中,用的最频繁的就是代码段[1]:

     1     public class Example
     2     {
     3         public static void Main()
     4         {
     5             Dictionary<string, string> openWith = new Dictionary<string, string>();
     6 
     7             openWith.Add("txt", "notepad.exe");
     8             openWith.Add("bmp", "paint.exe");
     9             openWith.Add("rtf", "wordpad.exe");
    10 
    11             if (!openWith.ContainsKey("ht"))
    12             {
    13                 openWith.Add("ht", "hypertrm.exe");
    14             }
    15 
    16             foreach (KeyValuePair<string, string> kvp in openWith)
    17             {
    18                 Console.WriteLine("Key = {0}, Value = {1}",
    19                     kvp.Key, kvp.Value);
    20             }
    21 
    22             Dictionary<string, string>.ValueCollection valueColl = openWith.Values;
    23             foreach (string s in valueColl)
    24             {
    25                 Console.WriteLine("Value = {0}", s);
    26             }
    27 
    28             Dictionary<string, string>.KeyCollection keyColl = openWith.Keys;
    29             foreach (string s in keyColl)
    30             {
    31                 Console.WriteLine("Key = {0}", s);
    32             }
    33             Console.ReadKey();
    34         }
    Dictionary的通常用法

      其中Dictionary<键,值>的键通常就是int或者string类型,那现在我们需要自定义值类型,并把它作为字典的键应该怎么做?

    二、自定义值类型,并考虑用于字典的键时遇到的问题

      整天定义引用类型,值类型还真写的少,其实是对值类型的优势了解的少!代码段[2]:

     1   private struct MyKey
     2   {
     3       private readonly int _a;
     4       private readonly int _b;
     5       public MyKey(int a, int b)
     6       {
     7            _a = a;
     8            _b = b;
     9        }
    10    }

      这就OK了,可以根据自己的需求加入一些属性和方法,对于一些简单的需求,定义一个struct要更高效节省。

      代码段[2]中的值类型就可以用于字典的键,但是,这就够了吗?万事都有不完美,你有没有考虑到值类型随之而来的[装箱]!比如代码段[3]:

    1  public static void Main()
    2   {
    3       Dictionary<MyKey, string> testDic = new Dictionary<MyKey, string>();
    4       MyKey key12 = new MyKey(1, 2);
    5       testDic.Add(key12, "1&2");
    6       Console.ReadKey();
    7   }

      插一句:在我们分析问题时,要想明白原理,弄清楚.net框架中是怎么实现的,就必须抄家伙(.NET Reflector)!这是我们进步的一个重要工具。

      Dictionary中Add方法调用的Insert方法的部分实现,如代码段[4]:

     1     int num = this.comparer.GetHashCode(key) & 0x7fffffff;
     2     int index = num % this.buckets.Length;
     3     int num3 = 0;
     4     for (int i = this.buckets[index]; i >= 0; i = this.entries[i].next)
     5     {
     6          if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
     7          {
     8              if (add)
     9             {
    10                  ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
    11             }
    12             this.entries[i].value = value;
    13             this.version++;
    14             return;
    15          }
    16          num3++;
    17      }

    注意:其中用到了对象的GetHashCode和Equals方法,我们知道所有类型最终都继承自System.Object,如果在自定义的类型以及其继承层次的所有类中没有重写GetHashCode和Equals方法(自定义值类型的继承层次是MyKey=>System.ValueType=>System.Object),那么就会调用基类Object的相应方法,那必然会导致[装箱]操作。

    三、发现问题了,那就解决问题!

      而在Dictionary中,代码段[4]显示的是通过this.comparer调用的这两个方法,那this.comparer是什么呢?继续挖掘,是Dictionary类中维护的一个IEqualityComparer<T>类型的对象。代码段[5]:

     1 public Dictionary(int capacity, IEqualityComparer<TKey> comparer)
     2 {
     3     if (capacity < 0)
     4     {
     5         ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
     6     }
     7     if (capacity > 0)
     8     {
     9         this.Initialize(capacity);
    10     }
    11     this.comparer = comparer ?? EqualityComparer<TKey>.Default;
    12 }

      如果在创建Dictionary时没有在参数中提供比较器,则会使用默认的EqualityComparer<T>.Default对象给this.comparer赋值,它的构造方法是代码段[6]:

     1 [SecuritySafeCritical]
     2 private static EqualityComparer<T> CreateComparer()
     3 {
     4     RuntimeType c = (RuntimeType) typeof(T);
     5     if (c == typeof(byte))
     6     {
     7         return (EqualityComparer<T>) new ByteEqualityComparer();
     8     }
     9     if (typeof(IEquatable<T>).IsAssignableFrom(c))
    10     {
    11         return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(GenericEqualityComparer<int>), c);
    12     }
    13     if (c.IsGenericType && (c.GetGenericTypeDefinition() == typeof(Nullable<>)))
    14     {
    15         RuntimeType type2 = (RuntimeType) c.GetGenericArguments()[0];
    16         if (typeof(IEquatable<>).MakeGenericType(new Type[] { type2 }).IsAssignableFrom(type2))
    17         {
    18             return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(NullableEqualityComparer<int>), type2);
    19         }
    20     }
    21     if (c.IsEnum && (Enum.GetUnderlyingType(c) == typeof(int)))
    22     {
    23         return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(EnumEqualityComparer<int>), c);
    24     }
    25     return new ObjectEqualityComparer<T>();
    26 }
    CreateComparer方法

      可以看出,根据不同的情况它会使用各式不同的比较器。其中最适合我们的自然就是实现IEquatable<T>接口的分支了:

    1     if (typeof(IEquatable<T>).IsAssignableFrom(c))
    2     {
    3   return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(GenericEqualityComparer<int>), c);
    4     }

      为了能够符合这个if的条件,我们的自定义值类型MyKey应当实现IEquatable<MyKey>接口(实现Equals方法),并且重写GetHashCode方法(Object的GetHashCode方法也会导致装箱),才能彻底避免装箱操作。

      因此,我的最终实现是代码段[7]:

     1     public struct MyKey : IEquatable<MyKey>
     2     {
     3         private readonly int _a;
     4         private readonly int _b;
     5 
     6         public MyKey(int a, int b)
     7         {
     8             _a = a;
     9             _b = b;
    10         }
    11         public override int GetHashCode()
    12         {
    13             return (this._a ^ this._b);
    14         }
    15 
    16         public bool Equals(MyKey that)
    17         {
    18             return (this._a == that._a) && (this._b == that._b);
    19         }
    20     }

      就这样,一个简单的问答被我们剖析了个遍,很过瘾吧!

      最后,文中如果有不对的地方,欢迎各位大牛指正!这里先谢过了!

      如果你觉得本文对你有帮助,那就点个赞吧,权当鼓励。

  • 相关阅读:
    EasyDSS视频平台定制项目中普通用户登录无法全局搜索出文件的排查及优化
    EasyDSS前端代码运行npm install命令报错如何处理?
    运行EasyDSS前端代码报错Can't find Python executable“python”如何处理?
    EasyDSS视频平台前端点播服务列表跨页新增水印不展示第二页数据优化
    EasyDSS视频平台点播服务选中点播文件删除失败分析及排查
    EasyDSS视频平台下载录像由于ts文件原因导致MP4文件缺秒问题优化
    ffmpeg推流到服务器如何通过EasyDSS视频平台将视频保存为文件?
    EasyDSS视频平台是如何对直播流和点播流做处理的?
    EasyDSS视频平台点播页选中视频通道进行导出表单操作无反应排查
    如何允许EasyDSS在线课堂系统上传点播文件时文件名带有空格?
  • 原文地址:https://www.cnblogs.com/such/p/3793794.html
Copyright © 2011-2022 走看看