Dave Cook
Dave Cook

Reputation: 71

Equality of F# record Types when consumed from C#

I have the following record type in an F# lib:

type Rating = {
        Id : string
        AverageRating : decimal
        ValueRange : int
        Label : string }

And as expected, the following test passes in an F# test lib:

[Fact]
let ``ShouldConsiderTwoInstancesOfAClassToBeTheSame`` () =

    let a = {Rating.Id = "id"
             AverageRating = 4.3m
             ValueRange = 10
             Label = "label"}

    let b = {Rating.Id = "id"
             AverageRating = 4.3m
             ValueRange = 10
             Label = "label"}

    a = b |> should equal true
    a.Equals b |> should equal true
    a <> b |> should equal false
    System.Object.ReferenceEquals(a, b) |> should equal false

However, the following test in C# fails...both of the first 2 assertions fail, though the third assertion passes fine:

    [Test]
    public void ShouldConsiderTwoInstancesOfAClassToBeTheSame()
    {
        var a = new Rating("id", 4.3m, 10, "label");
        var b = new Rating("id", 4.3m, 10, "label");

        Assert.True(a == b);
        Assert.False(a != b);
        Assert.True(a.Equals(b));
        Assert.False(ReferenceEquals(a, b));
    }

Is there a way to get the structural equality that record types provide out of the box when consuming from C# using just the operators, or do you need to call Equals() ?

Upvotes: 4

Views: 683

Answers (2)

Asti
Asti

Reputation: 12687

In idiomatic C#, reference types are not expected to test positive for structural equality. IEquatable as a contract makes more semantic sense.

Equality, by default, means equality of reference. F# is quite different in this regard - it uses structural equality for comparisons. If you look at what F# does for a = b, it calls

a.Equals(b, GenericEqualityComparer);

However, for a type with an == operator implemented, the C# compiler knows to pick the operator's method.

push.0 //ldloc, ldfld, etc.
push.1
call bool Rating::op_Equality(valuetype Rating, valuetype Rating)

What generally happens for a == b, is:

push.0
push.1
ceq

ceq is much faster than any of the method call alternatives, and is the default equality comparison, which gives you equality of reference.

Other core .NET types may not explicitly implement == and !=, but the JIT has specific implementation details to perform equality comparison for those intrinsic types (signed, fp, etc.), so they still equate correctly.

If an == comparison is semantically important to you, you can implement the operators yourself:

type Rating = {
    Id : string
    AverageRating : decimal
    ValueRange : int
    Label : string 
} with
    static member op_Equality (a: Rating, b: Rating) =
      a.Equals b
    static member op_Inequality (a: Rating, b: Rating) =
          not (a.Equals b)

And your tests should work again.

Upvotes: 4

flq
flq

Reputation: 22859

In order for == to work in C#, the class in question needs to implement the == operator

bool operator ==(Rating a, Rating b) => ...

If you look at the generated IL-Code for Rating, you will see that it implement IEquatable<T>, IComparable<T>, GetHashCode(), etc. but I have seen no operator implementation.

Upvotes: 1

Related Questions