What’s New in C# 10: Value Type Records

This is part of a series on the new features introduced with C# 10.

In a previous post I wrote about records in C# 9.Prior to C# 10 record types were reference types. In C# 10 you can now declare records as value types.

You declare a value record type by adding the struct keyword.

You may also add the readonly modifier if you want to create an immutable value type:

// struct modifier - this will create a value type (mutable)
public record struct CurrencyExchangeRate3(string SourceCurrencyCode,
                                                    string DestinationCurrencyCode,
                                                    decimal ExchangeRate);

// struct modifier (and readonly) - this will create a value type (immutable)
public readonly record struct CurrencyExchangeRate4(string SourceCurrencyCode,
                                                string DestinationCurrencyCode,
                                                decimal ExchangeRate);

If you don’t specify the struct modifier you will get a reference record. If you want to you can add the class modifier if you think it will make the code more readable:

// No modifier - this will be a reference type record
public record CurrencyExchangeRate1(string SourceCurrencyCode,
                                    string DestinationCurrencyCode,
                                    decimal ExchangeRate);


// Explicit class modifier - this will also be a reference type record
public record class CurrencyExchangeRate2(string SourceCurrencyCode,
                                            string DestinationCurrencyCode,
                                            decimal ExchangeRate);

All of the above examples use the positional syntax for defining the record properties.

Record Struct Equality

The default equality for record structs is the same for non-record structs:2 objects will be equal if they are both the same type and have the same values.

There is one key difference and that is how the default equality is implemented. With normal non-record structs, to determine equality reflection is used behind the scenes which can be slow. With record structs however reflection is not used, the equality code is synthesized by the compiler.

If we use a tool like DotPeek to decompile the Equals method we get the following:

public bool Equals(CurrencyExchangeRate3 other)
{
  // ISSUE: reference to a compiler-generated field
  // ISSUE: reference to a compiler-generated field
  // ISSUE: reference to a compiler-generated field
  // ISSUE: reference to a compiler-generated field
  if (EqualityComparer<string>.Default.Equals(this.\u003CSourceCurrencyCode\u003Ek__BackingField, other.\u003CSourceCurrencyCode\u003Ek__BackingField) && EqualityComparer<string>.Default.Equals(this.\u003CDestinationCurrencyCode\u003Ek__BackingField, other.\u003CDestinationCurrencyCode\u003Ek__BackingField))
  {
    // ISSUE: reference to a compiler-generated field
    // ISSUE: reference to a compiler-generated field
    return EqualityComparer<Decimal>.Default.Equals(this.\u003CExchangeRate\u003Ek__BackingField, other.\u003CExchangeRate\u003Ek__BackingField);
  }
  return false;
}

Notice the preceding code is not using reflection to determine if the data items are equal. This means is some situations a record struct may perform better that a standard struct. Check out this related article on struct performance I wrote.

Another difference between record class and record struct is that in class records you can write a custom copy constructor, for example one that always set the exchange rate to 0:

public record class CurrencyExchangeRate5(string SourceCurrencyCode,
                                          string DestinationCurrencyCode,
                                          decimal ExchangeRate)
    {
        // Copy constructor
        protected CurrencyExchangeRate5(CurrencyExchangeRate5 previous)
        {
            SourceCurrencyCode = previous.SourceCurrencyCode;
            DestinationCurrencyCode = previous.DestinationCurrencyCode;
            ExchangeRate = 0;
        }
    }
}

Now if you wrote: CurrencyExchangeRate6 f2 = f1 with { SourceCurrencyCode = "xyz" }; f2 would have it’s currency set to 0.

If you tried this with a record struct, the custom copy constructor won’t be called.

SHARE:

Add comment

Loading