Equality and gethashcode override for c# structs with reference type members after calling parameterless constructor

937 views Asked by At

I am designing a struct to compare method signatures from two different sources (currently taking them directly from assemblies using System.Reflection). Since I only care about uniqueness, I chose HashSet< MethodSignature> to store my structs and compare them using the subset method.

public struct MethodSignature : IEquatable<MethodSignature>
{
    #region Immutable fields
    public readonly string AssemblyName;
    public readonly string ClassName;
    public readonly string MethodName;
    public readonly System.Type ReturnType;
    public readonly Dictionary<string, System.Type> Parameters;
    #endregion

    #region Constructors
    public MethodSignature(string assemblyName, string className, string methodName, Type returnType, Dictionary<string, System.Type> parameters)
    {
        AssemblyName = assemblyName;
        ClassName = className;
        MethodName = methodName;
        ReturnType = returnType;
        Parameters = parameters;
    }
    #endregion

    #region public Methods
    public override string ToString()
    {
        string paramts = GetParametersAsString();
        return string.Format("{0} {1}::{2}.{3}({4})", ReturnType.ToString(), AssemblyName, ClassName, MethodName, paramts);
    }
    public static bool operator ==(MethodSignature signature1, MethodSignature signature2)
    {
        // No nasty null checking thanks to value types :D :D :D
        return signature1.Equals(signature2);
    }
    public static bool operator !=(MethodSignature signature1, MethodSignature signature2)
    {
        // No nasty null checking thanks to value types :D :D :D
        return !signature1.Equals(signature2);
    }
    public bool Equals(MethodSignature signature)
    {
        return AreMethodSignatureEquals(signature);
    }
    public override bool Equals(object obj)
    {
        if (obj is MethodSignature)
            return Equals((MethodSignature)obj);
        else
            return false;
    }
    #endregion

    #region private Members
    private string GetParametersAsString()
    {
        StringBuilder sb = new StringBuilder();
        foreach (KeyValuePair<string, System.Type> param in Parameters)
        {
            sb.Append(string.Format("{0} {1},", param.Value.ToString(), param.Key.ToString()));
        }

        //Remove trailing comma
        sb.Length--;
        return sb.ToString();
    }

    private bool AreMethodSignatureEquals(MethodSignature signature)
    {
        return (AreAssemblyNamesEqual(signature.AssemblyName)
             && AreClassNameEquals(signature.ClassName)
             && AreMethodNameEquals(signature.MethodName)
             && AreReturnTypeEquals(signature.ReturnType)
             && AreParametersEquals(signature.Parameters));
    }

    private bool AreParametersEquals(Dictionary<string, Type> parameters)
    {
        return parameters.Count == Parameters.Count
            && AreSameSizeDictionariesKeyValuePairsEqual(parameters);
    }

    private bool AreSameSizeDictionariesKeyValuePairsEqual(Dictionary<string, Type> parameters)
    {
        foreach (KeyValuePair<string, Type> param in Parameters)
        {
            Type paramType;

            //TryGetValue returns true if finds the keyValuePair              
            if (parameters.TryGetValue(param.Key, out paramType))
            {
                if (AreParameterTypesDifferent(param.Value, paramType))
                {
                    return false;
                }
            }
            else
            {
                return false;
            }
        }

        return true;
    }

    private static bool AreParameterTypesDifferent(Type typeParameter1, Type typeParameter2)
    {
        return !typeParameter2.Equals(typeParameter1);
    }

    private bool AreReturnTypeEquals(Type returnType)
    {
        return returnType.Equals(ReturnType);
    }

    private bool AreMethodNameEquals(string methodName)
    {
        // Ensuring case sensitive using IEquatable<string>
        return methodName.Equals(MethodName);
    }

    private bool AreClassNameEquals(string className)
    {
        // Ensuring case sensitive using IEquatable<string>
        return className.Equals(ClassName);
    }

    private bool AreAssemblyNamesEqual(string assemblyName)
    {
        // Ensuring case sensitive using IEquatable<string>
        return assemblyName.Equals(AssemblyName);
    }
    #endregion
}

I have checked some implementations for similar types in System.Reflection, however, I do prefer using a custom struct since the Equality is overridden, and also because the default comparison for ValueTypes will compare by reference the dictionary (as it should be for a reference type), this is not desired for my purposes.

The full Equality implementation is ready and works flawlessly (Implemented IEquatable< MethodSignature>, overrode Object.Equals, overloaded == and !=)

But, now I stumped upon an all-zeroed instance of MethodSignature, and its behavior when using equality... Let's take a look

MethodSignature ms1 = new MethodSignature();
MethodSignature ms2 = new MethodSignature();

// This will throw null reference exception
bool areEqual = ms1.Equals(ms2);

The compiler does not complain because the ms1 and ms2 are considered initialized. I do know that this goes down to the fact that all Value Types in C# have by default the parameter less constructor that defaults all its members. If I compare this behavior to a Microsoft provided Value Type

        int a = new int();
        int b = new int();

        // Returns true
        Console.WriteLine(a.Equals(b));

Surely they are equal and comparing both returns of GetHashCode() returns true as well.

I have checked this and this too, however, I cannot figure out how to create a default for every reference type for this struct that complies with the GetHashCode concept (Two objects that are equal return hash codes that are equal. taken from Microsoft )

So finally my question is:

Any idea on how to override GetHashCode() that complies with the IEquatable implementation when there are reference types within a struct while using the default parameterless constructor?

1

There are 1 answers

2
Johan Donne On BEST ANSWER

First of when checking equality of MethodSignature instances created with the default constructor, you will get exceptions due to all the fields being null (they are all reference types). If you want the two instances of

 MethodSignature ms1 = new MethodSignature();
 MethodSignature ms2 = new MethodSignature();

to be considered equal, you should adjust your code as follows:

    private bool AreParametersEquals(Dictionary<string, Type> parameters)
    {   
        if((parameters == null) && (Parameters == null)) return true;
        if((parameters == null) || (Parameters == null)) return false;
        if(parameters.Count != Parameters.Count) return false;

        var paramArray1 = parameters.OrderBy(p => p.Key).ToArray();
        var paramArray2 = Parameters.OrderBy(p => p.Key).ToArray();
        for(int i = 0; i < paramArray1.Length; i++)
        {
            if(!string.Equals(paramArray1[i].Key, paramArray2[i].Key)) return false;
            if(!string.Equals(paramArray1[i].Key, paramArray2[i].Key)) return false;
        }
        return true;
    }

    private bool AreReturnTypeEquals(Type returnType)
    {
        if((returnType == null) && (ReturnType == null)) return true;
        return (returnType != null) && returnType.Equals(ReturnType);
    }

    private bool AreMethodNameEquals(string methodName)
    {
        // Ensuring case sensitive using IEquatable<string>
        return string.Equals(methodName, MethodName);
    }

    private bool AreClassNameEquals(string className)
    {
        // Ensuring case sensitive using IEquatable<string>
        return string.Equals(className, ClassName);
    }

    private bool AreAssemblyNamesEqual(string assemblyName)
    {
        // Ensuring case sensitive using IEquatable<string>
        return string.Equals(assemblyName, AssemblyName);

    }

Furthermore an implementation of GetHashCode that acts the way you want (based on the suggestion of Jon Skeet in What is the best algorithm for an overridden System.Object.GetHashCode?):

public override int GetHashCode()
    {
        unchecked // Overflow is fine, just wrap
        {
            int hash = (int)2166136261;
            // Suitable nullity checks etc, of course :)
            hash = (hash * 16777619) ^ AssemblyName?.GetHashCode()??0;
            hash = (hash * 16777619) ^ ClassName?.GetHashCode()??0;
            hash = (hash * 16777619) ^ MethodName?.GetHashCode()??0;
            hash = (hash * 16777619) ^ ReturnType?.GetHashCode()??0;
            if(Parameters == null) return hash;
            var paramArray = Parameters.OrderBy(p => p.Key).ToArray();
            for(int i = 0; i < Parameters.Count; i++)
            {
                hash = (hash * 16777619) ^ paramArray[i].Key?.GetHashCode()??0;
                hash = (hash * 16777619) ^ paramArray[i].Value?.GetHashCode()??0;
            }
            return hash;
        }
    }

This implementation will work with null values for the fields and return the same result for different instances that have the exact same values in the fields.

Mind: once this hashcode is used (e.g. to store MethodSignature instances in a Dictionary) you should never change the underlying Parameter Dictionary as this will impact the GetHashCode calculation.