Les structures ref en C#, introduites dans la version 7.2, sont des types spéciaux qui ne peuvent être alloués que sur la pile. Elles empêchent la mise en boîte (conversion en object), ce qui élimine les coûts de ramasse-miettes pour les opérations critiques.
Auparavant, des fonctionnalités similaires nécessitaient des pointeurs non sécurisés. Les structures ref permettent désormais une gestion mémoire efficace dans du code sécurisé. Depuis C# 13, elles peuvent implémenter des interfaces, augmentant leur flexibilité dans la conception de composants performants.
Cas d'usage principaux et exemples de code
1. Calculs numériques haute performance
public ref struct Matrice3D
{
public float A1, A2, A3;
public float B1, B2, B3;
public float C1, C2, C3;
public void Multiplier(ref Matrice3D autre)
{
// Opérations de multiplication directement sur la pile
// ... implémentation performante ...
}
}
// Utilisation
var matrice1 = new Matrice3D();
var matrice2 = new Matrice3D();
matrice1.Multiplier(ref matrice2); // Aucune allocation sur le tas
2. Parseurs sensibles à la mémoire
public ref struct ParseurJson
{
private readonly ReadOnlySpan<char> _donneesJson;
public ParseurJson(string json) => _donneesJson = json.AsSpan();
public bool EssayerParseEntier(out int valeur)
{
// Analyse directe sur le Span source
if (int.TryParse(_donneesJson, out valeur))
return true;
valeur = default;
return false;
}
}
// Utilisation - évite la copie de grandes chaînes
var parseur = new ParseurJson(grandeChaineJson);
parseur.EssayerParseEntier(out int identifiant); // Sans allocation tas</char>
3. Manipulation mémoire bas niveau
unsafe ref struct RedacteurMemoire
{
private Span<byte> _tampon;
private int _position;
public RedacteurMemoire(Span<byte> tampon) => _tampon = tampon;
public void EcrireEntier(int valeur)
{
// Écriture directe dans le Span mémoire
BinaryPrimitives.WriteInt32LittleEndian(
_tampon.Slice(_position), valeur);
_position += sizeof(int);
}
}
// Utilisation
byte[] donnees = new byte[1024];
var redacteur = new RedacteurMemoire(donnees);
redacteur.EcrireEntier(42); // Manipulation directe du tableau</byte></byte>
4. Combinaison avec Span alloué sur la pile
public ref struct ProcesseurImage
{
public Span<byte> Pixels;
public void AppliquerNiveauxGris()
{
for (int i = 0; i < Pixels.Length; i += 4)
{
byte gris = (byte)((Pixels[i] + Pixels[i+1] + Pixels[i+2]) / 3);
Pixels[i] = Pixels[i+1] = Pixels[i+2] = gris;
}
}
}
// Utilisation
byte[] donneesImage = ObtenirGrandeImage();
new ProcesseurImage { Pixels = donneesImage }.AppliquerNiveauxGris(); // Traitement sans copie</byte>
Caractéristiques et limitations clés
| Caractéristique | Description | Impact sur les performances |
|---|---|---|
| Allocation exclusive sur la pile | Pas de mise en boîte, ne peut pas être champ de classe, ne peut pas implémenter d'interface | Évite les pauses du ramasse-miettes |
| Zéro copie mémoire | Manipulation directe des Spans mémoire | Réduit les coûts de copie de gros objets |
| Pas d'appels de méthode virtuelle | Interdiction d'implémenter des interfaces | Élimine les recherches de table de fonctions virtuelles |
| Limitation de cycle de vie | Ne peut pas traverser les frontières await/yield | Garantit la sécurité de l'allocation sur la pile |
Quand éviter leur utilisation
- Stockage à long terme : ne peuvent pas être stockés dans des objets tas (comme les champs de classe). ```
class Cache {
// Erreur : ref struct ne peut pas être un champ de classe
// private Matrice3D _matriceEnCache;
}
- Dans les méthodes asynchrones : ne peuvent pas traverser les points await. ```
async Task TraiterAsync() {
var parseur = new ParseurJson(json); // Autorisé
await Task.Delay(100); // Point asynchrone
// Erreur : le cycle de vie de parseur est terminé
// parseur.EssayerParseEntier(out int valeur);
}
- Pour un comportement polymorphe : ne peuvent pas implémenter d'interface ou servir de classe de base. ```
// Erreur : ref struct ne peut pas implémenter d'interface
// public ref struct Vecteur : IComparable { ... }
Guide des meilleures pratiques
- Prioriser leur utilisation dans les chemins d'exécution critiques pour la performance.
- Maintenir une petite taille (moins de 128 octets) pour éviter les débordements de pile.
- Combiner avec
Span<T>pour un traitement efficace des tranches mémoire. - Clarifier le cycle de vie pour garantir qu'il ne dépasse pas les frontières de sécurité.
// Exemple performant : pipeline de traitement d'image
public static void TraiterImage(Span<byte> pixels)
{
// Chaîne de processeurs créée sur la pile
var etape1 = new AjusteurCouleur(pixels);
var etape2 = new ReducteurBruit(pixels);
var etape3 = new Netteteur(pixels);
etape1.Executer();
etape2.Executer();
etape3.Executer(); // Toutes les opérations sur la pile
}</byte>