Cet article fait partie d’une série d’articles sur les nouveautés fonctionnelles de C# 13.
La version C# 13 introduit plusieurs améliorations concernant les ref struct et les variables de référence ref:
- Les
ref structont désormais la capacité d’implémenter des interfaces, - Une nouvelle contrainte de type générique permet l’utilisation d’objets
ref struct, - Les variables de référence
refpeuvent être implémentées dans les méthodes avecyield return(itérateurs) ainsi que dans les méthodesasync, - L’utilisation d’
unsafeest maintenant autorisée dans les méthodes employantyieldet les méthodesasync.
Implémentation d’interfaces par les ref struct
Nouvelle contrainte de type générique pour les objets ref struct
Variables locales ref dans les méthodes avec yield et async
Variable locale ref dans une méthode avec yield
Variable locale ref dans une méthode async
ref structLe tas managé (managed heap) héberge les objets de type référence (tels que les classes), tandis que la pile (stack) stocke généralement les objets de type valeur (comme les struct). La manipulation d’objets de type référence s’effectue via des références vers ces objets. En revanche, pour les objets de type valeur, leur manipulation peut entraîner des copies par valeur selon la méthode employée.
Bien qu’une structure soit un objet de type valeur habituellement stockée dans la pile (stack), certaines situations particulières modifient ce comportement:
- Une structure déclarée comme objet statique sera allouée dans un tas spécifique (loader heap ou high frequency heap).
- Lorsqu’une structure constitue un membre d’un objet de type référence, son stockage s’effectue dans le tas managé (managed heap).
- Le mécanisme de boxing peut également conduire au stockage d’une structure dans le tas managé.
La déclaration ref struct (disponible depuis C# 7.2) permet de créer une struct exclusivement utilisable dans la pile. Cette déclaration impose des restrictions supplémentaires à une struct afin de garantir son stockage permanent dans la pile. Ces restrictions entraînent toutefois certaines limitations d’utilisation:
- L’impossibilité d’utiliser une
ref structau sein d’un tableau. - L’interdiction pour une
ref structd’être membre d’une classe. - L’impossibilité d’utiliser une
ref structcomme argument dans une expression lambda.
Depuis C# 13/.NET 9, plusieurs limitations existantes depuis l’apparition des ref struct (C# 7.2) ont été levées:
- Une
ref structpeut désormais satisfaire une interface - Une
ref structpeut servir d’argument pour un type générique - L’utilisation de
ref structdans une méthodeasyncet dans un itérateur est maintenant autorisée.
L’utilisation de ref struct vise l’amélioration des performances en favorisant la manipulation de l’objet dans la pile plutôt que dans le tas managé. De plus, lors de la manipulation d’une ref struct, l’emploi de variables de référence ref est recommandé pour éviter les copies par valeur:
- Passage en paramètre de méthode:
Soit laref struct:ref struct RefStruct { }et la méthode:
private void PassByRef(ref RefStruct refStruct) { }Le passage en paramètre s’écrit:
var refStruct = new RefStruct(); PassByRef(ref refStruct); - Retour de fonction:
Soit la méthode:private ref RefStruct ReturnByRef(ref RefStruct refStruct) { return ref refStruct; }L’appel peut s’effectuer ainsi:
ref RefStruct returnedRef = ref ReturnByRef(ref refStruct); - Manipulation de variable locale:
var refStruct = new RefStruct(); ref RefStruct otherRefStruct = ref refStruct;
Implémentation d’interfaces par les ref struct
Depuis C# 13, les objets ref struct peuvent implémenter des interfaces, mais la conversion (cast) d’une variable de type ref struct vers une interface demeure impossible. Cette restriction persiste pour empêcher le boxing.
Considérons l’interface suivante:
internal interface IRectangle
{
int Length { get; set; }
int Width { get; set; }
int GetArea();
}
L’implémentation dans une ref struct est possible:
internal ref struct Rectangle : IRectangle
{
public int Length { get; set; }
public int Width { get; set; }
public Rectangle(int length, int width)
{
Length = length;
Width = width;
}
public int GetArea()
{
return Length * Width;
}
}
Cependant, le cast reste interdit:
Rectangle rect = new Rectangle(10, 20);
IRectangle iRect = rect; // KO
De manière similaire, le passage d’une ref struct en argument de fonction sous la forme d’une interface n’est pas autorisé, par exemple si on considère une fonction comme celle-ci:
private int CalculateArea(IRectangle rectangle)
{
return rectangle.GetArea();
}
L’appel avec une ref struct échoue:
var rectangle = new Rectangle(3, 4);
int area = CalculateArea(rectangle); // KO
Comme mentionné précédemment, cette restriction s’explique par le fait que la conversion vers l’interface nécessite du boxing pour stocker l’objet dans le tas managé. Cette opération est impossible puisque les ref struct doivent exclusivement résider dans la pile. Cette limitation concerne uniquement les ref struct et ne s’applique pas aux struct classiques.
Par exemple avec la struct:
internal struct RectangleStruct : IRectangle
{
// Même contenu que Rectangle
// ...
}
Le code suivant fonctionne:
var rectangleStruct = new RectangleStruct();
IRectangle rect = rectangleStruct;
int area = rect.GetArea();
L’examen du code MSIL correspondant révèle l’exécution d’une opération de boxing:
IL_0000: nop
// RectangleStruct rectangleStruct = new RectangleStruct(3, 4);
IL_0001: ldloca.s 0
IL_0003: ldc.i4.3
IL_0004: ldc.i4.4
IL_0005: call instance void CS13.RectangleStruct::.ctor(int32, int32)
// IRectangle rectangle = rectangleStruct;
IL_000a: ldloc.0
IL_000b: box CS13.RectangleStruct
IL_0010: stloc.1
// int area = rectangle.GetArea();
IL_0011: ldloc.1
IL_0012: callvirt instance int32 CS13.IRectangle::GetArea()
IL_0017: stloc.2
Nouvelle contrainte de type générique pour les objets ref struct
Une contrainte existante pour les objets génériques permet d’exiger que le paramètre générique soit une struct:
public class ObjectExample<T> where T: struct
{
// ...
}
Cette contrainte seule n’autorise que l’utilisation de paramètres génériques de type struct, excluant les ref struct. Pour inclure les ref struct, il faut employer la nouvelle contrainte allows ref struct. La documentation (cf. learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#allows-ref-struct) qualifie cette contrainte d’anti-contrainte, car elle n’ajoute pas de limitation supplémentaire sur le type du paramètre générique mais élargit plutôt les types acceptables.
Si on ajoute allows ref struct à l’objet précédent:
public class ObjectExample<T> where T: struct, allows ref struct
{
// ...
}
En présence de contraintes multiples :
allows ref structdoit impérativement apparaître en dernière position dans la liste des contraintes.allows ref structest incompatible avec la contrainteclass(imposant que le type soit une classe).
Exemple d’utilisation:
internal ref struct ShapeAreaCalculator<T> where T: IRectangle, allows ref struct
{
private readonly T innerShape;
public ShapeAreaCalculator(T shape)
{
innerShape = shape;
}
public int CalculateArea()
{
return innerShape.GetArea();
}
}
Bien-que la conversion de ref struct vers une interface reste impossible (comme vu précédemment), l’utilisation de la contrainte where T: IRectangle est autorisée:
Rectangle rect = new Rectangle(10, 20);
var shapeAreaCalculator = new ShapeAreaCalculator<Rectangle>(rect);
int area = shapeAreaCalculator.CalculateArea();
L’ajout de cette contrainte a permis à Microsoft d’étendre significativement la compatibilité des différents objets du framework .NET avec les ref struct. Par exemple, avant C# 13, l’utilisation de ref struct avec Func<T> était impossible :
Func<Rectangle, int> getShapeArea = (shape) => shape.GetArea(); // Possible à partir de C# 13
Microsoft a intégré la contrainte allows ref struct dans de nombreux objets du framework, notamment pour les Func<T>: src/libraries/System.Private.CoreLib/src/System/Function.cs#L9.
L’ajout de la contrainte allows ref struct entraîne des restrictions significatives. Par exemple, l’utilisation du paramètre générique comme donnée membre d’un objet impose à cet objet d’être une ref struct. Dans l’exemple précédent, l’objet ShapeAreaCalculator ne peut être ni une struct ni une classe:
internal class ShapeAreaCalculator<T> where T : IRectangle, allows ref struct // KO: impossible
{
private readonly T innerShape;
// ...
}
Une struct simple n’est également pas autorisée:
internal struct ShapeAreaCalculator<T> where T : IRectangle, allows ref struct // KO: impossible
{
private readonly T innerShape;
// ...
}
Un avantage majeur de la contrainte allows ref struct est de permettre l’utilisation de Span<T> et ReadOnlySpan<T> comme types de paramètres génériques (Span et ReadOnlySpan étant des ref struct).
Exemple:
internal ref struct ShapeCollectionAreaCalculator<T> where T : allows ref struct
{
private readonly T innerCollection;
private readonly int collectionSize;
private readonly Func<T, int, int> getItemArea;
public ShapeCollectionAreaCalculator(T collection, int collectionSize, Func<T, int, int> getItemArea)
{
this.innerCollection = collection;
this.getItemArea = getItemArea;
}
public int CalculateArea()
{
int area = 0;
for (int i = 0; i < collectionSize; i++)
{
area = +getItemArea(innerCollection, i);
}
return area;
}
}
Utilisation de cet objet:
Span<RectangleStruct> shapes = stackalloc RectangleStruct[10];
Func<Span<RectangleStruct>, int, int> getShapeArea = (span, index) => span[index].GetArea();
var shapeCalculator = new ShapeCollectionAreaCalculator<Span<RectangleStruct>>(shapes, 10, getShapeArea);
Variables locales ref dans les méthodes avec yield et async
Le mot-clé ref permet d’effectuer des manipulations par référence sur des objets de type valeur:
- Pour un objet de type valeur: on manipule une référence vers cet objet (plus précisément, on utilise un objet
refpointant vers l’objet de type valeur). - Pour un objet de type référence: on manipule une référence de la référence vers l’objet (on utilise un objet
refpointant vers la référence de l’objet de type référence, cette dernière étant elle-même un objet de type valeur).
Davantage de détails ici : cdiese.fr/csharp7-value-type-object-by-reference-valeur-par-reference/#cs7-value_type_by_ref-sumup.
Avant C# 13.0, l’utilisation de variables locales dans une méthode employant yield ou dans une méthode async était interdite.
Variable locale ref dans une méthode avec yield
Considérons l’itérateur suivant:
internal class CustomIterator : System.Collections.IEnumerable
{
private readonly CustomEnumerator enumerator;
public CustomIterator(int max)
{
enumerator = new CustomEnumerator(max);
}
public System.Collections.IEnumerator GetEnumerator()
{
return enumerator;
}
}
internal class CustomEnumerator : System.Collections.IEnumerator
{
private readonly int max;
private int current;
public CustomEnumerator(int max)
{
this.max = max;
current = -1;
}
public object Current => current;
public bool MoveNext()
{
current++;
return current < max;
}
public void Reset()
{
current = -1;
}
}
Son utilisation avec yield return:
public System.Collections.IEnumerable RefLocalVariableInIterator()
{
var customIterator = new CustomIterator(5);
foreach (var item in customIterator)
{
yield return item;
}
}
L’emploi de variables locales ref est désormais autorisé dans ce type de méthode :
public System.Collections.IEnumerable RefLocalVariableInIterator()
{
var customIterator = new CustomIterator(5);
int simpleSum = 0;
// C# 13: ref in iterator local variable
ref int refSum = ref simpleSum; // Variable locale déclarée par référence
foreach (var item in customIterator)
{
simpleSum++;
refSum = ref simpleSum;
yield return item;
}
}
Variable locale ref dans une méthode async
L’utilisation d’une variable locale ref dans une méthode async est maintenant possible, par exemple:
internal class AsyncAwaitDemo
{
private int innerValue = 0;
public async Task RunAsync()
{
int localValue = 0;
ref int refValue = ref localValue; // C# 13: ref local variable in async method
ref int refInnerValue = ref innerValue;
await Task.Delay(1000); // Asynchronously wait for 1 second
}
}
- ref and unsafe in iterators and async methods: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#ref-and-unsafe-in-iterators-and-async-methods
- Constraints on type parameters: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters
- Generic ref struct parameters: https://developers.redhat.com/articles/2025/04/21/c-13-advanced-features#generic_ref_struct_parameters
- Memory Efficiency: ref and unsafe Feature in C# 13: https://www.thetechplatform.com/post/memory-efficiency-ref-and-unsafe-feature-in-c-13