A record típus működése

A C# 9.0 bevezette a record típust, ami kiválóan alkalmas immutable szerkezetek létrehozására, valamint érték szerinti egyezés és deep clone támogatást biztosít. De hogyan is működik mindez?

A kivizsgálás apropóját az alábbi kódrészlet váltotta ki. Pontosabban a CS0246 hibaüzenet, miszerint:

The type or namespace name ‘record’ could not be found (are you missing a using directive or an assembly reference?)

//fordítási hibát eredményez
public record Generic<T> where T: record { }

Naivan gondolhatnánk, hogy a compiler vagy a CLR fejlesztői nem implementálták a record típust generikus constraint-nek. Azonban a helyzet nem ennyire egyszerű.

A record IL kód szinten nem létezik. A CLR számára a record ugyanazzal a .class assembly utasítással van reprezentálva, mint egy “normál” osztály.

Erről könnyen megbizonyosodhatunk az ILSpy segítségével és egy egyszerű mintaprogrammal:

public record RecordTest
{
	public string Name { get; init; }
	public int Value { get; init; }
}

public class ClassTest
{
	public string Name { get; init; }
	public int Value { get; init; }
}

A ClassTest IL kódja:

.class public auto ansi beforefieldinit recordProgram.ClassTest
	extends [System.Runtime]System.Object

A RecordTest IL kódja:

.class public auto ansi beforefieldinit recordProgram.RecordTest
	extends [System.Runtime]System.Object
	implements class [System.Runtime]System.IEquatable`1<class recordProgram.RecordTest>

Ahogy látható, az osztály és record változat között IL szinten annyi különbség van, hogy a record a compiler által generálva hozza magával a System.IEquatable<T> interfész implementációját, generált == és != operátorokat és egy <Clone>$ nevű, magasabb szinten (C#) nem látható metódust. Ezen felül minden record tartalmaz még egy plusz EqualityContract nevű property-t, ami Type típusú és a record GetHashCode() működésében játszik szerepet.

Ezek alapján adódik, hogy Reflection használatkor sem annyi eldönteni, hogy egy típus record vagy class, hogy a Type-on megnézzük az IsRecord property-t, mivel ez sem létezik.

Viszont könnyen összerakhatunk egy metódust, amit a fenti információk alapján pont ezt teszi. Ezt érdemes Extension Method-ként definiálni, hogy könnyem használható legyen:

public static class RecordReflectionExtension
{
        public static bool IsRecord(Type type)
        {
            var check1 = type
                .GetTypeInfo()
                .DeclaredProperties
                .FirstOrDefault(x => x.Name == "EqualityContract")?
                .GetMethod?
                .GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is object;

            var check2 = type.GetMethod("<Clone>$") is object;

            return check1 && check2;
        }
}

Összefoglalás

Hosszan kifejtve a record típus azért nem használható generikus constraint-nek, mert a CLR típus szintjén nem különbözik az osztálytól. Az eltérés csak a generált viselkedésben keresendő.

Ebből adódóan készíthetünk ugyan generikus rekordot, csak constraint-nek a IEquatable<T> interfészt kell megadnunk:

public record GenericRecord<T> where T: IEquatable<T> { }

Azonban az IEquatable<T> interfészt implementálhatja egy osztály is, ezért ez nem teljesen valódi constraint típus tekintetében. Valamint megjegyezném, hogy attól, hogy valamit megtehetünk, még nem biztos, hogy meg kell tennünk.

A record típus alapvetően Data-driven programming paradigmát segíti. Ehhez pedig nem kell generikus támogatás.