Implementace porovnávání u referenčních typů

Tomáš Holan       03.08.2012       C#       11090 zobrazení

Jazyk C# rozlišuje dva typy porovnávání - porovnání referenční (reference equality) a hodnotové (value equality). Hodnotové porovnávání se chápe tak, že dva objekty jsou shodné pokud tyto objekty představují stejnou hodnotu, referenční se chápe tak, že dvě reference jsou shodné pokud odkazují na stejný objekt (tj. stejnou instanci). Pro referenční porovnávání se používá metoda ReferenceEquals, pro hodnotové metoda Equals (případně statická metoda Equals).

Pokud referenční typ hodnotové porovnávání neimplementuje provádí metoda Equals i operátory == (rovno) a != (nerovno) pouze porovnávání referenční.

Hodnotové porovnávání by měli implementovat objekty, jejíž vnitřní data sémanticky představují nějakou hodnotu, tj. měli by přepsat metodu Equals a GetHashCode a přetížit operátor ==  a operátor !=. POZOR ale, že tyto objekty by také měli být vždy implementovány jako immutable tj. aby se jejich vnitřní stav (hodnota) nemohl v průběhu života instance měnit.

Rozdíl obou typů porovnávání u typu implementujícího hodnotové porovnávání (zde třída Point) můžeme demonstrovat například takto:

static class Test
{
    static void Main()
    {
        Point p1 = new Point(1, 3);
        Point p2 = new Point(1, 3);

        Console.WriteLine(object.ReferenceEquals(p1, p2));  //False
        Console.WriteLine(p1 == p2);    //True
    }
}

A jak správně a optimálně hodnotové porovnávání implementovat?

Ukážeme si to na jednoduchém příkladu výše zmíněné třídy Point obsahující pouze dvě vlastnosti X a Y určující hodnotu instance.

Implementace této třídy bude vypadat takto:

[System.Diagnostics.DebuggerDisplay("\\{ X = {X}, Y = {Y} \\}")]
public class Point
{
    #region member varible and default property initialization
    public int X { get; private set; }
    public int Y { get; private set; }
    #endregion

    #region constructors and destructors
    public Point(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
    #endregion

    #region action methods
    public override bool Equals(object obj)
    {
        if (obj == null)
        {
            return false;
        }
        var other = obj as Point;
        if ((object)other == null)
        {
            return false;
        }
        return this == other;
    }

    public bool Equals(Point other)
    {
        if ((object)other == null)
        {
            return false;
        }
        return this == other;
    }

    public override int GetHashCode()
    {
        return this.X.GetHashCode() ^ this.Y.GetHashCode();
    }

    public static bool operator ==(Point left, Point right)
    {
        if (object.ReferenceEquals(left, right))
        {
            return true;
        }
        if ((object)left == null || (object)right == null)
        {
            return false;
        }
        return left.X == right.X && left.Y == right.Y;
    }

    public static bool operator !=(Point left, Point right)
    {
        return !(left == right);
    }

    public override string ToString()
    {
        return string.Format("X = {0}, Y = {1}", this.X, this.Y);
    }
    #endregion
}

Poznámky k implementaci:

  • Datový typ Point je immutable a vlastnosti X a Y se nastavují pouze v konstruktoru třídy.
  • Metoda Equals neporovnává vlastnosti X a Y instancí other a this, ale namísto toho používá přetížený operátor ==. Je to z toho důvodu, aby vlastní “logika” srovnání dvou instancí byla jen na jednom místě (to by mělo hlavně význam pokud by vlastností určující hodnotu instance bylo větší množství).
  • Kromě přepsání metody Equals(object) se z důvodu zvýšení výkonu doporučuje doplnit ještě i metodu Equals(Type).
  • Porovnávání na null v metodách Equals(object), Equals(Type) a v operátoru == se musí provádět včetně přetypování na object. Je to z toho důvodu, aby se nevolal náš přetížený operátor ==. Druhou variantou je používat volání metody ReferenceEquals.
  • Metoda GetHashCode používá operátor ^ pro “sloučení” dílčích výsledků volání GetHashCode na jednotlivých vlastnostech určujících hodnotu instance (více o tom jak implementovat metodu GetHashCode je popsáno zde).
  • Pokud má třída malý počet vlastností určující hodnotu instance (tj. jeden nebo maximálně dvě), nevadilo by operátor == případně implementovat pouze jako jediný výraz:
public static bool operator ==(Point left, Point right)
{
    return ((object)left == null && (object)right == null) || ((object)left != null && (object)right != null && left.X == right.X && left.Y == right.Y);
}

U implementace porovnání pro hodnotové datové typy (struktury) odpadá nutnost porovnávání na hodnotu null, s tím jsme se již setkali v článku zde.

 

hodnocení článku

0       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

RE: Implementace porovnávání u referenčních typů

@Petr To lze samozřejmě také. Já jsem pro účely příkladu raději volil variantu, kdy je implementace metody Equals(object) nezávislá na tom, zda druhou metodu Equals(Type) do typu doplníme či nikoliv. Je to pouze z toho důvodu, že její doplnění není bezpodmínečná nutnost a v některých scénářích tak učinit nemusíme.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

RE: Implementace porovnávání u referenčních typů

Původní equals bych upravil takto

public override bool Equals(object obj)

{

if (obj == null)

{

return false;

}

var other = obj as Point;

return this.Equals(other);

}

Petr

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback