0

i am doing an app to manage the creation of role-playing sessions, but i am having problems with the section to do rules summaries so the Master doesnt have to be reading the core book every sec, i have the data structures in this way.

User have a list of campaigns, that campaign have a list of scenaries and that scenaries have a list of adventures.

Users -> Lcampaings -> Lscenaries -> Ladventures

Each campaign, scenary or adventure, have resources which contains the list of documents, images, resources etc, and a hashset of summaries.

Campaign/Scenary/Adventure -> Resources -> Ldocuments/LImages/.../HashSet Summaries

ok, so to modify the summaries i have implemented equality and gethashcode

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Windows;

namespace ElEscribaDelDJ.Classes
{
    public class Resumenes: INotifyPropertyChanged, IEqualityComparer<Resumenes>
    {

        private string _nombre;

        public string Nombre
        {
            get { return _nombre; }
            set { 
                _nombre = value;
                OnPropertyChanged("Nombre");
            }
        }

        private string _etiquetas;

        public string Etiquetas
        {
            get { return _etiquetas; }
            set { 
                _etiquetas = value;
                OnPropertyChanged("Etiquetas");
            }
        }

        private string _descripcion;

        public string Descripcion
        {
            get { return _descripcion; }
            set { 
                _descripcion = value;
                OnPropertyChanged("Descripcion");
            }
        }

        private int _pagina;

        public int Pagina
        {
            get { return _pagina; }
            set {
                if (value <= 0) 
                    _pagina = 1;
                else
                    _pagina = value;
                OnPropertyChanged("Pagina");
            }
        }

        private string _manual;

        public string Manual
        {
            get { return _manual; }
            set { 
                _manual = value;
                OnPropertyChanged("Manual");
            }
        }

        private string _manualurl;

        public string ManualUrl
        {
            get { return _manualurl; }
            set
            {
                _manualurl = value;
                OnPropertyChanged("ManualUrl");
            }
        }

        private string _tipoaventura;

        public string TipoAventura
        {
            get { return _tipoaventura; }
            set { 
                _tipoaventura = value;
                OnPropertyChanged("TipoAventura");
            }
        }

        private string _nombretipoaventura;

        public string NombreTipoAventura
        {
            get { return _nombretipoaventura; }
            set {
                _nombretipoaventura = value;
                OnPropertyChanged("NombreTipoAventura");
            }
        }


        private int _indice;

        public int Indice
        {
            get { return _indice; }
            set
            {
                _indice = value;
                OnPropertyChanged("Indice");
            }
        }

        private List<int> _indiceslibres;

        public List<int> IndicesLibres
        {
            get { return _indiceslibres; }
            set
            {
                _indiceslibres = value;
                OnPropertyChanged("IndicesLibres");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string PropertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
        }

        public bool Equals(Resumenes x, Resumenes y)
        {
            if (x.Nombre.Equals(y.Nombre) && x.Descripcion.Equals(y.Descripcion))
                return true;
            else
                return false;
        }

        public int GetHashCode(Resumenes obj)
        {
            MessageBox.Show("El Hash code es " + obj.Nombre.GetHashCode());
            return obj.Nombre.GetHashCode();
        }

        public Resumenes CopiarValores ()
        {
            return (Resumenes)this.MemberwiseClone();
        }
    }
}

(In gethashcode i have the messagebox just to know if was called ofc i know it shouldnt be there)

I am using the name and description of two objects to know if they are equally or not, and for gethashcode the name.

I have done this after read a lot of questions about how it works hashcode and equallity, hashcodeA == hashcodeB means they could be equally so name looks like perfect for that and thats why in equallity i use also description, because if you have same name and same description its mostly the same summary.

Ok, so i show a list of all summaries, the user select one, click edit, in the windows for add or edit i do a not in deep copy of the objects and after that i call for example campaign edit summary, where i delete the old object and add the new one, because i readed that's the best way if you have modified the fields used to make the hashcode.

public void EditarResumen(Resumenes resumenviejo, Resumenes resumennuevo)
        {
            DatosAplicacion.CampanaSeleccionada.Recursos.Resumenes.Remove(resumenviejo);
            DatosAplicacion.CampanaSeleccionada.Recursos.Resumenes.Add(resumennuevo);
            RecursosAplicacion.SesionUsuario.ReemplazarCampana();
        }

"Datosaplicacion" is a static class which have the campaign, scenary and aventure that the users chose from all of them

using System;
using System.Collections.Generic;
using System.Text;

namespace ElEscribaDelDJ.Classes.Utilidades.Aplicacion
{
    public static class DatosAplicacion
    {
        private static Campana _campana = new Campana();

        public static Campana CampanaSeleccionada
        {
            get { return _campana; }
            set { _campana = value; }
        }

        public static int IndiceCampana;

        private static EscenarioCampana _escenarioseleccionado = new EscenarioCampana();

        public static EscenarioCampana EscenarioSeleccionado
        {
            get { return _escenarioseleccionado; }
            set { _escenarioseleccionado = value; }
        }

        public static int IndiceEscenario;

        private static Aventura _aventuraseleccionada;

        public static Aventura AventuraSeleccionada
        {
            get { return _aventuraseleccionada; }
            set { _aventuraseleccionada = value; }
        }

        public static int IndiceAventuraSeleccionada;
    }
}

resumenviejo (oldsummary) is made with

public Resumenes CopiarValores ()
        {
            return (Resumenes)this.MemberwiseClone();
        }

this should be fine because i dont have any reference object or similar.

But when i debugg the application the remove operation always throw false, and never calls the equality operation neither the gethashcode.

And i don't know what is happening.

I used this article to do the operations https://dotnetcodr.com/2017/01/12/how-to-check-whether-two-hashsets-are-equal-in-c-net-2/#:~:text=Two%20HashSet%20objects%20in%20C#,their%20order%20in%20the%20collection.

I have the full code uploaded to github https://github.com/davidgmd/Proyecto-de-fin-de-grado

  • Are you *changing* the `Nombre` at all in this scenario? it is assumed that hashcodes are effectively immutable (at least while being used in a hash/dictionary/etc) – Marc Gravell May 24 '21 at 11:22
  • It could be changed, should i do it absolute inmutable? – David Pinedo Solano May 24 '21 at 11:24
  • If you change an object's field used to compute a hash code after you have inserted that object into a HashSet, that HashSet will likely now be in a broken state. – Matthew Watson May 24 '21 at 11:26
  • Put a break point into the GetHashCode to see if you reach the method. You can for testing return a constant value to see if that works. The GetHashCode is a first test to see if values are equal and when two keys are equal then the Equals method is called. Testing is best way of finding issue. – jdweng May 24 '21 at 11:26
  • @MatthewWatson thats why when i edit one summary i'm trying to delete the old one and add it again with the new value, is this not enought to have a right hash code?. – David Pinedo Solano May 24 '21 at 11:37
  • Yes, honestly: if you're using it as a hash key, `Nombre` should probably be readonly; and I know it is deleted now, but @derpirscher's answer has a lot of wisdom: it is a good idea to also override the default `Equals` and `GetHashCode` methods; honestly, it is very odd to have your *leaf type* also implement `IEqualityComparer` - `IEquatable` would be fine, but not the comparer; can we see how you create the hash-set? did you pass *in* a comparer? – Marc Gravell May 24 '21 at 11:37
  • @jdweng the break point is not working, neither return for example 1, so gethashcode and equality are not being called at all – David Pinedo Solano May 24 '21 at 11:37
  • @MarcGravell i declare the hashset like private HashSet _resumenes = new HashSet(); public HashSet Resumenes { get { return _resumenes; } set { _resumenes = value; } } And i add elements with public void AnadirResumen(Resumenes resumen) { DatosAplicacion.CampanaSeleccionada.Recursos.Resumenes.Add(resumen); RecursosAplicacion.SesionUsuario.ReemplazarCampana(); } in other wors i use the by default definition, and just add the elements using the add – David Pinedo Solano May 24 '21 at 11:40
  • @DavidPinedoSolano that's the problem then; see my answer for more details; you're implementing the *comparer* API, but never using that anywhere. – Marc Gravell May 24 '21 at 11:45

2 Answers2

6

You have two methods GetHashCode and Equals

public bool Equals(Resumenes x, Resumenes y)

public int GetHashCode(Resumenes obj)

But they are not overriding the correct methods from the framework so they won't be called. You have to override the following to methods, so that they will be used by the framework

public override bool Equals(object obj) {
  if (!(obj is Resumenes)) return false;
  var other = obj as Resumenes;
  return this.Nombre.Equals(other.Nombre) && this.Descripcion.Equals(other.Descripcion);
}

public override int GetHashCode() {
   return this.Nombre.GetHashCode();
}

Note, that this is not really needed. It's just to clarify that this instance is compared with the other object passed in.

EDIT

You can use your overriding of IEqualityComparer<Resumenes> but then you will have to pass it to the constructor of the hashset. But it's quite uncommon for the data object you put into a HashSet to implement IEqualityComparer. Better your Resumenes should implement the IEquatable<T> interface

public class Resumenes: INotifyPropertyChanged, IEquatable<Resumenes> {


    public override bool Equals(object obj) { ... }
    public bool Equals(Resumenes other) { ... }
    public override int GetHashCode() { ... }
}

derpirscher
  • 14,418
  • 3
  • 18
  • 35
  • Actually by default `HashSet` uses [`IEqualityComparer`](https://referencesource.microsoft.com/#mscorlib/system/collections/generic/iequalitycomparer.cs,66a06cfe895400c7) for its comparisons. – Matthew Watson May 24 '21 at 11:29
  • @MatthewWatson Yes. But if you don't specify which `IEqualityComparer` to use with the constructor of the hashset, it uses `EqualityComparer.Default`, which calls `T::Equals(obj)` and `T::GetHashcode()` by default. https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.equalitycomparer-1.default?redirectedfrom=MSDN&view=net-5.0#remarks – derpirscher May 24 '21 at 11:43
  • When i write override "public override bool Equals(Resumenes x, Resumenes y)" it says he doesnt find a right member to override, the same with gethashcode – David Pinedo Solano May 24 '21 at 11:49
  • @DavidPinedoSolano You should be overriding the versions that derpirscher mentions in their answer. – Matthew Watson May 24 '21 at 11:55
  • @derpirscher Good point - this is the correct answer, I think. – Matthew Watson May 24 '21 at 11:56
3

There are a few things here:

  1. since Nombre is effectively the hash-key, if it changes while the item is in the hash: all bets are off; a simple way to avoid that is to make it read-only
  2. it is very odd to have a leaf type implement IEqualityComparer<T>; I wonder if this is a large part of the problem - especially if you haven't passed a explicit comparer into the hash-set; however, honestly, it would be simpler and preferable to implement IEquatable<T> here:
public class Resumenes : INotifyPropertyChanged, IEquatable<Resumenes>
{
        // ...
        public override bool Equals(object obj) => obj is Resumenes other && Equals(other);
        public bool Equals(Resumenes other)
            => other is not null && other.Nombre == this.Nombre && other.Descripcion == this.Descripcion;

        public override int GetHashCode()
            => Nombre.GetHashCode();
}

You can do this with a custom equality comparer, but you'd need to explicitly pass such a comparer into the new HashSet<Resumenes>(comparer) constructor. I would expect this comparer to be a singleton instance of a different type, for example ResumenesComparer.Instance. Using IEquatable<T> is far more obvious and convenient.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900