調査の動機
Dictionary<string, XXX>
のように、Keyとしてstring
を使うことはよくやると思うが、Dictionary<自作クラス, XXX>
みたいに、Keyとしてstring
やint
など以外を使いたい場面があり、落とし穴がありそうな気がしたので調べてみた。
(C#ではstring
は==
で内容比較をするが、通常のクラスは==
は(演算子をoverrideしなければ)C言語でいうポインタを比較しているようなイメージになる。とかでKeyの一意性に問題がでそうな予感がしたため。)
.NETの内部処理を確認する
Step1 - KeyからValueを取り出す処理
this[TKey]
で取得設定するときに、Keyの一致性をどう判定しているか。
ILSpyで内部処理を覗いてみる。
[__DynamicallyInvokable]publicTValuethis[TKeykey]{[__DynamicallyInvokable]get{intnum=FindEntry(key);if(num>=0){returnentries[num].value;}ThrowHelper.ThrowKeyNotFoundException();returndefault(TValue);}[__DynamicallyInvokable]set{Insert(key,value,add:false);}}
内部では、entries[num]
(numはint
型)に格納されるらしい。num
を算出しているFindEntry(key)
を調べればよさそう。
Step2 - KeyからHash値を取って検索する処理
privateintFindEntry(TKeykey){if(key==null){ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);}if(buckets!=null){intnum=comparer.GetHashCode(key)&int.MaxValue;for(intnum2=buckets[num%buckets.Length];num2>=0;num2=entries[num2].next){if(entries[num2].hashCode==num&&comparer.Equals(entries[num2].key,key)){returnnum2;}}}return-1;}
int num = comparer.GetHashCode(key) & int.MaxValue;
で設定しているので、まずはcomparer
を調べる。
Step3 - comparer
Dictionary
クラスの
メンバcomparer
の宣言は、private IEqualityComparer<TKey> comparer;
であり、
varhoge=newDictionary<Keyの型,Valueの型>();
のようにした場合、以下のようにコンストラクタで設定される。
publicDictionary():this(0,(IEqualityComparer<TKey>)null){/* この中は処理がない */}
つまり、下記のコンストラクタが、capacity
が0
で、comparer
がnull
として実行される。
[__DynamicallyInvokable]publicDictionary(intcapacity,IEqualityComparer<TKey>comparer){・・中略(capacityに関する処理)・・this.comparer=(comparer??EqualityComparer<TKey>.Default);}
つまり、(引数なしのコンストラクタを使った場合は、)comparer
には、EqualityComparer<TKey>.Default
が設定される。
publicstaticEqualityComparer<T>Default{[__DynamicallyInvokable]get{returndefaultComparer;}}privatestaticreadonlyEqualityComparer<T>defaultComparer=CreateComparer();
えぐいコードきた。。
[SecuritySafeCritical]privatestaticEqualityComparer<T>CreateComparer(){RuntimeTyperuntimeType=(RuntimeType)typeof(T);if(runtimeType==typeof(byte)){return(EqualityComparer<T>)newByteEqualityComparer();}if(typeof(IEquatable<T>).IsAssignableFrom(runtimeType)){return(EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<int>),runtimeType);}if(runtimeType.IsGenericType&&runtimeType.GetGenericTypeDefinition()==typeof(Nullable<>)){RuntimeTyperuntimeType2=(RuntimeType)runtimeType.GetGenericArguments()[0];if(typeof(IEquatable<>).MakeGenericType(runtimeType2).IsAssignableFrom(runtimeType2)){return(EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(NullableEqualityComparer<int>),runtimeType2);}}if(runtimeType.IsEnum){switch(Type.GetTypeCode(Enum.GetUnderlyingType(runtimeType))){caseTypeCode.Int16:return(EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(ShortEnumEqualityComparer<short>),runtimeType);caseTypeCode.SByte:return(EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(SByteEnumEqualityComparer<sbyte>),runtimeType);caseTypeCode.Byte:caseTypeCode.UInt16:caseTypeCode.Int32:caseTypeCode.UInt32:return(EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(EnumEqualityComparer<int>),runtimeType);caseTypeCode.Int64:caseTypeCode.UInt64:return(EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(LongEnumEqualityComparer<long>),runtimeType);}}returnnewObjectEqualityComparer<T>();}
ちゃんと解析できてないけど、Dictionary
のKeyとする自作クラスが、以下
- IEquatable<T>
を実装している(String
は実装しているようである。)
- ジェネリックを使用している
- 数値型
のいずれでもない場合は、return new ObjectEqualityComparer<T>();
が呼ばれると思われる。ちょっと自信がないが。
Step4 - GetHashCode()
■おさらい
Step2で、int num = comparer.GetHashCode(key) & int.MaxValue;
を調査対象としていた。
Step3で、comparer
がObjectEqualityComparer<T>
らしいことを見た。
なので、ここでは、comparer.GetHashCode(key)
が何を返すかを見る。
publicoverrideintGetHashCode(Tobj){returnobj?.GetHashCode()??0;}
結局、KeyのクラスのGetHashCode()
が呼ばれる。
いったん結論
飛躍している感が否めないが、Step2の処理の以下の部分が肝であり、
if(entries[num2].hashCode==num&&comparer.Equals(entries[num2].key,key)){returnnum2;}
以下2点を満たせればよい。
KeyのクラスのGetHashCode()
が、
【1】. 「同じ」とみなしたいデータのGetHashCode()
が、同じ値を返すこと1。かつ、Equals()
が同じ結果を返すこと。
【2】. 「違う」とみなしたいデータのEquals()
が、異なる値を返すこと。
GetHashCode()
の計算内容はどういう点を考えてつくればよいのか?
→ Equalsより処理が軽くて、できるだけ衝突しないようにするのがよい。参考サイト参照。。。
続き
Step5 - buckets
引数なしのコンストラクタを呼んだだけではnull
のままであり、
Key(とValue)が追加されるときに初期化されるっぽい。
privatevoidInsert(TKeykey,TValuevalue,booladd){if(key==null){ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);}if(buckets==null){Initialize(0);}intnum=comparer.GetHashCode(key)&int.MaxValue;intnum2=num%buckets.Length;intnum3=0;for(intnum4=buckets[num2];num4>=0;num4=entries[num4].next){if(entries[num4].hashCode==num&&comparer.Equals(entries[num4].key,key)){if(add){ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);}entries[num4].value=value;version++;return;}num3++;}intnum5;if(freeCount>0){num5=freeList;freeList=entries[num5].next;freeCount--;}else{if(count==entries.Length){Resize();num2=num%buckets.Length;}num5=count;count++;}entries[num5].hashCode=num;entries[num5].next=buckets[num2];entries[num5].key=key;entries[num5].value=value;buckets[num2]=num5;version++;if(num3>100&&HashHelpers.IsWellKnownEqualityComparer(comparer)){comparer=(IEqualityComparer<TKey>)HashHelpers.GetRandomizedEqualityComparer(comparer);Resize(entries.Length,forceNewHashCodes:true);}}privatevoidInitialize(intcapacity){intprime=HashHelpers.GetPrime(capacity);buckets=newint[prime];for(inti=0;i<buckets.Length;i++){buckets[i]=-1;}entries=newEntry[prime];freeList=-1;}
HashHelpers.GetPrime(capacity)
[ReliabilityContract(Consistency.WillNotCorruptState,Cer.Success)]publicstaticintGetPrime(intmin){if(min<0){thrownewArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));}for(inti=0;i<primes.Length;i++){intnum=primes[i];if(num>=min){returnnum;}}for(intj=min|1;j<int.MaxValue;j+=2){if(IsPrime(j)&&(j-1)%101!=0){returnj;}}returnmin;}publicstaticreadonlyint[]primes=newint[72]{3,7,11,17,23,29,37,47,59,71,89,107,131,163,197,239,293,353,431,521,631,761,919,1103,1327,1597,1931,2333,2801,3371,4049,4861,5839,7013,8419,10103,12143,14591,17519,21023,25229,30293,36353,43627,52361,62851,75431,90523,108631,130363,156437,187751,225307,270371,324449,389357,467237,560689,672827,807403,968897,1162687,1395263,1674319,2009191,2411033,2893249,3471899,4166287,4999559,5999471,7199369};
ハッシュのテーブルの処理が結構複雑で、見る気力がなくなった。。
参考サイト
内部構造調べた後に見つけた。まだ見きれていない。
http://mocotan.hatenablog.com/entry/2017/10/31/064738
GetHashCode()
はObject型であればメンバとして持っているので、これをoverride
してやればよい。 ↩