0

I am using a neat little HashCode utility with some instance methods that help quickly generate Hash values. I've simplified it for this question, but it's basically this:

public struct HashCode
{
    private readonly int _hashCode;
    public HashCode(int hashCode) { _hashCode = hashCode; }
    override int GetHashCode() => _hashCode;

    public static HashCode Start => new HashCode(17);    
    public HashCode Hash(int integer) => new HashCode(unchecked(integer + hashCode * 31));    
    public HashCode Hash<T>(T value) where T : struct => Hash(value.GetHashCode());    
    public HashCode Hash(string value) => Hash(value?.GetHashCode() ?? 17);
    public HashCode Hash<T>(T? nullable) where T : struct =>
        Hash(nullable?.GetHashCode() ?? 17);
    // Other reference types, user must explicitly specify a comparer
    public HashCode Hash<T>(T obj, IEqualityComparer<T> comparer) =>
        Hash(obj == null ? 17: comparer.GetHashCode(obj));  

    public static implicit operator int(HashCode hashCode) => hashCode.GetHashCode();
}

Allowing for slick hash implementations like:

class Person {
    // ...
    override GetHashCode() => HashCode.Start.Hash(name).Hash(age).Hash(...);
}

As you can see, it goes out of its way to avoid boxing, etc. If you're hashing a reference type directly, you must specify a comparer to make sure you know how it's being hashed. Seems reasonable


Now, I was hoping to add some extension methods in one of my projects (i.e. without duplicating & modifying the library) so that I could easily add consice hash functions some of my own frequently used types, and employ them using the exact same syntax:

public static class HashCode_Extensions
{
    public static HashCode Hash(this HashCode hc, DateRange range) =>
        hc.Hash(range?.begin).Hash(range?.end);

    public static HashCode Hash(this HashCode hc, IEnumerable<T> list) where T : struct =>
        list.Aggregate(hc, (hc, elem) => hc.Hash(elem));

    // etc...
}

I thought I was so clever and tons of copy-pasted code was going to disappear.

class Meeting {
    // ...
    override GetHashCode() => HashCode.Start.Hash(name).Hash(dateRange).Hash(invitees);
}

Unfortunately, the compiler is choosing the generic instance method over my own, even though my method is a perfect fit and the generic method has a type constraint that is not

CS0453 The type 'DateRange' must be a non-nullable value type in order to use it as parameter 'T' in the generic type of method 'HashCode.Hash(T)'

Obviously the compiler has decided that the "best overload" of Hash is the one with the non-satisfied generic type constraint. Too bad, because my extension method would have been a perfect fit.


Is there any way I can trick the compiler into using the correct method without having to resort to using a different function name or include 'dummy' arguments in my call to Hash?

I'd also be happy if there is some backwards-compatible way for me to change the HashCode library that would allow my extension methods to be noticed. (I'd rather not add my custom overloads to the base library, as those custom types don't currently exist in its namespace)

Alain
  • 26,663
  • 20
  • 114
  • 184
  • Generic type constaints are not considered when finding valid overload targets. They are only used afterwards when validating whether the actual found method can be used or not. – Lasse V. Karlsen May 16 '18 at 22:57
  • 1
    Does https://stackoverflow.com/questions/2974519/generic-constraints-where-t-struct-and-where-t-class/36775837#36775837 help? – mjwills May 16 '18 at 22:59

1 Answers1

0

A little trick will work. Create a class like this:

public class RequireStruct<T> where T : struct { }

Put that as an optional parameter in your generic method as shown below:

public struct HashCode
{
    private readonly int _hashCode;
    public HashCode Hash<T>(T value, RequireStruct<T> ignore = null) where T : struct => Hash(value.GetHashCode());

}

And now when you do this, it will choose your extension method like you want it to:

HashCode code = new HashCode();
code.Hash(new DateRange());

Stolen from here.

CodingYoshi
  • 25,467
  • 4
  • 62
  • 64
  • I hate it, but it works! Thanks @mjwills for suggesting it, and CodingYoshi for catering it to my question. – Alain May 17 '18 at 01:19