3

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?

Hemi81
  • 578
  • 2
  • 15
  • 34
monsieurgutix
  • 137
  • 1
  • 7
  • The question is quite confusing; why are you not simply using `==` to determine if two strings are equal? Or, if you like using a method, the static `String.Equals(s1, s2)` method? – Eric Lippert Sep 01 '17 at 18:47
  • @EricLippert I know that for strings the overload for the operator == considers null instances, I checked also using my unit tests System.Type and Dictionary and the == operator handles nulls properly! Does the operator == for reference types always check null as well? – monsieurgutix Sep 01 '17 at 19:54
  • @monsiergutix: Yes `==` on reference types correctly checks for null references. That said, if you have overloaded `==` on a reference type then *you* are responsible for implementing it correctly for null operands. – Eric Lippert Sep 01 '17 at 20:01
  • @EricLippert Thanks! So just changing all the equalities using the IEquatable Interface for the operator == will make the trick for the NullPointerException. That being solved, why does GetHashCode() for the struct MethodSignature does not complain if the reference types within are defaulted to null? as far as I understand, the GetHashCode does not check for nulls.... – monsieurgutix Sep 01 '17 at 20:08
  • @monsiergutix: Apparently your belief is incorrect. Why do you believe that the default `GetHashCode` doesn't know about nulls? – Eric Lippert Sep 01 '17 at 21:42
  • Leaving that aside, I note that your final paragraphs are very important; you are *required* to implement your own `GetHashCode` such that `x.Equals(y))` implies that `x.GetHashCode().Equals(y.GetHashCode())` and you have not done so. – Eric Lippert Sep 01 '17 at 21:45
  • Also, your program is wrong in other ways. Suppose we have `class C { static void M(int x, string y) { } static void M(string y, int x) { }`. Your code says that the method signatures of both methods are the same, but plainly they are not. – Eric Lippert Sep 01 '17 at 21:47
  • Your code also says that `class C { static void M() {} static void M() {} }` have the same signature when plainly they do not. – Eric Lippert Sep 01 '17 at 21:47
  • I suspect that you might have some misconceptions about how GetHashCode works and what you are required to do; my article on the subject might be helpful. https://blogs.msdn.microsoft.com/ericlippert/2011/02/28/guidelines-and-rules-for-gethashcode/ – Eric Lippert Sep 01 '17 at 21:53
  • Also consider that the methods in `class C { static void M() {}} class C { static void M() {} }` also have the same class name, method name, and parameters. You need to take generic arity into consideration at both the class and method level. – Eric Lippert Sep 01 '17 at 21:57
  • Also don't forget that classes can be nested. In `class C { class D { static void M() {} } } class D { static void M() { } }` the two methods have the same class name, method name and parameters, but are plainly different methods. – Eric Lippert Sep 01 '17 at 21:58
  • @EricLippert you are absolutely right, I must check these cases too. Actually, this class is part of a custom solution for the `NotMethodFound Exception ` thrown by Webforms. When Dynamic compilation is _ON_, and a method signature changes in a referenced assembly, the aspx page still references the old signature if the page was already compiled and cached. The idea is to erase only the catched pages with method signature changes. – monsieurgutix Sep 02 '17 at 04:20
  • This will require us to decompile the assemblies and navigate through the IL in order to extract the method signatures used by the aspx page. This will be our next step :) – monsieurgutix Sep 02 '17 at 04:25
  • [Here](https://msdn.microsoft.com/en-us/library/ms366723.aspx#Anchor_5) a link that explains the expected exceptions when using dynamic compilation – monsieurgutix Sep 02 '17 at 04:31

1 Answers1

2

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.

Johan Donne
  • 3,104
  • 14
  • 21
  • 3
    I would suggest replacing `if((parameters == null) && (Parameters == null)) return true;` with `if (ReferenceEquals(parameters, Parameters)) return true;`. This not only handles the "both null" case, it also handles the "non-null reference equals" case. If the references are equal then *every other test for equality will succeed*, so there's no point in doing them; just take the early out. – Eric Lippert Sep 01 '17 at 19:59
  • @JohanDonne many thanks for the suggested GetHashCode implementation! – monsieurgutix Sep 02 '17 at 03:34