Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.
Utilisation de readonly sur les membres d’une structure
readonly sur des méthodes membres
readonly sur des propriétés
readonly au niveau d’un index
Cette fonctionnalité permet d’indiquer que des membres d’une structure ne modifient aucune données membres de cette structure. On peut ne pas comprendre au premier abord l’utilité de cette fonctionnalité car d’autres fonctionnalités déjà existantes (comme readonly struct
apparue en C# 7) permettent déjà de rendre une structure immutable. Pour comprendre son intérêt, il faut avoir en tête quelques éléments:
- Une structure
struct
est un objet de type valeur stocké le plus souvent sur la pile toutefois elle peut être stockée dans le tas managé si elle satisfait une interface ou si elle est le membre d’un objet de type référence. - Une structure
ref struct
est un objet de type valeur toujours stocké sur la pile. - Les affectations ou les passages en argument de méthode d’une structure entraînent une copie par valeur de l’objet. Cette copie peut avoir un impact sur les performances durant l’exécution dans le cas où certaines opérations sont effectuées fréquemment et si la structure contient beaucoup de membres.
- L’utilisation des mot-clés in ou ref (apparus en C# 7) permettent de manipuler des structures par référence et ainsi éviter des copies lors des affectations ou des passages en argument si la structure est immutable. Dans le cas où la structure n’est pas immutable, le runtime peut effectuer des defensive copies (on explique par la suite ce qu’est une defensive copy) dégrandant elles-aussi les performances.
- Pour qu’une structure soit immutable par syntaxe, on peut utiliser les mots-clés
readonly struct
(oureadonly ref struct
dans le cas d’uneref struct
).
Le gros inconvénient de readonly struct
et readonly ref struct
est qu’ils rendent la structure complètement immutable et qu’il n’y a pas d’autre granularité possible. C# 8.0 permet d’utiliser readonly
à un niveau plus fin en autorisant à l’appliquer sur une méthode membre, des propriétés ou des index.
readonly
ne s’applique qu’aux objets struct
et ref struct
On peut utiliser le mot-clé readonly
sur des méthodes, sur des propriétés ou sur des index d’une structure ou d’un objet de type ref struct
pour indiquer au compilateur que l’opération ne modifie pas la structure. Il n’est pas possible d’appliquer ce mot-clé dans le cas d’une classe.
En effet, readonly
permet de se prémunir des defensive copies qui peuvent se produire dans le cas d’objet de type valeur comme les structures. Les classes sont des objets de type référence stockés dans le tas managé et manipulés avec des références. Elles ne sont pas concernées par les defensive copies.
A l’opposé, readonly
au niveau d’une donnée membre peut s’appliquer dans le cas d’une structure et d’une classe.
Utilisation de readonly sur les membres d’une structure
readonly sur des méthodes membres
readonly
peut être appliquer sur des méthodes membres d’une structure.
Par exemple, si on considère la structure suivante:
public struct Circle
{
public int radius;
public Circle(int radius)
{
this.radius = radius;
}
// Modifie une donnée membre
public void UpdateRadius(int newRadius)
{
this.radius = newRadius;
}
// Ne modifie pas la structure
public int AddToRadius(int number)
{
return this.radius + number;
}
}
On souhaite pouvoir utiliser cette structure de façon à ce qu’elle soit immutable et en évitant dans certains cas les defensive copies:
- Si on appelle seulement
AddToRadius()
, la structure reste immutable toutefois il peut y avoir quand même des defensive copies car le compilateur ne sait pas siAddToRadius()
réellement ou non la structure. - Si on rend la structure immutable en la déclarant avec
readonly
:public readonly struct Circle { ... }
Il y aura une erreur de compilation car l’affectation dans
UpdateRadius()
n’est plus possible.
Permettre de rajouter readonly
au niveau des fonctions membres, des propriétés ou des index permet d’indiquer l’aspect immutable d’une opération sur une structure à un niveau plus fin pour éviter que toute la structure soit immutable.
Dans le cas de l’exemple précédent, si on modifie le code de cette façon:
public struct Circle
{
public int radius;
public Circle(int radius)
{
this.radius = radius;
}
public void UpdateRadius(int newRadius)
{
this.radius = newRadius;
}
public readonly int AddToRadius(int number)
{
return this.radius + number;
}
}
Si on exécute seulement la fonction AddToRadius()
, la structure est immutable et il n’y a pas de defensive copies. On peut, toutefois, effectuer des opérations rendant la structure mutable avec UpdateRadius()
.
readonly sur des propriétés
On peut appliquer readonly
sur des propriétés de façon à indiquer que l’utilisation de la propriété ne modifie pas la structure.
Par exemple, si on considère l’exemple précédent de la structure Circle
, on peut appliquer readonly
au niveau d’un accesseur:
- En lecture seulement:
public struct Circle { private int radius; public int Radius { readonly get => this.radius; set => this.radius = value; } // ... }
- En écriture seulement:
public struct Circle { private int radius; public int Radius { get => this.radius; readonly set => Console.WriteLine(value); } // ... }
Une erreur survient à la compilation si on applique une opération en écriture avec
readonly set => ...
. - En lecture et en écriture en mettant
readonly
au niveau de la propriété plutôt que des accesseurs:public struct Circle { private int radius; public readonly int Radius { get => this.radius; set => Console.WriteLine(value); } // ... }
readonly au niveau d’un index
readonly
peut être appliqué au niveau d’un index de la même façon que pour les propriétés. Si on considère la structure suivante:
public struct Numbers
{
private int[] numbers;
private Numbers(int count)
{
this.numbers = new int[count];
}
public int this[int i]
{
get => this.numbers[i];
set => this.numbers[i] = value;
}
}
On peut indiquer que l’utilisation de l’index ne modifie pas la structure:
- En lecture seulement:
public int this[int i] { readonly get => this.numbers[i]; set => this.numbers[i] = value; }
- En écriture seulement:
public int this[int i] { get => this.numbers[i]; readonly set => this.numbers[i] = value; }
- En lecture et en écriture:
public readonly int this[int i] { get => this.numbers[i]; set => this.numbers[i] = value; }
readonly
et ref readonly
Même si le mot-clé readonly
est utilisé dans les 2 cas, readonly
utilisé pour indiquer qu’une opération ne modifie pas une structure et ref readonly
sont 2 notions différentes:
readonly
sur les membres d’une structure permet d’éviter les defensive copies lors d’opérations appliquées à la structure.ref readonly
permet d’indiquer qu’un objet de type valeur est manipulé par référence et non par valeur.
Par exemple, si on considère la structure suivante:
public struct IntRefWrapper
{
private int[] numbers;
public IntRefWrapper(int count)
{
this.numbers = new int[count];
}
public readonly ref readonly int GetIntByRef(int index)
{
return ref this.numbers[index];
}
}
Dans la fonction GetIntByRef()
, on utilise les 2 notions:
public readonly
permet d’indiquer que la méthode ne modifie pas la structure.ref readonly int
indique le type de retour de la fonction est un objet de typeint
retourné par référence.
Précisions sur les defensive copies
Pour se rendre compte des defensive copies, on peut considérer l’exemple de la structure suivante:
public struct Circle
{
public int radius;
public Circle(int radius)
{
this.radius = radius;
}
public void UpdateRadius(int newRadius)
{
this.radius = newRadius;
}
}
Cette structure est mutable à cause de la méthode UpdateRadius()
qui permet de modifier la donnée membre radius
.
Si on considère la méthode suivante:
public static void ChangeRadius(int newRadius, in Circle circle)
{
circle.UpdateRadius(newRadius);
Console.WriteLine(circle.radius);
}
Cette méthode utilise le paramètre Circle circle
avec le mot-clé in
de façon à ce que ce soit une référence du paramètre en lecture seule qui soit utilisée et éviter une copie par valeur de l’objet (pour plus de détails sur in
voir Manipuler des objets de type valeur par référence). Le gros inconvénient de in
est qu’il entraîne un defensive copy, on peut s’en rendre compte si on exécute le code suivant:
var circle = new Circle(4);
ChangeRadius(3, circle); // 4
Console.WriteLine(circle.radius); // 4
radius
contient toujours 4
car in
impose que circle
dans ChangeRadius()
soit en lecture seule. Le compilateur effectue une defensive copy pour assurer que circle
n’est effectivement pas modifié dans le corps de ChangeRadius()
. On modifie le code de la structure Circle
pour afficher l’adresse de l’objet:
public struct Circle
{
// ...
public void UpdateRadius(int newRadius)
{
// On commente volontairement cette ligne de façon à rendre la structure immutable
//this.radius = newRadius;
// Permet d’afficher l’adresse mémoire de l’instance
unsafe
{
fixed (Circle* ptr = &this)
{
Console.WriteLine(new IntPtr(ptr));
}
}
}
}
Si on exécute le même code, on s’aperçoit que l’adresse est différente à cause de la defensive copy:
var circle = new Circle(4);
// Permet d’afficher l’adresse mémoire de circle
unsafe
{
fixed (Circle* ptr = &circle)
{
Console.WriteLine(new IntPtr(ptr));
}
}
ChangeRadius(3, circle);
Console.WriteLine(circle.radius);
Le résultat est:
347086513304 347086512744 4 4
L’adresse est différente à cause de la copie même si la structure est maintenant immutable. Pour empêcher cette copie, on peut indiquer au compilateur que la structure est immutable en modifiant sa déclaration en readonly struct
:
public readonly struct Circle
{
// ...
}
Si on re-éxécute le code, les adresses sont maintenant identiques car il n’y a plus de defensive copy:
950267405944 950267405944 4 4
Comme on l’a indiqué plus haut, on peut éviter de rendre toute la structure immutable avec readonly struct
. On peut se contenter d’indiquer au compilateur que l’appel à UpdateRadius()
ne modifie pas la structure en rajoutant readonly
au niveau de la fonction uniquement. L’implémentation de la structure devient:
public struct Circle
{
public int radius;
public Circle(int radius)
{
this.radius = radius;
}
public readonly void UpdateRadius(int newRadius)
{
//this.radius = newRadius;
// Permet d’afficher l’adresse mémoire de l’instance
unsafe
{
fixed (Circle* ptr = &this)
{
Console.WriteLine(new IntPtr(ptr));
}
}
}
}
Si on exécute le code suivant:
ChangeRadius(3, circle);
Console.WriteLine(circle.radius);
Le résultat est:
950267448258 950267448258 4 4
Les adresses mémoire de la structure sont les mêmes avant et après exécution de la méthode UpdateRadius()
. Dans ce cas là, il n’y a pas non plus de defensive copy. L’utilisation de readonly
au niveau de la méthode permet d’éviter de rendre toute la structure immutable. On peut, ainsi, implémenter dans la même structure:
- des méthodes qui ne modifient pas de données membres qui seront réservées aux endroits critiques où il ne faut pas que le runtime effectue des defensive copies.
- d’autres méthodes modifiant éventuellement des données membres dans la structure.
Associer ces 2 types de méthodes n’est pas possible avec une readonly struct
.
readonly protège seulement des affectations
Utiliser readonly
au niveau des membres d’une structure permet d’empêcher les nouvelles affectations de données membres dans la structure. Si on tente d’effectuer ce type d’affectation, une erreur de compilation surviendra. En revanche, si on tente de modifier une donnée membre sans effectuer d’affectation, il n’y aura pas d’erreur de compilation.
Si on considère le code suivant proche de l’exemple précédent:
public struct Numbers
{
private int id; // Objet de type valeur
private List<int> numbers; // Référence vers un objet de type référence
public Numbers(int id, IEnumerable<int> numbers)
{
this.id = id;
this.numbers = new List<int>(numbers);
}
public readonly int ID => this.id;
public readonly IList<int> numbers => this.numbers;
public readonly void AddNumber(int newNumber)
{
this.numbers.Add(newNumber);
}
public void UpdateID(int newId)
{
this.id = newId,
}
}
// ...
public static void ChangeIDAndAddNumber(in Numbers numbers, int newId, int newNumber)
{
numbers.UpdateID(newId);
numbers.AddNumber(newNumber);
Console.WriteLine($" ID: {numbers.ID}; item count: {numbers.Items.Count()}");
}
Dans l’implémentation de la structure, on peut constater que la signature public readonly void AddNumber()
n’empêche pas de modifier le membre numbers
avec this.numbers.Add(newNumber)
. Il n’y a pas d’erreur de compilation.
Si on exécute ce code:
var numbers = new Numbers(1, new int[] { 1, 2, 3});
Console.WriteLine($" ID: {numbers.ID}; item count: {numbers.Items.Count()}");
ChangeIDAndAddNumber(numbers, 2, 4);
Le résultat est:
ID: 1; item count: 3 ID: 1; item count: 4
Le résultat est similaire à l’exemple de la partie précédente. La defensive copy qui est effectuée dans la méthode ChangeIDAndAddNumber()
entraîne que l’objet d’origine n’est pas modifié, la valeur de ID
est toujours la même. En revanche le nombre d’éléments dans la liste numbers
est différent car cette liste est modifiée.
Ainsi, readonly
ne sert à se prémunir que des defensive copies qui peuvent se produire dans le cas d’objets de type valeur. Le membre numbers
dans la structure Numbers
est une référence vers une liste qui est un objet de type référence. La liste est stockée dans le tas managée mais la référence numbers
(la référence est un objet de type valeur) est stockée dans la pile comme la structure Number
. La liste peut être modifiée dans le tas managée toutefois la référence dans la structure n’est pas modifiée. C’est la raison pour laquelle malgré la defensive copy, le nombre d’éléments de la liste change.
Pour résumer, readonly
au niveau des membres d’une structure permet d’empêcher de modifier les membres de la structure par affectation toutefois, les membres de type référence peuvent être modifiés.