Marcin Jahn | Dev Notebook
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • Home
  • Programming
  • Technologies
  • Projects
  • About
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Garbage Collector
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Standalone Components
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the CSS section CSS
    • Responsive Design
    • CSS Tips
    • CSS Pixel
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • ASP.NET Core Projects
  • An icon of the Core section Core
    • Programs Execution
    • Stack and Heap
    • Asynchronous Programming
      • Overview
      • Event Queues
      • Fibers
      • Stackless Coroutines
  • An icon of the .NET section .NET
    • HTTPClient
    • Async
      • How Async Works
      • TAP Tips
    • Equality
    • Comparisons
    • Enumerables
    • Unit Tests
    • Generic Host
    • Logging
    • Configuration
    • Records
    • Nullability
    • Garbage Collector
    • IL and Allocations
    • gRPC
    • Source Generators
    • Platform Invoke
    • ASP.NET Core
      • Overview
      • Middleware
      • Razor Pages
      • Routing in Razor Pages
      • Web APIs
      • Filters
      • Identity
      • Validation
      • Tips
    • Entity Framework Core
      • Overview
      • Testing
      • Tips
  • An icon of the Angular section Angular
    • Overview
    • Components
    • Directives
    • Services and DI
    • Routing
    • Observables (RxJS)
    • Forms
    • Pipes
    • HTTP
    • Modules
    • NgRx
    • Angular Universal
    • Tips
    • Standalone Components
  • An icon of the JavaScript section JavaScript
    • OOP
    • JavaScript - The Weird Parts
    • JS Functions
    • ES Modules
    • Node.js
    • Axios Tips
    • TypeScript
      • TypeScript Environment Setup
      • TypeScript Tips
    • React
      • React Routing
      • MobX
    • Advanced Vue.js Features
  • An icon of the Rust section Rust
    • Overview
    • Cargo
    • Basics
    • Ownership
    • Structures
    • Enums
    • Organization
    • Collections
    • Error Handling
    • Generics
    • Traits
    • Lifetimes
    • Closures
    • Raw Pointers
    • Smart Pointers
    • Concurrency
    • Testing
    • Tips
  • An icon of the C/C++ section C/C++
    • Compilation
    • Structures
    • OOP in C
    • Pointers
    • Strings
    • Dynamic Memory
    • argc and argv Visualization
  • An icon of the GTK section GTK
    • Overview
    • GObject
    • GJS
  • An icon of the CSS section CSS
    • Responsive Design
    • CSS Tips
    • CSS Pixel
  • An icon of the Unity section Unity
    • Unity
  • An icon of the Functional Programming section Functional Programming
    • Fundamentals of Functional Programming
    • .NET Functional Features
    • Signatures
    • Function Composition
    • Error Handling
    • Partial Application
    • Modularity
    • Category Theory
      • Overview
      • Monoid
      • Other Magmas
      • Functors
  • An icon of the Algorithms section Algorithms
    • Big O Notation
    • Array
    • Linked List
    • Queue
    • Hash Table and Set
    • Tree
    • Sorting
    • Searching
  • An icon of the Architecture section Architecture
    • What is architecture?
    • Domain-Driven Design
    • ASP.NET Core Projects

Records

Records can be stored on a heap or a stack:

// heap, like a class
record Person(string Name);
// stack, like a struct
record struct Person2(string Name);

Depending on the choice as above, the compiler will transform the record into either a class or a struct (in a process called the lowering). Record is just s “sugar syntax” in .NET.

Records are useful when we’re dealing with classes that just carry information and do not have any logic/methods (DTOs).

Features

Printing

Printing an instance of a record by default prints its content. An instance of a class would print its type.

Console.WriteLine(recordInstance); // Person { Name = "Marcin" }
Console.WriteLine(classInstance); // Namespace.ClassName

Records printing behavior may be overridden just like in a normal class.

Equality

Class instances (unless explicitly coded otherwise) will not be equal even if all properties have the same values. Equality is checked by reference.

In the case of records, an equality check compares the values of the properties.

The with operator

Records may be copied (by value) with some changes to original values like this:

var rec1 = new Person("Marcin", 25);
var rec2 = rec1 with { Age = 20 }; // only age gets modified in the new record instance

Anonymous Types

The with operator can also be applied to anonymous types in C#.

Entity Framework Core

Record types aren’t appropriate for use as entity types in Entity Framework Core. Read more at MSDN.

Deconstructing

A bit similarly to JS, we can extract some values from records:

(var name, var age) = rec1;

To support deconstructing, the compiler generates the Deconstruct method for a record during the lowering process of compilation.

Intermediate Language

To better understand the difference between a class and a struct, let’s compare IL generated for an empty class and record.

Class

public class SimpleClass
{
}
.class public auto ansi beforefieldinit
HelloWorld.SimpleClass
extends [System.Runtime]System.Object
{
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method SimpleClass::.ctor
} // end of class HelloWorld.SimpleClass

The generated IL is really simple, and it really just corresponds to the provided C# code. The only thing added is a constructor.

Record

public record SimpleRecord();
.class public auto ansi beforefieldinit
HelloWorld.SimpleRecord
extends [System.Runtime]System.Object
implements class [System.Runtime]System.IEquatable`1<class HelloWorld.SimpleRecord>
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(unsigned int8)
= (01 00 01 00 00 ) // .....
// unsigned int8(1) // 0x01
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(unsigned int8)
= (01 00 00 00 00 ) // .....
// unsigned int8(0) // 0x00
.interfaceimpl type class [System.Runtime]System.IEquatable`1<class HelloWorld.SimpleRecord>
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(unsigned int8)
= (01 00 00 00 00 ) // .....
// unsigned int8(0) // 0x00
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
// [3 1 - 3 30]
IL_0000: ldarg.0 // this
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method SimpleRecord::.ctor
.method family hidebysig virtual newslot specialname instance class [System.Runtime]System.Type
get_EqualityContract() cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldtoken HelloWorld.SimpleRecord
IL_0005: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000a: ret
} // end of method SimpleRecord::get_EqualityContract
.method public hidebysig virtual instance string
ToString() cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 2
.locals init (
[0] class [System.Runtime]System.Text.StringBuilder V_0
)
IL_0000: newobj instance void [System.Runtime]System.Text.StringBuilder::.ctor()
IL_0005: stloc.0 // V_0
IL_0006: ldloc.0 // V_0
IL_0007: ldstr "SimpleRecord"
IL_000c: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
IL_0011: pop
IL_0012: ldloc.0 // V_0
IL_0013: ldstr " { "
IL_0018: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(string)
IL_001d: pop
IL_001e: ldarg.0 // this
IL_001f: ldloc.0 // V_0
IL_0020: callvirt instance bool HelloWorld.SimpleRecord::PrintMembers(class [System.Runtime]System.Text.StringBuilder)
IL_0025: brfalse.s IL_0030
IL_0027: ldloc.0 // V_0
IL_0028: ldc.i4.s 32 // 0x20
IL_002a: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
IL_002f: pop
IL_0030: ldloc.0 // V_0
IL_0031: ldc.i4.s 125 // 0x7d
IL_0033: callvirt instance class [System.Runtime]System.Text.StringBuilder [System.Runtime]System.Text.StringBuilder::Append(char)
IL_0038: pop
IL_0039: ldloc.0 // V_0
IL_003a: callvirt instance string [System.Runtime]System.Object::ToString()
IL_003f: ret
} // end of method SimpleRecord::ToString
.method family hidebysig virtual newslot instance bool
PrintMembers(
class [System.Runtime]System.Text.StringBuilder builder
) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldc.i4.0
IL_0001: ret
} // end of method SimpleRecord::PrintMembers
.method public hidebysig static specialname bool
op_Inequality(
class HelloWorld.SimpleRecord left,
class HelloWorld.SimpleRecord right
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(unsigned int8)
= (01 00 02 00 00 ) // .....
// unsigned int8(2) // 0x02
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldarg.0 // left
IL_0001: ldarg.1 // right
IL_0002: call bool HelloWorld.SimpleRecord::op_Equality(class HelloWorld.SimpleRecord, class HelloWorld.SimpleRecord)
IL_0007: ldc.i4.0
IL_0008: ceq
IL_000a: ret
} // end of method SimpleRecord::op_Inequality
.method public hidebysig static specialname bool
op_Equality(
class HelloWorld.SimpleRecord left,
class HelloWorld.SimpleRecord right
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(unsigned int8)
= (01 00 02 00 00 ) // .....
// unsigned int8(2) // 0x02
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldarg.0 // left
IL_0001: ldarg.1 // right
IL_0002: beq.s IL_0013
IL_0004: ldarg.0 // left
IL_0005: brfalse.s IL_0010
IL_0007: ldarg.0 // left
IL_0008: ldarg.1 // right
IL_0009: callvirt instance bool HelloWorld.SimpleRecord::Equals(class HelloWorld.SimpleRecord)
IL_000e: br.s IL_0011
IL_0010: ldc.i4.0
IL_0011: br.s IL_0014
IL_0013: ldc.i4.1
IL_0014: ret
} // end of method SimpleRecord::op_Equality
.method public hidebysig virtual instance int32
GetHashCode() cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: call class [System.Collections]System.Collections.Generic.EqualityComparer`1<!0/*class [System.Runtime]System.Type*/> class [System.Collections]System.Collections.Generic.EqualityComparer`1<class [System.Runtime]System.Type>::get_Default()
IL_0005: ldarg.0 // this
IL_0006: callvirt instance class [System.Runtime]System.Type HelloWorld.SimpleRecord::get_EqualityContract()
IL_000b: callvirt instance int32 class [System.Collections]System.Collections.Generic.EqualityComparer`1<class [System.Runtime]System.Type>::GetHashCode(!0/*class [System.Runtime]System.Type*/)
IL_0010: ret
} // end of method SimpleRecord::GetHashCode
.method public hidebysig virtual instance bool
Equals(
object obj
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(unsigned int8)
= (01 00 02 00 00 ) // .....
// unsigned int8(2) // 0x02
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: ldarg.1 // obj
IL_0002: isinst HelloWorld.SimpleRecord
IL_0007: callvirt instance bool HelloWorld.SimpleRecord::Equals(class HelloWorld.SimpleRecord)
IL_000c: ret
} // end of method SimpleRecord::Equals
.method public hidebysig virtual newslot instance bool
Equals(
class HelloWorld.SimpleRecord other
) cil managed
{
.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(unsigned int8)
= (01 00 02 00 00 ) // .....
// unsigned int8(2) // 0x02
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: ldarg.1 // other
IL_0002: beq.s IL_001d
IL_0004: ldarg.1 // other
IL_0005: brfalse.s IL_001a
IL_0007: ldarg.0 // this
IL_0008: callvirt instance class [System.Runtime]System.Type HelloWorld.SimpleRecord::get_EqualityContract()
IL_000d: ldarg.1 // other
IL_000e: callvirt instance class [System.Runtime]System.Type HelloWorld.SimpleRecord::get_EqualityContract()
IL_0013: call bool [System.Runtime]System.Type::op_Equality(class [System.Runtime]System.Type, class [System.Runtime]System.Type)
IL_0018: br.s IL_001b
IL_001a: ldc.i4.0
IL_001b: br.s IL_001e
IL_001d: ldc.i4.1
IL_001e: ret
} // end of method SimpleRecord::Equals
.method public hidebysig virtual newslot instance class HelloWorld.SimpleRecord
'<Clone>$'() cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: newobj instance void HelloWorld.SimpleRecord::.ctor(class HelloWorld.SimpleRecord)
IL_0006: ret
} // end of method SimpleRecord::'<Clone>$'
.method family hidebysig specialname rtspecialname instance void
.ctor(
class HelloWorld.SimpleRecord original
) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method SimpleRecord::.ctor
.property instance class [System.Runtime]System.Type EqualityContract()
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.get instance class [System.Runtime]System.Type HelloWorld.SimpleRecord::get_EqualityContract()
} // end of property SimpleRecord::EqualityContract
} // end of class HelloWorld.SimpleRecord

That’s a huge difference. An empty record results in lots of code being generated. All this code is a result of features that records support:

  • equality logic (you can see a few equality-related methods)
  • ToString override - interesting point is how the { character is added via Append(string) override of StringBuilder, while the closing bracket (}) uses Append(char) (with value 125 representing })
  • cloning
←  Configuration
Nullability  →
© 2023 Marcin Jahn | Dev Notebook | All Rights Reserved. | Built with Astro.