1

I am trying to use some code that will generate hashcode based on the value of all properties inside an object, but the following returns a 0 for the HashCodeOnProperties

Console.WriteLine("Hello, World!");

var request = new Request()
{

    NorthEastLatitude = 43.13306116240615,
    NorthEastLongitude = -80.9355926513672,
    NorthWestLatitude = 43.13306116240615,
    NorthWestLongitude = -81.573486328125014,
    SouthEastLatitude = 42.831667202614092,
    SouthEastLongitude = -80.9355926513672 ,
    SouthWestLatitude = 42.831667202614092,
    SouthWestLongitude = -81.573486328125014
};

var hash = request.GetHashCodeOnProperties();
Console.WriteLine(hash);

Console.ReadKey();


public class Request
{
    public double? SouthWestLatitude { get; set; }
    public double? SouthWestLongitude { get; set; }
    public double? NorthEastLatitude { get; set; }
    public double? NorthEastLongitude { get; set; }


    public double? SouthEastLatitude { get; set; }
    public double? SouthEastLongitude { get; set; }
    public double? NorthWestLatitude { get; set; }
    public double? NorthWestLongitude { get; set; }

}
public static class HashCodeByPropertyExtensions
{
    public static int GetHashCodeOnProperties<T>(this T inspect)
    {
        return inspect.GetType().GetProperties().Select(o => o.GetValue(inspect)).GetListHashCode();
    }

    public static int GetListHashCode<T>(this IEnumerable<T> sequence)
    {
        return sequence
            .Where(item => item != null)
            .Select(item => item.GetHashCode())
            .Aggregate((total, nextCode) => total ^ nextCode);
    }
}
Zoinky
  • 4,083
  • 11
  • 40
  • 78
  • 1
    Well, have you debugged through the code to see what's happening? (Even just adding `.Select(hash => { Console.WriteLine($"Partial hash: {hash}"); return hash; }` would give you more information...) – Jon Skeet Apr 13 '23 at 11:10
  • 4
    But fundamentally, your hash combination code means that if you get the same hash code for two properties, they'll cancel each other out (due to XOR). Now look at the values you've provided in your Request object... – Jon Skeet Apr 13 '23 at 11:11
  • Consider using [`HashCode`](https://learn.microsoft.com/dotnet/api/system.hashcode) for scenarios like these; `HashCode.Combine` goes well with `.Aggregate`. You'll also want to take a look at `record`s, as these provide hash code implementations "for free". – Jeroen Mostert Apr 13 '23 at 11:27
  • Consider using the [`HashCode` struct](https://learn.microsoft.com/en-us/dotnet/api/system.hashcode) (unless you are on .NET Framework). [Edit: Ah, Jeroen Mostert wrote that while I was typing.] But still interesting to hear the explanation. But as Jon Skeet says, it depends on the value. One thing I see, is the overload of `Aggregate` you use will throw if all the properties have null values. This includes, of course, the case where there are no properties. – Jeppe Stig Nielsen Apr 13 '23 at 11:27
  • btw, records override `GetHashCode()` and would give you this functionality out of the box: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records – Christoph Lütjen Apr 13 '23 at 11:33

3 Answers3

2

You are xoring values without "offset", so this will result in same values canceling each other out (exclusive or is associative and commutative so 42 ^ 77 ^ 42 ^ 77 is equivalent to 42 ^ 42 ^ 77 ^ 77 which is clearly is 0). You can do something like:

public static int GetListHashCode<T>(this IEnumerable<T> sequence)
{
    var hash = 17;
    return sequence
        .Where(item => item != null)
        .Select(item => item.GetHashCode())
        .Aggregate(hash, (total, nextCode) => unchecked(total*23 + nextCode));
}

Or use System.HashCode (available since .NET Core 2.1) to perform aggregation:

public static int GetListHashCode<T>(this IEnumerable<T> sequence)
{
    var hashCode = new HashCode();
    return sequence
        .Where(item => item != null)
        .Select(item => item.GetHashCode())
        .Aggregate(new HashCode(), (code, i) =>
        {
             hashCode.Add(i);
             return hashCode;
        })
        .ToHashCode();
}

P.S.

Calculating hashcode should be fast operation, while reflection is usually not very fast approach. Consider using source generators or some dynamic code compilation with expression trees (see for example this or this answers for some inspiration) or switching to using record's which have equality members autogenerated.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
1

Just a comment with images. If your Request class is supposed to always represent an annulus sector of the Earth whose boundaries follow lines of longitude and latitude, then you need only store coordinates for two diametrically opposite corners.

Polar Earth map with "annulus sector" drawn on top

Another sector which shares "vertical" borders

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
1

@GuruStron has the correct answer, I just want to include a more robust aggregate hash code function based on code generated by VS.

public static int GetListHashCode<T>(this IEnumerable<T> sequence)
{
    unchecked
    {
        return sequence.Where(item => item != null)
          .Aggregate(-1817952719, 
            (hc, item) => (-1521134295) * hc + item.GetHashCode()); 
    }
}

The values of -1817952719 and -1521134295 are just large fancy numbers to replace 17 and 23 in order to reduce changes for a collision. Also enclosing the operation in unchecked is required to avoid any integer overflow exceptions.

--

I got the values from the suggested code when I issue a generate GetHashCode() command in Visual Studio.

fig1

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
John Alexiou
  • 28,472
  • 11
  • 77
  • 133
  • Numbers `1817952719` and `1521134295` are not primes in the strict mathematical sense, but since they are relatively prime to `Pow(2, 32)`, they are good enough. – Jeppe Stig Nielsen Apr 13 '23 at 11:58
  • @JeppeStigNielsen - the negative sign is important. Cast to unsigned int to get the actual number used internally. They transform to `2477014577` and `2773833001`. But alas, they are not primes either. – John Alexiou Apr 13 '23 at 12:02
  • But `-1817952719 + Pow(2, 32)` is also not a mathematical prime (being `16363 * 151379`), so why do people call such numbers "prime"? – Jeppe Stig Nielsen Apr 13 '23 at 12:06
  • @JeppeStigNielsen - ok I updated my answer not calling them primes anymore. Thank you. – John Alexiou Apr 13 '23 at 12:09