Records (C# 9) #
A record is a special kind of class, intended for working with immutable data. It allows for a concise way to define a type, avoiding writing boilerplate code.
public record Person(string FirstName, string LastName);In place of the record, the compiler will generate a Person class for us, along with init-only properties, a constructor, a deconstructor, comparison operators, and overridden Equals, GetHashCode, and ToString methods.
The Person class generated by the compiler
[CompilerGenerated]
[NullableContext(1)]
[Nullable(0)]
public class Person : IEquatable<Person>
{
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string <FirstName>k__BackingField;
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly string <LastName>k__BackingField;
[CompilerGenerated]
protected virtual Type EqualityContract
{
[CompilerGenerated]
get
{
return typeof(Person);
}
}
public string FirstName
{
[CompilerGenerated]
get
{
return <FirstName>k__BackingField;
}
[CompilerGenerated]
init
{
<FirstName>k__BackingField = value;
}
}
public string LastName
{
[CompilerGenerated]
get
{
return <LastName>k__BackingField;
}
[CompilerGenerated]
init
{
<LastName>k__BackingField = value;
}
}
public Person(string FirstName, string LastName)
{
<FirstName>k__BackingField = FirstName;
<LastName>k__BackingField = LastName;
base..ctor();
}
[CompilerGenerated]
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Person");
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(' ');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
[CompilerGenerated]
protected virtual bool PrintMembers(StringBuilder builder)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
builder.Append("FirstName = ");
builder.Append((object)FirstName);
builder.Append(", LastName = ");
builder.Append((object)LastName);
return true;
}
[NullableContext(2)]
[CompilerGenerated]
public static bool operator !=(Person left, Person right)
{
return !(left == right);
}
[NullableContext(2)]
[CompilerGenerated]
public static bool operator ==(Person left, Person right)
{
return (object)left == right || ((object)left != null && left.Equals(right));
}
[CompilerGenerated]
public override int GetHashCode()
{
return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(<FirstName>k__BackingField)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(<LastName>k__BackingField);
}
[NullableContext(2)]
[CompilerGenerated]
public override bool Equals(object obj)
{
return Equals(obj as Person);
}
[NullableContext(2)]
[CompilerGenerated]
public virtual bool Equals(Person other)
{
return (object)this == other || ((object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(<FirstName>k__BackingField, other.<FirstName>k__BackingField) && EqualityComparer<string>.Default.Equals(<LastName>k__BackingField, other.<LastName>k__BackingField));
}
[CompilerGenerated]
public virtual Person <Clone>$()
{
return new Person(this);
}
[CompilerGenerated]
protected Person(Person original)
{
<FirstName>k__BackingField = original.<FirstName>k__BackingField;
<LastName>k__BackingField = original.<LastName>k__BackingField;
}
[CompilerGenerated]
public void Deconstruct(out string FirstName, out string LastName)
{
FirstName = this.FirstName;
LastName = this.LastName;
}
}Comparison Semantics #
Records implement comparison by value, similarly to tuples. The Equals method and comparison operators check sequentially if all properties are equal to each other.
public record Person(string FirstName, string LastName, int Age);
var john = new Person("John", "Doe", 30);
var doe = new Person("John", "Doe", 30);
Console.WriteLine($"Person: {john == doe}"); // TrueNon-destructive mutation #
Record objects are immutable. The with expression allows for the creation of a new record instance that is a copy of an existing one, but with selected properties changed. Under the hood, this mechanism relies on a special, compiler-generated copy constructor (e.g., protected Person(Person original)) that creates a shallow copy of the object before applying the changes.
var john = new Person("John", "Doe", 30);
var jane = john with { FirstName = "Jane", Age = 0 };
Console.WriteLine(john); // Person { FirstName = John, LastName = Doe, Age = 30 }
Console.WriteLine(jane); // Person { FirstName = Jane, LastName = Doe, Age = 0 }record struct (C# 10)
#
Since C# 10, we can also create records of a value type. The compiler will generate a mutable structure for a record struct, and an immutable one for a readonly record struct.
public record struct Vector3(double X, double Y, double Z);
public readonly record struct Point2(double X, double Y);The
recordkeyword itself is an abbreviation forrecord class.
Customizing Records #
Records are intended for storing data, and for that, the set generated by the compiler is usually sufficient. If needed, you can also add your own fields, properties, and methods to records. Moreover, if we provide an implementation of a functionality that the compiler would normally provide (e.g., the ToString() method), our version will be used, overriding the default behavior.
public record Product(string Name, decimal Price)
{
public int Quantity { get; set; }
public Product(Product original)
{
Name = original.Name;
Price = original.Price;
Quantity = 0;
}
public override string ToString() => $"{Name}({Quantity}): {Price:C}";
}Product apple = new Product("Apple", 1.99m) { Quantity = 5 };
Product copy = apple with {Price = 2.99m};
Console.WriteLine(apple); // Apple(5): $1.99
Console.WriteLine(copy); // Apple(0): $2.99Anonymous Types #
Anonymous types are simple, small classes created “on the fly” by the compiler, without the need to give them an explicit name. They are used to create objects that are meant to store data only temporarily, within a single method.
They are created using the new keyword and an object initializer:
var anon = new { Name = "Alice", Age = 23 };The compiler will automatically generate a class that has public, unmodifiable properties, a constructor, and overloads for Equals, GetHashCode, and ToString.
The anonymous class generated by the compiler
[CompilerGenerated]
[DebuggerDisplay("\{ Name = {Name}, Age = {Age} }", Type = "<Anonymous Type>")]
internal sealed class <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly <Name>j__TPar <Name>i__Field;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly <Age>j__TPar <Age>i__Field;
public <Name>j__TPar Name
{
get
{
return <Name>i__Field;
}
}
public <Age>j__TPar Age
{
get
{
return <Age>i__Field;
}
}
[DebuggerHidden]
public <>f__AnonymousType0(<Name>j__TPar Name, <Age>j__TPar Age)
{
<Name>i__Field = Name;
<Age>i__Field = Age;
}
[DebuggerHidden]
public override bool Equals(object value)
{
<>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar> anon = value as <>f__AnonymousType0<<Name>j__TPar, <Age>j__TPar>;
return this == anon || (anon != null && EqualityComparer<<Name>j__TPar>.Default.Equals(<Name>i__Field, anon.<Name>i__Field) && EqualityComparer<<Age>j__TPar>.Default.Equals(<Age>i__Field, anon.<Age>i__Field));
}
[DebuggerHidden]
public override int GetHashCode()
{
return (-2097246416 * -1521134295 + EqualityComparer<<Name>j__TPar>.Default.GetHashCode(<Name>i__Field)) * -1521134295 + EqualityComparer<<Age>j__TPar>.Default.GetHashCode(<Age>i__Field);
}
[DebuggerHidden]
[return: Nullable(1)]
public override string ToString()
{
object[] array = new object[2];
<Name>j__TPar val = <Name>i__Field;
array[0] = ((val != null) ? val.ToString() : null);
<Age>j__TPar val2 = <Age>i__Field;
array[1] = ((val2 != null) ? val2.ToString() : null);
return string.Format(null, "{{ Name = {0}, Age = {1} }}", array);
}
}Similarly to records, anonymous classes are compared by value, and they support non-destructive mutation.
The most important limitation of anonymous types is their local scope. They cannot be used as a method’s return type or as a parameter, because they do not have a name that could be referenced. They are intended exclusively for temporary use inside a single method.
Property Names #
We can explicitly name the properties. If we do not do this, the compiler will try to deduce the name based on the name of the passed field or property.
int age = 23;
var anon = new { Name = "Bob", age, age.ToString().Length };
Console.WriteLine($"His {nameof(anon.Name)} is {anon.Name}");
Console.WriteLine($"His {nameof(anon.age)} is {anon.age}");
Console.WriteLine($"His {nameof(anon.Length)} is {anon.Length}");Anonymous Types in LINQ #
Anonymous types are particularly useful in LINQ when we need to create an object on the fly that aggregates some data.
var queryResult = ratings
.GroupBy(r => r.MovieId)
.Select(g => new
{
MovieId = g.Key,
Average = g.Average(r => r.Score)
})
.Where(x => x.Average > 8)
.Join(movies,
rating => rating.MovieId,
movie => movie.Id,
(rating, movie) => new
{
Movie = movie,
rating.Average
})
.GroupJoin(casts,
movie => movie.Movie.Id,
cast => cast.MovieId,
(movie, movieCasts) => new
{
movie.Movie,
movie.Average,
CastIds = movieCasts.Select(c => c.ActorId)
})
.Select(x => new
{
x.Movie,
x.Average,
Cast = x.CastIds
.Join(actors,
actorId => actorId,
actor => actor.Id,
(actorId, actor) => actor)
.ToList()
});