ハマった話
[Fact]
public void ArrayKeytest001()
{
byte[] arr1 = [0, 1, 2, 3];
byte[] arr2_1 = [0, 1];
byte[] arr2_2 = [2, 3];
byte[] arr2 = [.. arr2_1, .. arr2_2];
Dictionary<byte[], string> dic = [];
dic.Add(arr1, "Hoge");
dic.TryAdd(arr2, "Fuga"); // (1)
Log.LogInformation("dic={}", dic);
// dic=[System.Byte[], Hoge], [System.Byte[], Fuga]
}
UnitTestの形にして表記しましたが、arr1
とarr2
は共に[0, 1, 2, 3]
なので(1)のTryAdd()
はfalse
が返って、dic
の中身は{[0, 1, 2, 3], "Fuga"}
だけになるとおもった。
でも実際はdic
の中には2つのKeyValuePair
ができてしまった…。
理由
Dictionary
のKeyはGetHashCode()
の結果で比較されています。
参考:https://learn.microsoft.com/ja-jp/dotnet/api/system.collections.generic.dictionary-2
Dictionary では、キーが等しいかどうかを判断するために等価実装が必要です。 comparer パラメーターを受け取るコンストラクターを使用して、IEqualityComparer ジェネリック インターフェイスの実装を指定できます。実装を指定しない場合は、既定のジェネリック等値比較子 EqualityComparer.Default が使用されます。 型 TKeySystem.IEquatable ジェネリック インターフェイスを実装する場合、既定の等値比較子はその実装を使用します。
残念ながらbyte[]
(というか、配列全部)は当然IEqualityComparer<T>
を実装していないです。
https://learn.microsoft.com/ja-jp/dotnet/api/system.array
どうもIStructuralEquatable
は実装しているようですが、IStructuralEquatable
はDictionaryのKeyの同一性の判断には使用されないようです。
結果、arr1
とarr2
は中身は同じであってもEquals()
的には同一ではないと判断されるようです。
(デフォルトだとEquals()
はインスタンス自体が同じかどうかを比較するっぽいです。しらんけど。)
解決方法
Dictionary
のKeyにはIEqualityComparer<T>
を実装したクラスを指定する必要があります。
若しくは、Dictionary
のコンストラクト時にIEqualityComparer<TKey>
を引数に取るコンストラクタを使用し、その中でbyte[]とbyte[]を比較してやる必要があります。
IEqualityComparer<TKey>
を使う場合は↓のようなクラスを作成します。
public class ByteArrayEqualityComparer : IEqualityComparer<byte[]>
{
public bool Equals(byte[] x, byte[] y)
{
return GetHashCode(x) == GetHashCode(y);
}
public int GetHashCode([DisallowNull] byte[] obj)
{
HashCode hash = new();
foreach (byte b in obj)
{
hash.Add(b);
}
return hash.ToHashCode();
}
}
で、Dictionary
のコンストラクタにセットします。
[Fact]
public void ArrayKeytest002()
{
byte[] arr1 = [0, 1, 2, 3];
byte[] arr2_1 = [0, 1];
byte[] arr2_2 = [2, 3];
byte[] arr2 = [.. arr2_1, .. arr2_2];
Dictionary<byte[], string> dic = new(new ByteArrayEqualityComparer());
dic.Add(arr1, "Hoge");
dic.TryAdd(arr2, "Fuga");
Log.LogInformation("dic={}", dic); // dic=[System.Byte[], Hoge]
}
もしくは、byte[]
をラップしてIEquatable<T>
を実装した↓のようなClassを用意し、
public class ByteArrayWrapper(byte[] src) : IEqualityComparer<byte[]>, IEquatable<ByteArrayWrapper>
{
public bool Equals(byte[] x, byte[] y)
{
return GetHashCode(x) == GetHashCode(y);
}
public bool Equals(ByteArrayWrapper other)
{
return GetHashCode() == other.GetHashCode();
}
public int GetHashCode([DisallowNull] byte[] obj)
{
HashCode hash = new();
foreach (byte b in obj)
{
hash.Add(b);
}
return hash.ToHashCode();
}
public override int GetHashCode()
{
return GetHashCode(src);
}
}
これをDictionary
のKeyにしてやります。
[Fact]
public void ArrayKeytest003()
{
byte[] arr1 = [0, 1, 2, 3];
byte[] arr2_1 = [0, 1];
byte[] arr2_2 = [2, 3];
byte[] arr2 = [.. arr2_1, .. arr2_2];
Dictionary<ByteArrayWrapper, string> dic = []; // ***
dic.Add(new ByteArrayWrapper(arr1), "Hoge"); // ***
dic.TryAdd(new ByteArrayWrapper(arr2), "Fuga"); // ***
Log.LogInformation("dic={}", dic); // dic=[System.Byte[], Hoge]
}
※ ちなみに例示した実装は毎回Hash値を計算するのでちょっと賢くないです。不変なのに。
御後が宜しいようで…。