Struct vs Class vs Record vs Record Struct — Quel type choisir en C# ?

Vous creez un nouveau type de donnees en C#. Vous ouvrez votre IDE, tapez public et... vous hesitez. class ? struct ? Ou peut-etre record ? Depuis C# 9, nous avons quatre options : class, struct, record (record class) et record struct. Chacune a sa place — mais le choix n'est pas toujours evident.

Dans cet article, je vais vous montrer en quoi ils different, mesurer leurs performances avec BenchmarkDotNet et vous donner des conseils pratiques pour savoir quand utiliser chaque option.


Quelles sont les differences entre ces quatre types ?

Class — type reference

public class PersonClass
{
    public string Name { get; set; }
}

Une classe est un type reference. Les instances sont allouees sur le tas (heap) et gerees par le Garbage Collector. Lorsque vous passez un objet de classe a une methode, vous ne passez que la reference — l'objet lui-meme n'est pas copie.

Les classes prennent en charge l'heritage, l'implementation d'interfaces sans surcharge de boxing et la validation complete dans le constructeur. Par defaut, elles sont comparees par egalite de reference — deux objets ne sont "egaux" que s'ils pointent vers le meme emplacement memoire.

Struct — type valeur

public struct PersonStruct
{
    public string Name { get; set; }
}

Une structure est un type valeur. Elle est stockee inline — sur la pile ou directement a l'interieur de l'objet parent. Elle ne necessite pas d'allocation sur le tas ni de ramasse-miettes, ce qui la rend moins couteuse a creer et a detruire.

Attention : passer une structure a une methode signifie copier sa valeur. Pour les petites structures, c'est rapide, mais pour les grandes — couteux. Les structures ne prennent pas en charge l'heritage.

Record (record class) — classe avec egalite de valeur

public record PersonRecord(string Name);

Un record est une classe, mais avec une egalite de valeur (value equality) generee automatiquement par le compilateur. Deux records ayant les memes proprietes sont consideres comme egaux. Le compilateur genere pour vous Equals(), GetHashCode(), ToString() et l'operateur ==.

Sous le capot, c'est toujours un type reference — allocation sur le tas, GC, passage par reference. Le mot-cle record est en realite record class.

Record Struct — struct avec egalite de valeur

public record struct PersonRecordStruct(string Name);

La combinaison des avantages de la structure (type valeur, pas de GC) avec la commodite du record (egalite de valeur automatique). Ideal lorsque vous avez besoin d'un petit type immuable avec une comparaison pertinente.


Benchmark — mesurer les performances

La theorie c'est bien, mais verifions en pratique. J'utilise BenchmarkDotNet pour comparer la creation et l'iteration sur 1000 objets de chaque type.

Code du benchmark

Worker creant des listes d'objets a partir d'une liste de prenoms :

public class ClassVsStruct
{
    private readonly IReadOnlyList<string> _names;

    public ClassVsStruct(IReadOnlyList<string> names)
    {
        _names = names;
    }

    public List<PersonClass> BuildPersonClass()
    {
        return _names.Select(x => new PersonClass { Name = x }).ToList();
    }

    public List<PersonStruct> BuildPersonStruct()
    {
        return _names.Select(x => new PersonStruct { Name = x }).ToList();
    }

    public List<PersonRecordStruct> BuildPersonRecordStruct()
    {
        return _names.Select(x => new PersonRecordStruct(Name: x)).ToList();
    }

    public List<PersonRecord> BuildPersonRecord()
    {
        return _names.Select(x => new PersonRecord(Name: x)).ToList();
    }
}

Le benchmark cree 1000 objets, puis itere dessus en lisant la propriete Name :

[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn(NumeralSystem.Arabic)]
public class ClassVsStruct
{
    public List<string> Names => File.ReadAllLines("Resources/1000_imion.txt").ToList();

    [Benchmark]
    public void ThousandClasses()
    {
         var personClass = new Code.ClassVsStruct(Names).BuildPersonClass();

         for (int i = 0; i < personClass.Count; i++)
         {
             string personName = personClass.ElementAt(i).Name;
         }
    }

    [Benchmark]
    public void ThousandStructs()
    {
        var personStruct = new Code.ClassVsStruct(Names).BuildPersonStruct();

        for (int i = 0; i < personStruct.Count; i++)
        {
            string personName = personStruct.ElementAt(i).Name;
        }
    }

    [Benchmark]
    public void ThousandRecordStructs()
    {
        var personRecordStruct = new Code.ClassVsStruct(Names).BuildPersonRecordStruct();

        for (int i = 0; i < personRecordStruct.Count; i++)
        {
            string personName = personRecordStruct.ElementAt(i).Name;
        }
    }

    [Benchmark]
    public void ThousandRecord()
    {
        var personRecord = new Code.ClassVsStruct(Names).BuildPersonRecord();

        for (int i = 0; i < personRecord.Count; i++)
        {
            string personName = personRecord.ElementAt(i).Name;
        }
    }
}

Resultats

Environnement : Intel Core i9-13900H, Windows 11, .NET 9.0.12, BenchmarkDotNet v0.15.8

| Methode | Mean | Error | StdDev | Rank | |---------------------- |---------|---------|---------|-----| | ThousandRecordStructs | 84.46 us | 1.407 us | 1.316 us | 1 | | ThousandStructs | 88.40 us | 1.754 us | 4.830 us | 1 | | ThousandRecord | 89.60 us | 1.576 us | 1.474 us | 1 | | ThousandClasses | 95.53 us | 1.830 us | 2.034 us | 2 |

Analyse des resultats

Les resultats se regroupent en deux camps :

  • Types valeur (struct, record struct) : ~84-88 us — les plus rapides
  • Types reference (class, record) : ~89-95 us — plus lents

Les classes sont ~13% plus lentes que les record struct. La difference provient du surcharge d'allocation sur le tas et du travail du Garbage Collector.

Fait interessant : record vs class et record struct vs struct ont des performances quasi identiques. Le mot-cle record est principalement du sucre syntaxique — le compilateur genere le code repetitif, mais le mecanisme sous-jacent reste le meme.

Conclusion cle : l'immutabilite et le type valeur ont un cout — mais c'est la version mutable qui le paie. Si vous pouvez travailler avec des donnees en lecture seule, ce sera presque toujours plus rapide.


Quand choisir quoi ? Criteres de decision

Heritage et POO

Vous avez besoin de classes de base et derivees ? Seules les classes prennent en charge l'heritage. Les structures ne le supportent pas.

Les interfaces fonctionnent techniquement avec les structures, mais impliquent du boxing (conversion d'un type valeur en type reference), ce qui annule les avantages de performance. Si votre type doit implementer une interface, preferez une classe.

Validation des donnees

Les classes permettent une validation complete dans le constructeur. Le probleme avec les structures est que, dans certaines circonstances, elles peuvent etre creees avec des donnees mises a zero sans appel au constructeur — ce qui rend difficile l'application de la validite des donnees.

Performance de creation d'instances

  • Classe : plus couteuse — allocation sur le tas + GC
  • Struct : moins couteuse — inline, pas de GC

Mais attention au passage aux methodes :

  • Classe : rapide (vous passez une reference)
  • Struct : copie de la valeur (la vitesse depend de la taille)

Immutabilite

C# offre un meilleur support linguistique pour les structures immuables. Si votre type doit etre immuable, struct est le choix naturel.

Comparaison d'objets

| | Egalite de reference | Egalite de valeur | |---|---|---| | class | par defaut | necessite implementation | | struct | non applicable | necessite implementation | | record | non | automatique | | record struct | non applicable | automatique |

Si vous avez besoin de value equality sans ecrire de code repetitif — optez pour record.


Exemples concrets

Voici quelques scenarios typiques et les approches recommandees :

Adresse client (CustomerAddress)

Recommandation : record (record class)

Une adresse est un objet metier — elle regroupe de nombreuses proprietes (rue, ville, code postal). Elle est assez volumineuse, ce qui ne favorise pas les structures. Elle necessite probablement une validation. Le record class vous offre l'egalite de valeur (deux adresses identiques doivent etre "egales") sans ecrire manuellement Equals().

Controle DateTimePicker

Recommandation : class

Les controles UI necessitent l'egalite de reference — vous ne voulez pas affirmer que deux controles sont egaux simplement parce que l'utilisateur a saisi la meme date. Les controles font partie d'une hierarchie de classes (heritage). Struct est exclu.

Point3D (coordonnees 3D)

Recommandation : record struct

Petit type (3 champs : X, Y, Z), souvent cree en grande quantite dans des boucles serrees. La performance de creation d'instances est cruciale. Deux points ayant les memes coordonnees doivent etre egaux — record struct vous l'offre automatiquement.

Depot de donnees (IProductRepository)

Recommandation : class

Un depot implemente generalement une interface (par exemple pour les tests). Vous ne creez qu'une ou deux instances pendant le cycle de vie de l'application — le surcharge d'allocation n'a pas d'importance. Ce n'est pas un "groupe de proprietes", donc record ne convient pas.

ProductBase (classe de base des produits)

Recommandation : class

Le mot "Base" dit tout — vous avez besoin d'heritage, et cela necessite une classe. La seule question est : class ou record class ? Cela depend de la facon dont vous utilisez les instances de produits derives. N'oubliez pas : dans les chaines d'heritage, on ne peut pas melanger records et non-records.


Resume — aide-memoire

| Critere | class | struct | record | record struct | |---|:---:|:---:|:---:|:---:| | Type | reference | valeur | reference | valeur | | Allocation | heap | stack/inline | heap | stack/inline | | Heritage | oui | non | oui | non | | Egalite de valeur | manuelle | manuelle | automatique | automatique | | Validation constructeur | complete | limitee | complete | limitee | | Performance creation | plus lente | plus rapide | plus lente | plus rapide | | Passage aux methodes | reference | copie | reference | copie |

Regle empirique :

  • La plupart des types sont des classes — et c'est tres bien
  • Choisissez struct pour les petits types immuables ou la performance compte
  • Ajoutez record quand votre type est principalement un "groupe de proprietes" et que vous avez besoin de value equality
  • Record struct est le compromis ideal quand vous avez besoin de la performance de struct + la commodite de record

Et la conclusion la plus importante des benchmarks ? L'immutabilite a un cout — mais c'est la version mutable qui le paie, pas l'immuable. Si vous pouvez travailler avec des donnees en lecture seule, faites-le. Dans la plupart des applications, environ 90% du temps d'execution est consacre a la lecture des donnees — et c'est la que les types valeur brillent.