問題の背景#
まず、以前のプロジェクト開発時に遭遇した、カスタムタイプを Dictionary のキーとして使用する際の落とし穴について触れたいと思います。
プロジェクトでは、BusinessA と BusinessB という 2 つのビジネスクラスがあり、ある要件のためにこれらのクラスの間にマッピング関係を構築する必要があったため、Dictionary データ構造を導入してそれらを関連付けました:
private Dictionary<BusinessA, BusinessB> businessDic = new Dictionary<BusinessA, BusinessB>();
// 外部に提供する検索メソッド、Aを使って辞書からBを見つける
public BusinessB FindB(BusinessA a)
{
if (businessDic.ContainsKey(a))
{
return businessDic[a];
}
return null;
}
プロジェクトは共同開発であり、ビジネスロジックが増えるにつれて、他のビジネスロジックの中で辞書内のいくつかの Key オブジェクトが変更されました(BusinessA オブジェクトのデータは変わりません)。その後、再度そのオブジェクトを使って対応する BusinessB オブジェクトを探そうとしたところ、null が返されることに気付きました。当時、私たちの開発チームはこのバグの原因を見つけるのにかなりの時間を費やしました。
問題の分析#
このバグの根本的な原因は、C# における Dictionary の基礎的なストレージ原理を深く理解していなかったことにあります:
キーを辞書に追加すると、Dictionary はハッシュ関数を使用してそのキーのハッシュコードを計算します。これは整数値であり、キーが内部配列内の位置を決定するために使用されます。
最初は、参照型のオブジェクトを Dictionary のキーとして使用する場合、キーオブジェクトのデータが変わらなければ、キーを使って対応する値を取得できると考えていました。
Dictionary 使用時の Key の必要条件#
C# において、辞書 (Dcitionary<Tkey,TValue>) は Key を使用して値 Value を迅速に検索する際、辞書のキーは 2 つの重要な条件を満たす必要があります:
- 比較可能性
キーはその一意性を確定するために正しく比較できる必要があります。辞書内部では Equals メソッドを使用してキーを比較し、同じキーが同じ値にマッピングされることを保証します。
- 不変性
一度キーが辞書に追加されると、その値は変更できません。キーの状態が辞書に追加された後に変化すると、辞書の検索メカニズムが無効になり、バグを引き起こす可能性があります。
参照型を Key として使用する際の注意事項#
不安定なキーの使用#
辞書の正確性を確保するために、キーは辞書操作中に不変であるべきです。一般的には、辞書内でキーオブジェクトの状態を変更することは避けるべきです。推奨される方法は、不変オブジェクトをキーとして使用することです。readonly
フィールドや読み取り専用プロパティを使用して、キーオブジェクトの不変性を保証できます。
キーの等価性比較#
Equals
およびGetHashCode
メソッドを同時にかつ正しく実装することを確認してください。等価性比較は、オブジェクトのアイデンティティに影響を与えるすべてのフィールドを考慮すべきであり、GetHashCode
メソッドは常に同じ値を返すべきです(オブジェクトの状態が変わらない限り)。
以下のサンプルコードの出力を確認できます:
public class BusinessA
{
public int id;
public string name;
public BusinessA(int _id,string _name)
{
id = _id;
name = _name;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
BusinessA another = obj as BusinessA;
if (another != null)
{
return id == another.id && name == another.name;
}
return false;
}
}
public class BusinessB
{
private string describe;
private string score;
public BusinessB(string _des,string _score)
{
describe = _des;
score = _score;
}
}
public class DictionaryDemo : MonoBehaviour
{
private Dictionary<BusinessA, BusinessB> businessDic = new Dictionary<BusinessA, BusinessB>();
private BusinessA bA;
private BusinessB bB;
private BusinessA bAA;
void Start()
{
int id = 100;
string name = "zzz";
// 内部データが同じオブジェクトbAとbAAをそれぞれストレージのキーと検索のキーとして宣言
bA = new BusinessA(id, name);
bAA = new BusinessA(id, name);
// 値を宣言
bB = new BusinessB("yyy", "100");
// bAとbBを辞書に追加
businessDic.Add(bA, bB);
// bAAを使って辞書でbBを検索
BusinessB findB = FindB(bAA);
BusinessB findB1 = FindB(bA);
// findBがbBと等しいか比較
Debug.Log(bB == findB);
}
public BusinessB FindB(BusinessA a)
{
if (businessDic.ContainsKey(a))
{
return businessDic[a];
}
return null;
}
}
コンソール出力:
このように、bA と bAA の内部メンバーのデータは同じですが、GetHashCode メソッドをオーバーライドしていないため、これは異なるハッシュ値を持つ 2 つのオブジェクトと見なされ、bAA を使用して Value を見つけることができず、null が返されます。したがって、bB は findB と等しくありません。
解決策#
このことから、解決策が導き出されます:辞書にキーと値のペアを格納する際に、カスタムタイプを辞書のキーとして使用する必要がある場合、そのカスタムタイプは GetHashCode メソッドをオーバーライドし、正しく実装する必要があります。
検証:
BusinessA で GetHashCode メソッドをオーバーライドします:
public override int GetHashCode()
{
return string.Format("{0}-{1}", id, name).GetHashCode();
}
コンソール出力:
これにより、bA と bAA は異なるオブジェクト参照であるにもかかわらず、GetHashCode メソッドをオーバーライドした後、辞書検索時に正しく対応するキーをマッチングできるため、Value を見つけることができ、findB と bB は等しくなります。
==Equals と GetHashCode メソッドはどちらも必要不可欠です。==
まとめ#
コードレベルでバグが発生した場合、多くの場合、基礎的なロジックを理解していないことが原因です。普段から多くのテストを行い、検証し、原理を理解することが重要です。したがって、辞書をデータストレージおよび検索構造として使用する際には、不変タイプをキーとして使用することをお勧めします。値型(int、float など)や一般的な参照型 string などです。カスタムタイプを辞書のキーとして使用する必要がある場合は、次の 2 点に注意してください:1. 辞書操作中にキーを変更しないこと;2. キー Key のオブジェクトは Equals および GetHashCode メソッドをオーバーライドし、正しく実装する必要があります。これらの原則に従うことで、カスタムタイプオブジェクトを辞書のキーとして使用することによって引き起こされるバグを回避できます。