Poznáváme C# a Microsoft.NET 28. díl – HashProvidery a Klonování

Dnešní díl bude ještě z části souviset se třídami kolekcí. Dozvíme se totiž, co to jsou poskytovatelé hešových kódů (HashProvidery) a jak je možné tyto poskytovatele použít. V druhé části se budu zaobírat možností definovat způsob vytvoření kopie instance námi vytvářené třídy.

Komplexnější využití hešových tabulek a vlastní HashProvidery

V jednom z předchozích dílů pojednávajících o třídách kolekcí v .NET frameworku jsem popsal datovou strukturu HashTable neboli hešovou tabulku. Víme tedy, že hodnota asociované dvojice je vyhledána pomocí hešového kódu klíče, který by měl být pokud možno různý pro každou různou instanci třídy. Nemusí tomu tak ovšem vždy být a nějaké instance i přesto, že jsou logicky různé vrátí hešový kód stejný. Právě proto se při vyzvedávání hodnoty z tabulky ještě po zavolání metody GetHashCode na instanci představující klíč zavolá metoda Equals, která by měla v případě nálezu více odpovídajících prvků určit ten správný – námi hledaný.

My ovšem můžeme chtít, aby pro získání hešového kódu instance představující klíč v asociované dvojici byla použita jiná metoda než implicitně volaná GetHashCode. Řešení takovéto situace se naskýtá v podobě vytvoření třídy implementující rozhraní System.Collections.IHashProvider. Ve třídě naimplementujeme metodu GetHashCode, která přijímá jako parametr objekt, ke kterému má vytvořit hešový kód. Instanci takovéto třídy pak můžeme předat jedné z přetížených verzí konstruktoru třídy HashTable, čímž zajistíme, že bude vyhledáváno na základě námi určeného hešového kódu. Spolu s touto instancí můžeme konstruktoru předat i instanci třídy, která implementuje nám již známe rozhraní IComparer pokud nechceme, aby se pro porovnávání instancí při vyhledávání a přidávání nepoužívala implicitní metoda Equals.

Pro příklad vzpomeňme na třídu Osoba z minulého dílu. Řekněme, že její instance chceme využít jako klíče v hešové tabulce a také chceme mít možnost se rozhodnout zda bude hešový kód z dané instance získáván na základě jména nebo příjmení osoby. Zdrojový kód by po obohacení třídy o dva HashProvidery mohl vypadat následovně:

public class Osoba : IComparable
{
private string jmeno;
  private string prijmeni;
   
  public Osoba(string Jmeno, string Prijmeni)
  {
    this.jmeno = Jmeno;
    this.prijmeni = Prijmeni;
  }

  public static System.Collections.IComparer PorovnavaniPodleJmena
  {
    get{return new PorovnaniPodleJmena();}
  }

  public static System.Collections.IComparer PorovnavaniPodlePrijmeni
  {
    get{return new PorovnaniPodlePrijmeni();}
  }

  public static System.Collections.IHashCodeProvider HashPodlePrijmeni
  {
    get{return new HashProviderPodlePrijmeni();}
  }

  public static System.Collections.IHashCodeProvider HashPodleJmena
  {
    get{return new HashProviderPodleJmena();}
  }
private class PorovnaniPodleJmena : System.Collections.IComparer
  {
    public int Compare(object obj1, object obj2)
    {
      Osoba lOsoba1 = (Osoba) obj1;
      Osoba lOsoba2 = (Osoba) obj2;
      return String.Compare(lOsoba1.jmeno,lOsoba2.jmeno);
    }
  }
  private class PorovnaniPodlePrijmeni : System.Collections.IComparer
  {
    public int Compare(object obj1, object obj2)
    {
      Osoba lOsoba1 = (Osoba) obj1;
      Osoba lOsoba2 = (Osoba) obj2;
      return String.Compare(lOsoba1.prijmeni,lOsoba2.prijmeni);
    }

  }
  public int CompareTo(object obj)
  {
    if (!(obj is Osoba))
      throw new ArgumentException("Predany objekt neni typu osoba");
    Osoba lOsoba = (Osoba) obj;
    return String.Compare(this.prijmeni,lOsoba.prijmeni);
  }
  /// <summary>
  /// Vraci hesovy kod pro hodnotu jmena instance tridy osoba
  /// </summary>
  private class HashProviderPodleJmena : System.Collections.IHashCodeProvider
  {
    public int GetHashCode(object obj)
    {
      if (!(obj is Osoba))
        throw new ArgumentException("Predany objekt neni typu osoba");
      Osoba lOsoba = (Osoba) obj;
      return lOsoba.jmeno.GetHashCode();
    }
  }

  /// <summary>
  /// Vraci hesovy kod pro hodnotu prjmeni instance tridy osoba
  /// </summary>
  private class HashProviderPodlePrijmeni : System.Collections.IHashCodeProvider
  {
    public int GetHashCode(object obj)
    {
      if (!(obj is Osoba))
        throw new ArgumentException("Predany objekt neni typu osoba");
      Osoba lOsoba = (Osoba) obj;
      return lOsoba.prijmeni.GetHashCode();
    }
  }
  public override string ToString()
  {
    return jmeno + " " + prijmeni;
  }
}

Do třídy přibyly dvě vnitřní třídy implementující rozhraní System.Collections.IHashProvider, které jsou pro pohodlnější využití zpřístupnění pomocí statických vlastností. Implementace metody GetHashCode je v případě první vnitřní třídy taková, že vrací hešový kód na základě jména osoby a v případě třídy druhé na základě jejího příjmení. Jak se použití konkrétního HashProvideru projeví ukazuje tento příklad:

public class IHashProviderPriklad
{
  public static void Priklad()
  {
    //urcime ze hash bude ziskavan podle jmena
    Hashtable Adresy = new Hashtable(Osoba.HashPodleJmena,Osoba.PorovnavaniPodleJmena);
    Osoba lMichal = new Osoba("Michal","Hynek");
    Osoba lMartin = new Osoba("Martin","Pesek");
    Osoba lTomas = new Osoba("Tomas","Berger");
    Adresy.Add(lMichal,"Michalova adresa");
    Adresy.Add(lMartin,"Martinova adresa");
    Adresy.Add(lTomas,"Tomasova adresa");
    Osoba lNejakyMichal = new Osoba("Michal","Novak");
    //jelikoz je vyhledavano podle jmena bude vrace Michalova adresa
    Console.WriteLine(Adresy[lNejakyMichal]);
  }

  public static void Priklad2()
  {
    //urcime ze hash bude ziskavan podle prijmeni
    Hashtable Adresy = new Hashtable(Osoba.HashPodlePrijmeni,Osoba.PorovnavaniPodlePrijmeni);
    Osoba lMichal = new Osoba("Michal","Hynek");
    Osoba lMartin = new Osoba("Martin","Pesek");
    Osoba lTomas = new Osoba("Tomas","Berger");
    Adresy.Add(lMichal,"Michalova adresa");
    Adresy.Add(lMartin,"Martinova adresa");
    Adresy.Add(lTomas,"Tomasova adresa");
    Osoba lNejakyPesek = new Osoba("Petr","Pesek");
    //jelikoz je vyhledavano podle prijmeni bude vrace Martinova adresa
    Console.WriteLine(Adresy[lNejakyPesek]);
  }
}

Kopírování instancí a Rozhraní ICloneable

Občas můžeme potřebovat vytvořit novou instanci, která je kopií nějaké instance stávající. V některých případech může stačit využití protected metody MemberwiseClone třídy System.Object. Ovšem při jejím používání musíme být obezřetní a následující ukázka nastiňuje proč.

public class ZapouzdrenaHodnota
{
  private int hodnota;
  public ZapouzdrenaHodnota(int Hodnota)
  {
    this.hodnota = Hodnota;
  }
  public int Hodnota
  {
    get{return hodnota;}
    set{hodnota = value;}
  }
}
 
public class MojeTrida
{
  private ZapouzdrenaHodnota cislo;
  public MojeTrida(ZapouzdrenaHodnota Cislo)
  {
    this.cislo = Cislo;
  }
  public MojeTrida Klon()
  {
    return (MojeTrida)MemberwiseClone();
  }
  public ZapouzdrenaHodnota Cislo
  {
    get{return cislo;}
    set{cislo = value;}
  }
}

Důvodem k obezřetnosti je skutečnost, že metoda MemberwiseClone, vytváří takzvanou mělkou kopii objektu. To znamená, že pokud jsou atributy kopírované třídy referenčního typu, jsou zkopírovány pouze hodnoty referencí, ale referencované instance zkopírovány nejsou. Takže pokud spustíme následující příklad:

public class MojeTridaPriklad
{
  public static void Priklad()
  {
    MojeTrida lMujObjekt = new MojeTrida(new ZapouzdrenaHodnota(5));
    MojeTrida lMujObjekt2 = lMujObjekt.Klon();
    //pokud zmenime vnitrni hodnotu prvniho objektu, zmena se projevi
    //i u druheho objektu
    lMujObjekt.Cislo.Hodnota = 11;
    Console.WriteLine(lMujObjekt2.Cislo.Hodnota);
  }
}

bude vypsáno číslo 11.

Takže je vhodné v takovýchto případech, kdy jsou atributy klonované třídy referenčního typu, naimplementovat takzvanou hlubokou, nebo také úplnou kopii. A právě k tomuto účelu je určeno rozhraní ICloneable, které předepisuje jednu jedinou metodu Clone. Takže implementace třídy z předchozího příkladu, by se mohla změnit do této podoby:

public class MojeKlonovatelnaTrida : ICloneable
{
  private ZapouzdrenaHodnota cislo;
  public MojeKlonovatelnaTrida(ZapouzdrenaHodnota Cislo)
  {
    this.cislo = Cislo;
  }
  public ZapouzdrenaHodnota Cislo
  {
    get{return cislo;}
    set{cislo = value;}
  }
  #region ICloneable Members
  public object Clone()
  {
    return new MojeKlonovatelnaTrida(new ZapouzdrenaHodnota(cislo.Hodnota));
  }
  #endregion
}

Nyní je již při klonování vytvořena i nová instance třídy ZapouzdrenaHodnota.

public class MojeKlonovatelnaTridaPriklad
{
  public static void Priklad()
  {
    MojeKlonovatelnaTrida lMujObjekt = new MojeKlonovatelnaTrida(new ZapouzdrenaHodnota(5));
    MojeKlonovatelnaTrida lMujObjekt2 = (MojeKlonovatelnaTrida) lMujObjekt.Clone();
    lMujObjekt.Cislo.Hodnota = 11;
    Console.WriteLine(lMujObjekt2.Cislo.Hodnota);
  }
}

Takže po spuštění tohoto příkladu uvidíme na výstupu očekávané číslo 5.

Zdrojové kódy k příkladům jsou k dispozici zde.

Diskuze (4) Další článek: ATi uvádí Radeon X550

Témata článku: Software, Microsoft, Programování, Public, Klonování, Hash, LTO, Díl, Private, Microsoft NET, Kody, Nová instance, Klon


Určitě si přečtěte


Aktuální číslo časopisu Computer

Megatest mobilů do 8 000 Kč

Test bezdrátových headsetů

Linux i pro začátečníky

Jak surfovat anonymně