it-swarm-eu.dev

Co je to "Best Practice" pro porovnání dvou instancí referenčního typu?

Narazil jsem na to nedávno, až doposud jsem spokojeně překonal operátora rovnosti ( == ) a/nebo Equals method, abych zjistil, zda dva typy referencí skutečně obsahovaly totéž data (tj. dvě různé instance, které vypadají stejně).

Používám to ještě více, protože jsem se stále více do automatizovaného testování (porovnání referenční/očekávané údaje proti tomu vrátil).

Při pohledu na některé z standardů kódovacích norem na MSDN jsem narazil na článek , který radí proti němu. Nyní chápu proč, že tento článek říká (protože nejsou stejné instance), ale neodpovídá na otázku:

  1. Jak nejlépe porovnat dva typy referencí?
  2. Měli bychom implementovat IComparable ? (Také jsem viděl zmínku, že by to mělo být vyhrazeno pouze pro typy hodnot).
  3. Existuje nějaké rozhraní, o kterém nevím?
  4. Měli bychom si jen zahrát vlastní?

Mnoho díky ^ _ ^

Aktualizace

Vypadá to, že jsem si přečetl nějakou dokumentaci (je to dlouhý den) a převažující Equals může být způsob, jak jít.

Pokud implementujete typy referencí, měli byste zvážit přepsání metody Equals na referenčním typu, pokud váš typ vypadá jako základní typ, například Point, String, BigNumber a tak dále. Většina referenčních typů by neměla přetížit Rovnost operátor, dokonce pokud přepíše Equals . Pokud však implementujete typ odkazu, který má mít významovou sémantiku, například typ komplexního čísla, měli byste přepsat operátora rovnosti.

44
Rob Cooper

Vypadá to, že kódujete v jazyce C #, který má metodu nazvanou Equals, kterou by vaše třída měla implementovat, pokud chcete porovnat dva objekty s použitím jiné metriky než „jsou tyto dva ukazatele (protože popisovače objektů jsou právě to, ukazatele) na stejnou adresu paměti? ".

Chytil jsem pár ukázkových kódů z zde :

class TwoDPoint : System.Object
{
    public readonly int x, y;

    public TwoDPoint(int x, int y)  //constructor
    {
        this.x = x;
        this.y = y;
    }

    public override bool Equals(System.Object obj)
    {
        // If parameter is null return false.
        if (obj == null)
        {
            return false;
        }

        // If parameter cannot be cast to Point return false.
        TwoDPoint p = obj as TwoDPoint;
        if ((System.Object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public bool Equals(TwoDPoint p)
    {
        // If parameter is null return false:
        if ((object)p == null)
        {
            return false;
        }

        // Return true if the fields match:
        return (x == p.x) && (y == p.y);
    }

    public override int GetHashCode()
    {
        return x ^ y;
    }
}

Java má velmi podobné mechanismy. Metoda equals () je součástí třídy Object / a vaše třída ji přetěžuje, pokud chcete tento typ funkce.

Důvodem přetížení '==' může být špatný nápad pro objekty je to, že obvykle stále chcete být schopni udělat "jsou tyto stejné ukazatel" srovnání. Obvykle se na ně spoléhá například při vkládání prvku do seznamu, kde nejsou povoleny žádné duplikáty, a některé z vašich částí systému nemusí fungovat, pokud je tento operátor přetížen nestandardním způsobem.

21
Matt J

Implementace rovnosti v .NET správně, efektivně a bez duplikace kódu je těžké. Konkrétně, pro referenční typy s hodnotovou sémantikou (tj. neměnné typy, které považují ekvvialence za rovnost ), byste měli implementovat rozhraní System.IEquatable<T> , a měli byste implementovat všechny různé operace (Equals, GetHashCode a ==, != ).

Jako příklad uvádíme třídu implementující rovnost hodnot:

class Point : IEquatable<Point> {
    public int X { get; }
    public int Y { get; }

    public Point(int x = 0, int y = 0) { X = x; Y = y; }

    public bool Equals(Point other) {
        if (other is null) return false;
        return X.Equals(other.X) && Y.Equals(other.Y);
    }

    public override bool Equals(object obj) => Equals(obj as Point);

    public static bool operator ==(Point lhs, Point rhs) => object.Equals(lhs, rhs);

    public static bool operator !=(Point lhs, Point rhs) => ! (lhs == rhs);

    public override int GetHashCode() =>X.GetHashCode() ^ Y.GetHashCode();
}

Jedinými pohyblivými částmi ve výše uvedeném kódu jsou tučné části: druhý řádek v Equals(Point other) a metoda GetHashCode(). Ostatní kód by měl zůstat nezměněn.

Pro referenční třídy, které nepředstavují neměnné hodnoty, neimplementujte operátory == a !=. Místo toho použijte jejich výchozí význam, kterým je porovnání identity objektu.

Kód záměrně vyrovná i objekty odvozené třídy typu. Často to nemusí být žádoucí, protože rovnost mezi základní třídou a odvozenými třídami není dobře definována. Bohužel .NET a pokyny pro kódování zde nejsou příliš jasné. Kód, který vytvoří Resharper, poslaný v jiné odpovědi , je náchylný k nežádoucímu chování v takových případech, protože Equals(object x) a Equals(SecurableResourcePermission x) bude zacházet s tímto případem jinak.

Chcete-li toto chování změnit, musí být do metody silně zadané Equals výše vložena další kontrola typu:

public bool Equals(Point other) {
    if (other is null) return false;
    if (other.GetType() != GetType()) return false;
    return X.Equals(other.X) && Y.Equals(other.Y);
}
26
Konrad Rudolph

Níže jsem shrnula, co musíte udělat, když implementujete IEquatable, a poskytla zdůvodnění z různých stránek dokumentace MSDN.


Souhrn

  • Pokud je požadováno testování hodnoty rovnosti (například při použití objektů ve sbírkách), měli byste implementovat rozhraní IEquatable, přepsat Object.Equals a GetHashCode pro vaši třídu.
  • Pokud chcete testovat referenční rovnost, měli byste použít operátor ==, operátor! = A Object.ReferenceEquals .
  • Měli byste přepsat pouze operátory == a operator! = For ValueTypes a neměnné referenční typy.

Zarovnání

IEquatable

Rozhraní System.IEquatable se používá k porovnání dvou instancí objektu pro rovnost. Objekty jsou porovnávány na základě logiky implementované ve třídě. Výsledkem porovnání je logická hodnota označující, zda jsou objekty odlišné. To je v kontrastu k rozhraní System.IComparable, které vrací celé číslo udávající, jak se liší hodnoty objektu.

Rozhraní IEquatable deklaruje dvě metody, které musí být přepsány. Metoda Equals obsahuje implementaci, která provede skutečné porovnání a vrátí hodnotu true, pokud jsou hodnoty objektů stejné nebo false, pokud nejsou. Metoda GetHashCode by měla vrátit jedinečnou hodnotu hash, která může být použita k jednoznačnému identifikaci identických objektů, které obsahují různé hodnoty. Použitý typ algoritmu hash je specifický pro implementaci. 

IEquatable.Equals Metoda

  • Měli byste implementovat IEquatable pro vaše objekty zpracovat možnost, že budou uloženy v poli nebo obecné kolekce.
  • Pokud implementujete IEquatable, měli byste také přepsat implementace základní třídy Object.Equals (Object) a GetHashCode tak, aby jejich chování odpovídalo chování metody IEquatable.Equals.

Pokyny pro přepsání Equals () a Operator == (C # Programming Guide)

  • x.Equals (x) vrací hodnotu true.
  • x.Equals (y) vrací stejnou hodnotu jako y.Equals (x)
  • if (x.Equals (y) && y.Equals (z)) vrací true, pak x.Equals (z) vrací true.
  • Postupné vyvolání x. Rovnice (y) vrátí stejnou hodnotu, dokud objekty, na které odkazuje x a y, nebudou změněny.
  • x. Rovnice (null) vrátí hodnotu false (pouze u hodnot s nulovatelnou hodnotou. Další informace naleznete v tématu Nulovatelné typy (C # Programming Guide) .) 
  • Nová implementace Equals by neměla vyhazovat výjimky.
  • Doporučuje se, aby každá třída, která přepíše Equals, přepsala také Object.GetHashCode.
  • Je doporučeno, aby kromě implementace Equals (object) implementovala každá třída také Equals (typ) pro svůj vlastní typ, aby se zvýšil výkon.

Standardně operátor == testuje referenční rovnost určením, zda dva odkazy označují stejný objekt. Proto referenční typy nemusejí implementovat operátora == za účelem získání této funkce. Pokud je typ nezměnitelný, to znamená, že data, která jsou obsažena v instanci, nelze změnit, může být užitečné přetížit operátora == pro porovnání hodnoty rovnosti namísto referenční rovnosti, protože jako neměnné objekty mohou být považovány za stejné jako dlouhé. mají stejnou hodnotu. Není vhodné přepsat operátora == v neměnných typech.

  • Přetížení operátorů == implementace by nemělo vyvolávat výjimky.
  • Jakýkoliv typ, který přetíží operátora ==, by měl také přetížit operátora!.

== Operátor (C # Reference)

  • Pro předdefinované typy hodnot operátor rovnosti (==) vrací hodnotu true, pokud jsou hodnoty jeho operandů stejné, jinak je hodnota false.
  • Pro typy odkazů jiné než řetězec == vrací hodnotu true, pokud jeho dva operandy odkazují na stejný objekt. 
  • Pro typ řetězce == porovnává hodnoty řetězců.
  • Při testování null pomocí == porovnání v rámci vašeho operátora == přepíše, ujistěte se, že používáte operátor základní třídy objektů. Pokud tomu tak není, dojde k nekonečné rekurzi, která má za následek stackoverflow.

Object.Equals Metoda (Objekt)

Pokud váš programovací jazyk podporuje přetížení operátora a pokud se rozhodnete přetížit operátora rovnosti pro daný typ, musí tento typ přepsat metodu Equals. Tyto implementace metody Equals musí vrátit stejné výsledky jako operátor rovnosti

Následující pokyny jsou určeny pro implementaci typu value :

  • Zvažte přepsání Equals, abyste získali vyšší výkon, než který poskytuje výchozí implementace Equals na hodnotu ValueType.
  • Pokud přepíšete Equals a jazyk podporuje operátora přetížení, musíte přetížit operátora rovnosti pro váš typ hodnoty.

Následující pokyny jsou určeny pro implementaci typu reference :

  • Zvažte přepsání rovnic na referenčním typu, pokud sémantika typu vychází ze skutečnosti, že typ představuje určitou hodnotu (hodnoty).
  • Většina typů referencí nesmí přetížit operátora rovnosti, i když přepíše Equals. Pokud však implementujete typ odkazu, který má mít významovou sémantiku, například typ komplexního čísla, musíte přepsat operátora rovnosti.

Další Gotchas

16
Zach Burlingame

Tento článek pouze doporučuje, aby nedošlo k přepsání operátora rovnosti (pro referenční typy), nikoli proti převažujícím Equals. Měli byste přepsat Equals v rámci svého objektu (odkaz nebo hodnotu), pokud kontroly rovnosti znamenají něco více než referenční kontroly. Pokud chcete rozhraní, můžete také implementovat IEquatable (používané generickými sbírkami). Pokud implementujete IEquatable, měli byste také přepsat stejné hodnoty, jak uvádí oddíl s poznámkami IEquatable:

Pokud implementujete IEquatable <T>, měli byste také přepsat implementace základní třídy Object.Equals (Object) a GetHashCode tak, aby jejich chování odpovídalo metodě IEquatable <T> .Equals. Pokud přepíšete Object.Equals (Object), implementace přepsaná je také volána ve volání metody statické rovnice (System.Object, System.Object) ve vaší třídě. To zajišťuje, že všechny vyvolání metody Equals vrátí konzistentní výsledky. 

Pokud jde o to, zda byste měli implementovat Equals a/nebo operátora rovnosti:

Od Implementace metody Equals

Většina typů referencí by neměla přetížit operátora rovnosti, i když přepíše Equals.

Z Pokyny pro implementaci rovnosti a Operátora rovnosti (==)

Metodu Equals přepište vždy, když implementujete operátora rovnosti (==) a uděláte z nich totéž.

To jen říká, že je třeba přepsat Equals, kdykoli implementujete operátora rovnosti. To dělá ne říkat, že vy musíte přepsat operátora rovnosti když přepíšete Equals.

3
bdukes

Pro složité objekty, které budou přinášet konkrétní srovnání, pak implementace IComparable a definování porovnání v metodách Porovnání je dobrá implementace.

Máme například objekty "Vozidlo", kde jediným rozdílem může být registrační číslo a my ho použijeme k porovnání, abychom zajistili, že očekávaná hodnota vrácená testováním je ta, kterou chceme.

2
Paul Shannon

Mám tendenci používat to, co Resharper automaticky dělá. například to automaticky upravilo pro jeden z mých referenčních typů:

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.GetType() == typeof(SecurableResourcePermission) && Equals((SecurableResourcePermission)obj);
}

public bool Equals(SecurableResourcePermission obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    return obj.ResourceUid == ResourceUid && Equals(obj.ActionCode, ActionCode) && Equals(obj.AllowDeny, AllowDeny);
}

public override int GetHashCode()
{
    unchecked
    {
        int result = (int)ResourceUid;
        result = (result * 397) ^ (ActionCode != null ? ActionCode.GetHashCode() : 0);
        result = (result * 397) ^ AllowDeny.GetHashCode();
        return result;
    }
}

Chcete-li přepsat == a stále provádět kontroly šeků, můžete ještě použít Object.ReferenceEquals.

1
mattlant

Zdá se, že společnost Microsoft změnila svou melodii, nebo alespoň dochází ke konfliktním informacím o přetížení operátora rovnosti. Podle tohoto článku Microsoft s názvem Jak: Definovat rovnost hodnoty pro typ:

"Operátory == a! = Lze použít s třídami i v případě, že je třída nepřetíží. Výchozí chování je však provést kontrolu referenčního rovnosti. Pokud ve třídě přetížíte metodu Equals, měli byste přetížit metodu == a! = operátory, ale není to nutné. "

Podle Erica Lipperta v jeho odpovědi na otázku jsem se ptal na Minimální kodex rovnosti v C # - říká:

"Nebezpečí, které zde narazíte, je, že dostanete operátora == definovaného pro vás, který standardně odkazuje na rovnost. Můžete snadno skončit v situaci, kdy přetížená metoda Equals má hodnotu rovnosti a == nemá referenční rovnost a pak omylem použijete referenční rovnost na věci, které nejsou srovnatelné s hodnotami, které jsou rovny hodnotám, což je praxe náchylná k chybám, která je těžko zjistitelná kontrolou lidského kódu.

Před pár lety jsem pracoval na algoritmu statické analýzy, abych tuto situaci statisticky zjistil, a zjistili jsme, že rychlost chyb byla přibližně dvě instance na milion řádků kódu napříč všemi codebase, které jsme studovali. Při zvažování jen codebases, který měl někde přepsaný Equals, rychlost defektu byla zřejmě značně vyšší!

Navíc vezměte v úvahu náklady oproti rizikům. Pokud již máte implementaci IComparable, pak psaní všech operátorů je triviální one-liners, které nebudou mít chyby a nikdy se nezmění. Je to nejlevnější kód, který budete kdy psát. Pokud je dána možnost volby mezi pevnými náklady na psaní a testováním tucet drobných metod oproti neomezeným nákladům na nalezení a stanovení těžko viditelné chyby, kde se referenční hodnota používá namísto hodnoty rovnosti, vím, který z nich bych si vybral.

.NET Framework nikdy nepoužije == nebo! = S jakýmkoliv typem, který píšete. Ale nebezpečí je to, co by se stalo, kdyby to udělal někdo jiný. Pokud je tedy třída pro třetí stranu, pak bych vždy poskytovala operátory == a! =. Pokud je třída určena pouze pro interní použití skupinou, stále bych pravděpodobně implementovala operátory == a! =.

Pokud by byl implementován IComparable, implementoval bych pouze operátory <, <=,> a> =. IComparable by měl být implementován pouze v případě, že typ musí podporovat objednávání - jako při třídění nebo použití v objednaném generickém kontejneru jako je SortedSet.

Pokud by skupina nebo společnost zavedly politiku, která by nikdy neimplementovala operátory == a! = - pak bych tuto politiku samozřejmě dodržovala. Pokud by taková politika existovala, bylo by moudré jej prosadit nástrojem pro analýzu kódu Q/A, který označí jakýkoli výskyt operátorů == a! = Při použití s ​​referenčním typem.

1
Bob Bryan

Je pozoruhodné, jak těžké je to správně ...

Doporučení od Microsoftu mít Equals a == dělat různé věci v tomto případě nemá smysl pro mě. V určitém okamžiku bude někdo (oprávněně) očekávat, že Equals a == vytvoří stejný výsledek a kód bude bombardovat.

Hledal jsem řešení, které:

  • výsledkem je stejný výsledek, ať se používá Equals nebo == ve všech případech
  • být ve všech případech plně polymorfní (volání odvozené rovnosti prostřednictvím referencí bází)

To je to, co používám:

  class MyClass : IEquatable<MyClass> {
    public int X { get; }
    public int Y { get; }
    public MyClass(int X, int Y) { this.X = X; this.Y = Y; }

    public override bool Equals(object obj) => obj is MyClass o && X == o.X && Y == o.Y;
    public bool Equals(MyClass o) => object.Equals(this, o);
    public static bool operator ==(MyClass o1, MyClass o2) => object.Equals(o1, o2);
    public static bool operator !=(MyClass o1, MyClass o2) => !object.Equals(o1, o2);

    public override int GetHashCode() => HashCode.Combine(X, Y);
  }

Zde vše končí v Equals(object), což je vždy polymorfní, takže oba cíle jsou dosaženy.

Odvozte toto:

  class MyDerived : MyClass, IEquatable<MyDerived> {
    public int Z { get; }
    public int K { get; }
    public MyDerived(int X, int Y, int Z, int K) : base(X, Y) { this.Z = Z; this.K = K; }

    public override bool Equals(object obj) => obj is MyDerived o && base.Equals(obj) && Z == o.Z && K == o.K;
    public bool Equals(MyDerived o) => object.Equals(this, o);
    public static bool operator ==(MyDerived o1, MyDerived o2) => object.Equals(o1, o2);
    public static bool operator !=(MyDerived o1, MyDerived o2) => !object.Equals(o1, o2);

    public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Z, K);
  }

Což je v podstatě totéž, s výjimkou jedné gotcha - když Equals(object) chce zavolat base.Equals pozor na volání base.Equals(object) a ne base.Equals(MyClass) (což způsobí nekonečnou rekurzi).

Výstraha je, že Equals(MyClass) bude v této implementaci dělat nějaký box, ale box/unboxing je vysoce optimalizovaný a to je pro mě dobře stojí za to dosáhnout výše uvedených cílů.

demo: https://dotnetfiddle.net/lrMWV8

(Poznámka: pro C #> 7.0)
(na základě odpovědi Konrada)

0
kofifus

Věřím, že dostat něco tak jednoduchého, jako je kontrola objektů pro rovnost správnosti, je trochu složitější s designem .NET.

Pro strukturu

1) Proveďte IEquatable<T>. Výrazně zlepšuje výkon.

2) Vzhledem k tomu, že nyní máte vlastní Equals, přepište GetHashCode, a aby byl v souladu s různými kontrolami rovnosti, také object.Equals.

3) Přetížení operátorů == a != nemusí být nábožensky provedeno, protože kompilátor bude varovat, pokud neúmyslně porovnáte strukturu s jinou s == nebo !=, ale je to dobré udělat tak, aby byla konzistentní s Equals metodami.

public struct Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is Entity))
            return false;

        return Equals((Entity)obj);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Pro třídu

Z MS:

Většina typů referencí by neměla přetížit operátora rovnosti, i když přepíše Equals.

Pro mě == se cítí jako hodnota rovnosti, spíše jako syntaktický cukr pro metodu Equals. Psaní a == b je mnohem intuitivnější než psaní a.Equals(b). Zřídka budeme muset zkontrolovat referenční rovnost. V abstraktních úrovních zabývajících se logickými reprezentacemi fyzických objektů to není něco, co bychom museli zkontrolovat. Myslím si, že různé sémantiky pro == a Equals mohou být ve skutečnosti matoucí. Věřím, že to mělo být == pro hodnotu rovnosti a Equals pro odkaz (nebo lepší jméno jako IsSameAs) na prvním místě. Rád bych zde nebral vážně MS, a to nejen proto, že to pro mě není přirozené, ale také proto, že přetížení == nedělá žádné velké škody. To je na rozdíl od nepřeřazení nenegenerativní Equals nebo GetHashCode, která může kousnout zpět, protože framework nepoužívá == kdekoli, ale pouze pokud ji používáme my sami. Jediný skutečný přínos, který získám z nepřetížení == a !=bude konzistence s designem celého rámce, nad kterým nemám žádnou kontrolu. A to je opravdu velká věc, tak to bohužel budu držet .

S referenční sémantikou (proměnné objekty)

1) Přepsat Equals a GetHashCode.

2) Implementace IEquatable<T> není nutností, ale bude to Nice, pokud ho máte.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

S hodnotou sémantiky (neměnné objekty)

Tohle je složitá část. Může se snadno pokazit, pokud není postaráno.

1) Přepsat Equals a GetHashCode.

2) Přetížení == a != odpovídající Equals. Ujistěte se, že funguje pro null .

2) Implementace IEquatable<T> není nutností, ale bude to Nice, pokud ho máte.

public class Entity : IEquatable<Entity>
{
    public bool Equals(Entity other)
    {
        if (ReferenceEquals(this, other))
            return true;

        if (ReferenceEquals(null, other))
            return false;

        //if your below implementation will involve objects of derived classes, then do a 
        //GetType == other.GetType comparison
        throw new NotImplementedException("Your equality check here...");
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    public static bool operator ==(Entity e1, Entity e2)
    {
        if (ReferenceEquals(e1, null))
            return ReferenceEquals(e2, null);

        return e1.Equals(e2);
    }

    public static bool operator !=(Entity e1, Entity e2)
    {
        return !(e1 == e2);
    }

    public override int GetHashCode()
    {
        throw new NotImplementedException("Your lightweight hashing algorithm, consistent with Equals method, here...");
    }
}

Věnujte zvláštní pozornost tomu, abyste zjistili, jak by se mělo jednat, pokud může být vaše třída zděděna, v takových případech budete muset určit, zda objekt základní třídy může být roven objektu odvozené třídy. V ideálním případě, pokud pro kontrolu rovnosti nejsou použity žádné objekty odvozené třídy, může být instance základní třídy rovna instanci odvozené třídy a v takových případech není nutné kontrolovat Type rovnost v obecné Equals základní třídy.

Obecně dávejte pozor, aby nedošlo ke zdvojení kódu. Mohl jsem vytvořit obecnou abstraktní základní třídu (IEqualizable<T> nebo tak) jako šablonu, která by usnadnila opakované použití, ale bohužel v C #, která mě brání v získávání dalších tříd.

0
nawfal