1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

byte[]をDictionaryのkeyにしてはいけない!

Posted at

ハマった話

        [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の形にして表記しましたが、arr1arr2は共に[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の同一性の判断には使用されないようです。
結果、arr1arr2は中身は同じであっても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値を計算するのでちょっと賢くないです。不変なのに。

御後が宜しいようで…。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?