All articles

HashSet.Add végtelen ciklus oka és megoldása

Published 2013-02-10 - Last modified 2013-02-10

Original: https://soci.hu/blog/index.php/2013/02/10/hashsetadd-vegtelen-ciklus-oka-es-megoldasa/

Alapszabály, hogy HashSet, Dictionary vagy bármi más asszociatív kollekciókban kulcsként csak immutable objektumokat lehet használni. Ennek az az oka, hogy amikor berakunk egy elemet, akkor a kollekció meghívja az objektumon a GetHashCode-ot, és az azonos hashkódú objektumokat listába szervezi. Ha két objektum egyenlőnek tekintendő (az Equals alapján), akkor a hascodejuknak azonosnak kell lenni. Ha ez nem teljesül, vagy idővariáns, azaz később már nem igaz, akkor ezek a kollekciók meghülyülnek.
A példám így nézett ki:

public virtual bool Equals(EntityBase other)
{
if (other == null)
{
return false;
}
if (Id == 0)
{
//Transient object
return ReferenceEquals(this, other);
}
return other.Id == Id;
}

public override bool Equals(object obj)
{
if (!(obj is EntityBase))
{
return false;
}
return Equals((EntityBase)obj);
}

public override int GetHashCode()
{
return Id;
}

Eleve látszik, a GetHashCode és az Equals nem azonos logika szerint működik. A GetHashCode akkor ad vissza azonos hascodeot, ha az idk azonosak. Az Equals viszont az idtől függően vagy az idkat hasonlítja össze, vagy referenciális komparálást csinál. Ez már kapásból bug.

A másik bug, hogy ez az objektum nem immutable, az Id változik, amikor adatbázisba beszúrjuk az entitást, és kap egy idt. Emiatt az egész koncepció beteg.

A HashSet.Add meghívja a HashSet.AddIfNotPresetet, amiben van egy ciklus:

for (int i = this.m_buckets[num % this.m_buckets.Length] – 1; i >= 0; i = this.m_slots[i].next)
{
if (this.m_slots[i].hashCode == num && this.m_comparer.Equals(this.m_slots[i].value, value))
{
return false;
}
num3++;
}

Ez az ciklus akadt be. Az if feltétele sose teljesült, így sose lépett kis a ciklusból, mivel idt kapott az entitás, így megváltozott az Equals logikája.