Why does VS 2017 generate GetHashCode without an unchecked block
Asked Answered
V

2

5

I recently discovered that Visual Studio 2017 can auto-generate overrides for Equals and GetHashCode, but I was wondering why the GetHashCode implementation is not in an unchecked block?

I made a simple class with two public string properties Foo and Bar, and the generated GetHashCode implementation is shown below.

public override int GetHashCode()
{
    var hashCode = -504981047;
    hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Foo);
    hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Bar);
    return hashCode;
}

I was under the impression the unchecked for GetHashCode implementations was important because it was very likely to overflow, and we don't want any overflow exceptions because it's fine if it wraps around.

Vainglory answered 15/2, 2019 at 15:40 Comment(4)
It should be, but sadly C# project templates do not turn on overflow checking so they didn't consider it.Espinoza
@HansPassant your comment lead to check how to turn on overflow checking for a C# project, and if overflow checking is turned on for the project, then the override of GetHashCode is put in an unchecked block.Vainglory
That's pretty clever. Bummer if you decide to turn it on afterwards though.Espinoza
@HansPassant it would have been better if it was always in an unchecked block just for that scenario.Vainglory
V
6

By default C# projects do not check for overflow and underflow.

Right click on the project, Select Properties, On the Build Tab at the bottom select Advanced..., Check the box labeled Check for arithmetic overflow/underflow

Now the default behavior is to throw System.OverflowException if there is ever an overflow not in an explicit unchecked block.

If you auto-generate overrides for Equals and GetHashCode with overflow checking turned on for the project, then the unchecked block is there as expected

public override int GetHashCode()
{
    unchecked
    {
        var hashCode = -504981047;
        hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Foo);
        hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Bar);
        return hashCode;
    }
}
Vainglory answered 15/2, 2019 at 16:17 Comment(0)
V
2

Apparently my understanding of no checks vs unchecked vs checked was flawed. It was simple enough to write some simple tests to see overflow behavior in this fiddle.

The quick summary is this:

If running with no explicit checks

  • If the compiler can easily and statically determine that code will overflow, there will be a compile error.
  • If there is an overflow at runtime there will not be any overflow exceptions thrown.

If running explicitly unchecked

  • The compiler will allow code that will obviously overflow
  • No runtime overflow exceptions will be thrown

If running explicitly checked

  • If the compiler can easily and statically determine that code will overflow, there will be a compile error.
  • If there is an overflow at runtime a System.OverflowException will be thrown.

So... I guess the lesson from all of this is if you have some calculations that may overflow, and you care about overflow, it is very important to put that inside of a checked block. If you have code that may overflow, and you don't care about overflow, apparently you can skip the unchecked block (unless your code will obviously overflow from a static analysis point of view).

The code from the fiddle is copied here as well for posterity.

using System;

public class Program
{
    public static void Main()
    {
        var rand = new Random();
        int test = 0;

        //obscured enough that the compiler doesn't "know" that the line will produce an overflow
        //Does not run explicitly as checked, so no runtime OverflowException is thrown
        test = rand.Next(Int32.MaxValue-2, Int32.MaxValue) + 10;

        //simple enough that the compiler "knows" that the line will produce an overflow
        //Compilation error (line 16, col 10): The operation overflows at compile time in checked mode
        //test = Int32.MaxValue + 1;

        //Explicitly running as unchecked. Compiler allows line that is "known" to overflow.
        unchecked
        {
            test = Int32.MaxValue + 1;
        }

        Console.WriteLine(test);

        //Explicitly running as unchecked. Still no runtime OverflowException
        unchecked
        {
            test = test - 10;   
        }

        Console.WriteLine(test);

        //Explicitly running as checked. System.OverflowException: Arithmetic operation resulted in an overflow.
        checked
        {
            test = test + 10;
        }

        Console.WriteLine(test);
    }
}
Vainglory answered 15/2, 2019 at 15:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.