前天关注了老赵的微信公众号:赵人希。昨天就推送了一个快速问答,下面把我的答题经历跟大家分享,希望对于菜鸟同胞们有所帮助启发。
其实这个问题在他的博客里有专门的篇幅讲解,我猜到了,哈哈。但是大牛讲问题都是在一个高度上,水平差点的需要费点力气才能理解,而这个过程就是拉进我们与大牛距离的过程,用心总结下,才能不断强化自己靠近他们。
一、通常的字典用法(熟练可直接略过)
在我平时的编码中,用的最频繁的就是代码段[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<键,值>的键通常就是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 }
可以看出,根据不同的情况它会使用各式不同的比较器。其中最适合我们的自然就是实现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 }
就这样,一个简单的问答被我们剖析了个遍,很过瘾吧!
最后,文中如果有不对的地方,欢迎各位大牛指正!这里先谢过了!
如果你觉得本文对你有帮助,那就点个赞吧,权当鼓励。