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.