James Montemagno
James Montemagno

Live, Love, Bike, and Code.

Tags


Twitter


James Montemagno

Optimizing C# Struct Equality with IEquatable and ValueTuples

James MontemagnoJames Montemagno

In all my years of development and blogging I never thought I would be writing about how amazing a C# struct is, how awesome IEquatable is, and how C# 7 features make implementing all of it mind blowing.

In Xamarin.Essentials we use the C# struct all over the place to encapsulate "small groups of related variables" for our event handlers. They are groups of data that don't need to be created by the developers consuming the data and are only really used for reading the data.

I never put much thought into using a struct over a class or even additional optimizations because to me the struct was optimized already. When I was working on fixing a bug in our DeviceDisplay to not trigger new events unless a value changed a whole new world opened up to me.

Let's take the struct that we were using at the time:

public struct ScreenMetrics
{
    internal ScreenMetrics(double width, double height, double density, ScreenOrientation orientation, ScreenRotation rotation)
    {
        Width = width;
        Height = height;
        Density = density;
        Orientation = orientation;
        Rotation = rotation;
    }

    public double Width { get; set; }
    public double Height { get; set; }
    public double Density { get; set; }
    public ScreenOrientation Orientation { get; set; }
    public ScreenRotation Rotation { get; set; }
}

Mutable vs Immutable

The first issue we see here is that this struct is mutable in that you can actually change the data later on via the set properties. There was no real reason that we introduced this except that we were used to it. Since a developer should never really create one of these objects the setters should be removed and it should be marked as readonly to ensure that it is immutable, which is a pretty slick C# 7.2 feature:

public readonly struct ScreenMetrics
{
    internal ScreenMetrics(double width, double height, double density, ScreenOrientation orientation, ScreenRotation rotation)
    {
        Width = width;
        Height = height;
        Density = density;
        Orientation = orientation;
        Rotation = rotation;
    }

    public double Width { get; }
    public double Height { get; }
    public double Density { get; }
    public ScreenOrientation Orientation { get; }
    public ScreenRotation Rotation { get; }
}

Overriding Equals

Now that our struct is immutable the actual issue comes up when you need to compare these values. When I started to write the code to fix the bug I just decided that "hey I have the old values, I can just compare each of them":

 static void OnScreenMetricsChanged(ScreenMetricsChangedEventArgs e)
{
    if (currentMetrics.Width != e.Metrics.Width ||
        currentMetrics.Height != e.Metrics.Height ||
        currentMetrics.Density != e.Metrics.Density ||
        currentMetrics.Orientation != e.Metrics.Orientation ||
        currentMetrics.Rotation != e.Metrics.Rotation)
    {
        SetCurrent();
        ScreenMetricsChangedInternal?.Invoke(null, e);
    }
}

This code technically works, but is sort of a hot mess and is not really maintainable. Anyone using the library would have to write this code as well. The next logical step would be to just use .Equals on the entire metrics.

Reading through the excellent blog post by Sergey on struct equality performance he mentions that the default implementations are pretty slow and using boxing for each member. Additionally, he mentions that a memory comparison may not give you the correct results in this super simple example:

public struct MyDouble
{
    public double Value { get; }
    public MyDouble(double val) => Value = val;
}

double d1 = -0.0;
double d2 = +0.0;

// True
bool b1 = d1.Equals(d2);

//False
bool b2 = new MyDouble(d1).Equals(new MyDouble(d2));

So, this means we must override Equals!

public override bool Equals(object obj) =>
    (obj is ScreenMetrics metrics) && Equals(metrics);

public bool Equals(ScreenMetrics other) =>
    currentMetrics.Width.Equals(e.Metrics.Width) &&
    currentMetrics.Height.Equals(e.Metrics.Height) &&
    currentMetrics.Density.Equals(e.Metrics.Density) &&
    currentMetrics.Orientation.Equals(e.Metrics.Orientation) &&
    currentMetrics.Rotation.Equals(e.Metrics.Rotation);

Now we know for sure that if we call Equals, that our checks will work! However, we can do even better here with an awesome C# 7.3 feature called Tuple Equality! That is right, you can create a ValueTuple and simply compare them as they are super optimized, don't create any objects, and reduce this to a single line of code!

public bool Equals(ScreenMetrics other) =>
            (Width, Height, Density, Orientation, Rotation) == (other.Width, other.Height, other.Density, other.Orientation, other.Rotation);

Mind blown!!!

What about the HashCode

That is right! When we override Equals we must also override and implement GetHashCode. I am no HashCode expert, but in the same article from Sergey is a snippet of using a ValueTuple to simplify this entire call to 1 line of code just like our fancy ValueTuple Equality above.

public override int GetHashCode() =>
    (Height, Width, Density, Orientation, Rotation).GetHashCode();

This is really amazing code and works great for .NET Standard libraries. If you are in a .NET Core 2.1 application there is an even cooler way of doing this:

public override int GetHashCode() =>
    HashCode.Combine(Height, Width, Density, Orientation, Rotation);

I really hope this makes it to the next versions of .NET Standard as it really is discoverable.

More Optimized with IEquatable

Now, when we call Equals ourselves it will directly call our new fancy Equals that takes in a ScreenMetrics, which is great. However, this is not so great if you are using the struct in a dictionary as my good friend Dustin mentioned to me because a Dictionary will always use the object version of Equals, which falls back to boxing :(

Do not fear because if you simply implement IEquatable<T> the dictionary will use the strongly typed version! The nice thing is that we kind of actually already did this! So now we just have to do this:

public readonly struct ScreenMetrics : IEquatable<ScreenMetrics>
{
    //...
}

What about == and !=

Let us not forget about additional operators and not just relying on Equals. We can implement the == and != operators easily:

public static bool operator ==(ScreenMetrics left, ScreenMetrics right) =>
    Equals(left, right);

public static bool operator !=(ScreenMetrics left, ScreenMetrics right) =>
    !Equals(left, right);

Our Final Beautiful Struct

Here it is in all of it's glory:

public readonly struct ScreenMetrics : IEquatable<ScreenMetrics>
{
    internal ScreenMetrics(double width, double height, double density, ScreenOrientation orientation, ScreenRotation rotation)
    {
        Width = width;
        Height = height;
        Density = density;
        Orientation = orientation;
        Rotation = rotation;
    }
    public double Width { get; }
    public double Height { get; }
    public double Density { get; }
    public ScreenOrientation Orientation { get; }
    public ScreenRotation Rotation { get; }

    public static bool operator ==(ScreenMetrics left, ScreenMetrics right) =>
        Equals(left, right);

    public static bool operator !=(ScreenMetrics left, ScreenMetrics right) =>
        !Equals(left, right);

   public override bool Equals(object obj) =>
       (obj is ScreenMetrics metrics) && Equals(metrics);

    public bool Equals(ScreenMetrics other) =>
        (Width, Height, Density, Orientation, Rotation) == (other.Width, other.Height, other.Density, other.Orientation, other.Rotation);

    public override int GetHashCode() =>
        (Height, Width, Density, Orientation, Rotation).GetHashCode();
}

This also means that all of these cases work:

[Fact]
public void DeviceDisplay_Comparison_Equal()
{
    var device1 = new ScreenMetrics(
        width: 0,
        height: 0,
        density: 0,
        orientation: ScreenOrientation.Landscape,
        rotation: ScreenRotation.Rotation0);

    var device2 = new ScreenMetrics(
        width: 0,
        height: 0,
        density: 0,
        orientation: ScreenOrientation.Landscape,
        rotation: ScreenRotation.Rotation0);

    Assert.True(device1.Equals(device2));
    Assert.True(device1 == device2);
    Assert.False(device1 != device2);
    Assert.Equal(device1, device2);
    Assert.Equal(device1.GetHashCode(), device2.GetHashCode());
}

What if it was automated?

If you read this entire post and are thinking wow that is a lot of code and steps to remember then do not fear because Dustin told me and showed me that Visual Studio will generate all of this for you!!!!! Check this out:

Tune In To Merge Conflict

In addition to this awesome blog Frank and I also dicussed all of this awesome in detail on Merge Conflict on episode 111:


Tags



Live, Love, Bike, and Code

Checkout my monthly newsletter that you should subscribe to!

Comments