Pedro Costa
Pedro Costa

Reputation: 49

Getting duplicated entries of interfaces in a Dictionary

I have an interface named IResource that consist of 5 properties as readonly. Since I am adding those to a Dictionary<IResource, int>, I need a way to compare two IResource values so I don't have duplicates in the Dictionary.

Is there a way for me to add a default Equals(object obj) to every IResource?

I already added an Equals override to the Wood class and it solved the problem, but I would have to add a Equals(object obj) in every class that implements IResource.

public class Wood : IResource
{
    public string Package => "Core";

    public string Family => "Wood";

    public string Name => "Wood";

    public bool IsFractal => false;

    public ResourceType Type => ResourceType.Natural;
}

PS: I have an override of the Add(IResource key, uint value) method to the dictionary to check if the IResource already exists.

    public new void Add(IResource key, uint value)
    {
        if (base.ContainsKey(key))
            base[key] += value;
        else
            base.Add(key, value);
    }

Right now when I add a IResource interface to the dictionary, it always adds a new entry.

Upvotes: 0

Views: 88

Answers (3)

MikeJ
MikeJ

Reputation: 1379

While it's certainly possible to create an abstract base class - as others have pointed out - it's really not a great idea. You're creating a dependency that any class that implements IResource must also implement equality as you've defined it for IResource. And that might be fine or it might make it hard to maintain and lead to bugs.

The framework is designed to handle this situation by allowing you to customize how the dictionary does comparisons. It does this by using IEqualityComparer.

Here's an example for your IResource interface:

public class ResourceComparer : IEqualityComparer<IResource>
{
    public bool Equals([AllowNull] IResource x, [AllowNull] IResource y)
    {
        if (null == x && null == y)
            return true;

        if (null == x || null == y)
            return false;

        return x.Package.Equals(y.Package) &&
            x.Family.Equals(y.Family) &&
            x.Name.Equals(y.Name) &&
            x.IsFractal.Equals(y.IsFractal) &&
            x.Type.Equals(y.Type);
    }

    public int GetHashCode([DisallowNull] IResource obj)
    {
        HashCode hash = new HashCode();
        hash.Add(obj.Package);
        hash.Add(obj.Family);
        hash.Add(obj.Name);
        hash.Add(obj.IsFractal);
        hash.Add(obj.Type);
        return hash.ToHashCode();
    }
}

Once you've got that then you can create your dictionary with that comparer. I've used your Wood class and created one other called Metal. Neither has to share a base class or override Equals and GetHashCode.

    static void Main(string[] _)
    {
        var resourceMap = new Dictionary<IResource,uint>(new ResourceComparer());

        var resources = new IResource[] { new Wood(), new Metal(), 
                                         new Wood(), new Wood() };

        foreach (var r in resources)
        {
            if (resourceMap.TryGetValue(r, out var count))
                resourceMap[r] = count + 1;
            else
                resourceMap.Add(r, 1);
        }

        Console.WriteLine(resourceMap[new Wood()]);
        Console.WriteLine(resourceMap[new Metal()]);
    }

Here's the simple POCO style metal class:

public class Metal : IResource
{
    public string Package => "Core";

    public string Family => "Metal";

    public string Name => "Metal";

    public bool IsFractal => false;

    public ResourceType Type => ResourceType.ManMade;
}

Upvotes: 1

JohanP
JohanP

Reputation: 5472

You can create an abstract class that implements IResource. Use that class and override Equals and GetHashCode.

public abstract class Resource : IResource
{
    //make all your interface properties abstract
    public abstract string Package { get; }
    public abstract string Family { get; }
    public abstract string Name { get; }
    public abstract bool IsFractal { get; }
    public abstract ResourceType Type { get; }

    public override bool Equals(object obj)
    {
        if (!(obj is Resource resource)) return false;
        return ReferenceEquals(this, resource) ||
               Package == resource.Package &&
               Family == resource.Family &&
               Name == resource.Family &&
               IsFractal == resource.IsFractal &&
               Type == resource.Type;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Package, Family, Name, IsFractal, Type);
    }
}

Then make all your resources implement the abstract class Resource

public class Wood : Resource
{
    public override string Package => "Core";

    public override string Family => "Wood";

    public override string Name => "Wood";

    public override bool IsFractal => false;

    public override ResourceType Type => ResourceType.Natural;
}

public class Rock : Resource
{
    public override string Package => "Core";

    public override string Family => "Rock";

    public override string Name => "Rock";

    public override bool IsFractal => false;

    public override ResourceType Type => ResourceType.Natural;
}

This will give you the behavior you expect.

Upvotes: 1

Jerry
Jerry

Reputation: 1527

You can move your comparison to a base class and override Equals and GetHashCode there. Just add any members you want to use in the comparison to the abstract class and include them in the equity comparison.

For example:

public enum ResourceType { Natural }

public interface IResource
{
    public string Name { get; }
    public ResourceType ResourceType { get; }
}

public abstract class Resource
{
    public abstract string Name { get; }
    public abstract ResourceType ResourceType { get; }
    // other properties that you want to use for your resource comparision

    public override bool Equals(object obj)
        => obj is Resource r && Name == r.Name && ResourceType == r.ResourceType;

    public override int GetHashCode() => (Name, ResourceType).GetHashCode();
}

public class Wood : Resource, IResource
{
    public override string Name => "Wood";
    public override ResourceType ResourceType => ResourceType.Natural;
    // ... other properties
}

Upvotes: 2

Related Questions