Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8899

C# - DictionaryのKey一致判定の内部構造を調べてみた(途中)

$
0
0

調査の動機

Dictionary<string, XXX>のように、Keyとしてstringを使うことはよくやると思うが、
Dictionary<自作クラス, XXX>みたいに、Keyとしてstringintなど以外を使いたい場面があり、落とし穴がありそうな気がしたので調べてみた。

(C#ではstring==で内容比較をするが、通常のクラスは==は(演算子をoverrideしなければ)C言語でいうポインタを比較しているようなイメージになる。とかでKeyの一意性に問題がでそうな予感がしたため。)

結果だけ知りたい場合は、結論もしくは参考サイト参照

.NETの内部処理を確認する

Step1 - KeyからValueを取り出す処理

this[TKey]で取得設定するときに、Keyの一致性をどう判定しているか。
ILSpyで内部処理を覗いてみる。

Dictionary<TKey,TValue>クラス内部
[__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値を取って検索する処理

Dictionary<TKey,TValue>クラス内部
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の型>();

のようにした場合、以下のようにコンストラクタで設定される。

Dictionary<TKey,TValue>クラス内部
publicDictionary():this(0,(IEqualityComparer<TKey>)null){/* この中は処理がない */}

つまり、下記のコンストラクタが、capacity0で、comparernullとして実行される。

Dictionary<TKey,TValue>クラス内部
[__DynamicallyInvokable]publicDictionary(intcapacity,IEqualityComparer<TKey>comparer){・・中略(capacityに関する処理)・・this.comparer=(comparer??EqualityComparer<TKey>.Default);}

つまり、(引数なしのコンストラクタを使った場合は、)
comparerには、EqualityComparer<TKey>.Defaultが設定される。

EqualityComparer<T>クラス
publicstaticEqualityComparer<T>Default{[__DynamicallyInvokable]get{returndefaultComparer;}}privatestaticreadonlyEqualityComparer<T>defaultComparer=CreateComparer();

えぐいコードきた。。

EqualityComparer<T>クラス内部
[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で、comparerObjectEqualityComparer<T>らしいことを見た。

なので、ここでは、comparer.GetHashCode(key)が何を返すかを見る。

ObjectEqualityComparer<T>クラス内部
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)が追加されるときに初期化されるっぽい。

Dictionary<TKey,TValue>クラス内部
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)
HashHelpersクラス内部
[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


  1. GetHashCode()はObject型であればメンバとして持っているので、これをoverrideしてやればよい。 


Viewing all articles
Browse latest Browse all 8899

Trending Articles