Unmanaged constructed types (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

Le titre de cette fonctionnalité a été gardée en anglais car la traduction française de “constructed type” n’est pas vraiment utilisée.

Cette fonctionnalité permet d’étendre les types pouvant être utilisés lors d’appels à du code non managés, les arguments de ces appels devant être non managés.

Avant de commencer…

Quelques définitions:

Generic type vs constructed type

Les types génériques (i.e. generic type) sont des types comportant des arguments de type avec < >. Il peut n’y avoir aucun argument ou plusieurs arguments, par exemple:

  • int est un type non générique car il ne comporte pas de <>.
  • List<> est un type générique même s’il n’y a aucun argument de type. Il n’est pas possible d’instancier une classe de type List<> toutefois le type List<> existe.
  • List<T> est un type générique avec un argument de type non défini.
  • List<int> est un type générique avec un argument de type défini.
  • Dictionary<int, string> est un type générique avec des arguments définis.

Un constructed type est un sous-ensemble des types génériques, il s’agit d’un type générique comportant au moins un argument de type défini, par exemple:

  • List<> n’est pas un constructed type car il n’y a pas d’argument de type.
  • List<T> n’est pas un constructed type car l’argument de type n’est pas défini.
  • List<int> est un constructed type car l’argument de type est défini.
  • Dictionary<int, T> est un constructed type car il existe au moins un argument de type défini.

Type non managé

Un type non managé (i.e. unmanaged type) est un type d’objet qui peut ne pas être géré par le garbage collector. Les objets de types non managés peuvent, par exemple, être stockés sur la pile et non obligatoirement dans le tas managé. A l’opposé, les types managés ne peuvent pas être stockés sur la pile et sont exclusivement stockés dans le tas managé.

L’ensemble des types non managés est proche de celui des types blittables. Les types blittables qualifient les types dont la représentation est similaire en mémoire entre du code managé et du code natif. Ces types permettent d’effectuer des appels à du code natif en utilisant, par exemple, Platform Invoke. Les types non managés sont des types blittables toutefois l’inverse n’est pas forcément vrai (par exemple des objets complexes comme les classes peuvent devenir blittables suivant certaines conditions).

A l’opposé des types blittables, les types non managés peuvent être utilisés en dehors d’appels à du code natif.

Les types non managés sont:

  • Les types primitifs comme bool, byte, short, int, long, char, double, décimal et leurs équivalents non signés.
  • Les types enum
  • Le type pointeur
  • Les structures non managées c’est-à-dire les structures ne comportant que des membres non managés.

Par exemple, si on considère le code suivant:

int simpleInt = 5;
unsafe
{
  int* ptr = &simpleInt;
  Console.WriteLine(new IntPtr(ptr));
}

Ce code permet d’afficher la valeur d’un pointeur d’un entier stocké sur la pile. L’objet simpleInt de type int est stocké sur la pile et comme int est un type non managé, le compilateur autorise à utiliser l’opérateur & pour obtenir un pointeur vers cet objet. Ainsi en dehors de tableau et du type string, si le compilateur autorise & alors le type est non managé.

Obtenir un pointeur vers un objet de type référence avec fixed

A partir de C# 7.3, il est possible de manipuler un pointeur vers n’importe quel objet de type référence stocké dans le tas managé avec le mot-clé fixed (pour plus de détails, voir Amélioration de fixed en C# 7).

Structure managée vs structure non managée

Si on considère la structure suivante:

public struct SimpleStruct
{
  public int innerInt; // Type non managé
}

Cette structure est non managée car elle ne contient qu’un membre qui est non managé, on peut donc écrire:

SimpleStruct simpleStruct = new SimpleStruct() { innerInt = 5 };
unsafe
{
  SimpleStruct* ptr = &simpleStruct; // OK
  // ...
}

Pas d’erreur, on peut obtenir un pointeur avec &.

Si on modifie la structure en ajoutant un membre de type référence:

public struct SimpleStruct
{
  public int innerInt; // Type non managé
  public List<int> intList; // Type managé
}

On ne peut plus extraire le pointeur car la structure n’est plus un type non managé. Le membre intList est un objet de type référence stocké dans le tas managé donc la structure est stockée dans le tas managé. Par suite elle devient un type managé:

SimpleStruct simpleStruct = new SimpleStruct() {
  innerInt = 5,
  intList = new List<int>()
};
unsafe
{
  SimpleStruct* ptr = &simpleStruct; // ERREUR
  // ...
}

Le compilateur n’autorise plus l’utilisation de &:

Error CS0208: Cannot take the address of, get the size of, or declare a pointer to a managed type ...

Enfin une structure non managée peut aussi être stockée dans le tas managé si elle est le membre d’un objet stocké dans le tas managé.

Les tableaux

Les objets de type référence sont managés toutefois les tableaux qui sont des objets de type référence peuvent être non managés. Par exemple, un tableau peut être alloué sur la pile en utilisant stackalloc.

Constructed types non managés

C# 8.0

Avant C# 8.0, tous les objets avec un constructed type étaient managés. A partir de C# 8.0, les contructed types peuvent être non managés si les paramètres de type sont non managés.

Par exemple, si on considère la structure:

public struct GenericStruct<T1, T2>
{
  public T1 innerVar1;
  public T2 innerVar2;
}

Cette structure est générique avec des arguments de type non définis. Si on l’utilise avec des type non managés alors cette structure sera non managées:

var genericStruct = new GenericStruct<int, float>{
  innerVar1 = 1,
  innerVar2 = 1f;
};

unsafe
{
  GenericStruct<int, float>* ptr = &amp;genericStruct; // OK la structure est non managée
  Console.WriteLine(ptr->innerVar1);
  Console.WriteLine(ptr->innerVar2);
}

En revanche, si un argument de type ne permet pas de créer des objets non managés, la structure ne pourra pas être non managées:

var genericStruct = new GenericStruct<int, string>{
  innerVar1 = 1,
  innerVar2 = "";
};

unsafe
{
  GenericStruct<int, string>* ptr = &amp;genericStruct; // ERREUR genericStruct est managée
  // ...
}

string est un type référence donc la structure ne permet pas de créer des objets non managés.

Contrainte unmanaged

C# 7.3

A partir de C# 7.3, on peut utiliser la contrainte unmanaged pour indiquer qu’un argument de type doit être non managé:

public struct StructName<T> where T: unmanaged {}

Par exemple, si on prend l’exemple précédent, on peut définir la structure suivante:

public struct GenericStruct<T1, T2>
  where T1: unmanaged
  where T2: unmanaged
{
  public T1 innerVar1;
  public T2 innerVar2;
}

Il devient alors impossible d’instancier des objets avec des arguments de type qui ne sont pas non managés:

var genericStruct = new GenericStruct<int, float>(); // OK
var genericStruct = new GenericStruct<int, string>(); // ERREUR

Share on RedditTweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Enumérer de façon asynchrone (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

C# 8.0 apporte un cas d’utilisation supplémentaire au pattern async/await en permettant d’énumérer de façon asynchrone (i.e. asynchronous streams).

Rappels concernant yield

Pour rappel, le mot-clé yield a été introduit en C# 2.0 de façon à créer un énumérateur à la volée et de contrôler l’énumération avec:

  • yield return pour renvoyer un objet à la volée lors de l’énumération.
  • yield break pour arrêter l’énumération.

Par exemple, si on considère la fonction suivante:

public int[] GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  var numbers = new int[numberCount];  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = random.Next(0, 100);  
    Console.WriteLine($"Generating {number}");  

    numbers[i] = number;  
  }  

  return numbers;  
}   

A l’exécution, tout le tableau de nombre devra être complêté pour sortir de la fonction et pour commencer l’énumération dans la boucle foreach:

foreach (var randomNumber in GetRandomNumbers(10000))  
{  
  Console.WriteLine(randomNumber);  
  if (randomNumber == 3)  
  {  
    Console.WriteLine("3 found");  
    break;  
  }  
}  

Tant que la fonction GetRandomNumbers() n’a pas terminé son exécution, l’énumération ne peut pas commencer. Ainsi si randomNumber est égal à 3 à la 1ère itération, on aura généré 9999 nombres inutilement puisqu’ils ne serviront pas pour le reste de l’exécution:

Generating 3
Generating 32   
Generating 78  
...  
Generating 53  
3 found  

Si on utilise yield, l’implémentation devient:

public IEnumerable<int> GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = random.Next(0, 100);  
    Console.WriteLine($"Generating {number}");  
    yield return number;  
  }  
}  

A l’exécution de la boucle foreach, l’énumération commence sans exécuter GetRandomNumbers() complêtement. Chaque itération va exécuter le contenu de GetRandomNumbers(), si randomNumber est égal à 3 à la 1ère itération, le contenu de GetRandomNumbers() ne sera exécuté qu’une seule fois:

Generating 3
3 found  

Enumérer de façon asynchrone

C# 8.0 permet d’effectuer une énumération avec yield de façon asynchrone. Dans la fonction dans laquelle se trouve yield, la syntaxe doit être du type:

async IAsyncEnumerable EnumerationFunction(...)  
{  
  // ...  

  await ... // Code exécuté de façon asynchrone  
  yield return ... // Valeur renvoyée lors de l'énumération
}  

Syntaxe de l’énumération

Avec await foreach

Pour effectuer l’énumération asynchrone, on peut utiliser une boucle await foreach:

await foreach (var item in EnumerationFunction())  
{  
  // ...  
}  

Par exemple, si on reprend l’exemple précédent:

public async IAsyncEnumerable<int> GetRandomNumbers(int numberCount)  
{  
  Random random = new Random();  
  for (int i = 0; i < numberCount; i++)  
  {  
    int number = await Task.Run(() => random.Next(0, 100)); // Code exécuté de façon asynchrone
    Console.WriteLine($"Generating {number}");  
    yield return number; // Valeur renvoyée lors de l'énumération
  }  
}  

L’énumeration avec await foreach peut être effectuée de cette façon:

await foreach (var randomNumber in GetRandomNumbers(10000))  
{  
  Console.WriteLine(randomNumber);  
  if (randomNumber == 3)  
  {  
    Console.WriteLine("3 found");  
    break;  
  }  
}  

Avec une itération manuelle

Dans le cas où on itère de façon manuelle c’est-à-dire sans utiliser foreach, on peut utiliser:

Dans le cas de l’exemple précédent, le code deviendrait:

await using (IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator())  
{  
  while (await enumerator.MoveNextAsync())  
  {  
    if (enumerator.Current == 3)  
    {  
      Console.WriteLine("3 found");  
      break;  
    }  
  }  
}  

Cette syntaxe dispose l’objet IAsyncEnumerator<T> de façon asynchrone avec await using (voir “Disposer des objets de façon asynchrone” pour plus de détails).

Implémentation avec ConfigureAwait(false)

Il est possible de rajouter ConfigureAwait(false) pour optimiser le code en évitant d’utiliser le contexte d’exécution d’origine lors de l’exécution de la continuation avec async/await.

Par exemple:

  • Dans une boucle await foreach:
    await foreach (var randomNumber in GetRandomNumbers(10000).ConfigureAwait(false))  
    {  
      // ...  
    } 
    
  • Dans le cas d’une itération manuelle:
    IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator();  
    await using (var _ = enumerator.ConfigureAwait(false))  
    {  
      // ...  
    }  
    

    Ou

    IAsyncEnumerator<int> enumerator = GetRandomNumbers(10000).GetAsyncEnumerator();  
    await using var _ = enumerator.ConfigureAwait(false);  
    
    // ...  
    

Utiliser un CancellationToken

On peut introduire un CancellationToken pour interrompre l’exécution du code asynchrone.

Par exemple:

  • Dans une boucle await foreach:
    var tokenSource = new CancellationTokenSource();  
    await foreach (var randomNumber in GetRandomNumbers(10000).WithCancellation(tokenSource.Token))  
    {  
      // ...  
    }  
    
  • Dans le cas d’une itération manuelle:
    var tokenSource = new CancellationTokenSource();  
    var token = tokenSource.Token; 
    await using (var enumerator = GetRandomNumbers(10000).WithCancellation(token).GetAsyncEnumerator())  
    {  
      // ...  
    }  
    

    Ou

    await using var enumerator = GetRandomNumbers(10000).WithCancellation(token).GetAsyncEnumerator();   
    // ...  
    
Share on RedditTweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Disposer des objets de façon asynchrone (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

C# 8.0 apporte un cas d’utilisation supplémentaire au pattern async/await en permettant de disposer des objets de façon asynchrone avec using (i.e. asynchronous disposable).

Rappels sur async/await

Pour rappel async/await est apparu avec C# 5 de façon à faciliter l’implémentation de traitements asynchrones. Brièvement la syntaxe de cette implémentation doit être utilisée de cette façon:

  • async est placé dans la signature de la fonction comportement un traitement pouvait être exécuté de façon asynchrone.
  • await est placé devant l’instruction générant une tâche dont l’exécution est lancée et dont il faudra attendre la fin de l’exécution. L’instruction doit générer un objet dont le type est “awaitable” comme Task, Task<T>, ValueTask ou ValueTask<T> (ValueTask et ValueTask<T> apparus à partir de C# 7.0 sont des objets de type valeur équivalent à Task et Task<T>).
  • Dans une méthode asynchrone, tout ce qui se trouve après l’instruction await sera exécutée sous la forme d’une continuation.
  • Une méthode dont le corps contient une ou plusieurs instructions avec await doit comporter async dans sa signature.
  • Si une méthode contient async, il n’est pas obligatoire que le corps de la méthode contienne await. Si une méthode async ne contient aucune instruction avec await, l’exécution sera effectuée de façon synchrone.

Par exemple, si on considère la fonction suivante:

public async Task ExecuteAsynchronously() 
{ 
  await Task.Delay(10000); // On simule l'exécution d'un traitement dans une Task 
} 

On peut appeler cette méthode de cette façon:

// La méthode appelante doit comporter async si on utilise await
public async Task CallingMethod()  
{ 
  await ExecuteAsynchronously(); 
} 

Ou

// async n'est pas nécessaire si on n'utilise pas await 
public void CallingMethod()  
{ 
  // On ne fait qu'attendre le résultat d'une Task 
  ExecuteAsynchronously().Wait();  
  // ou 
  ExecuteAsynchronously().Result; 
} 

Le code dans ExecuteAsynchronously() est exécuté de façon asynchrone par rapport au code se trouvant à l’extérieur de ExecuteAsynchronously(). Cela signifie que:

  • L’appel await ExecuteAsynchronously() rendra la main immédiatement au code appelant de façon à ne pas bloquer son exécution.
  • Le code dans ExecuteAsynchronously() sera exécuté dans une Task différente du code appelant. Concrêtement et suivant le contexte d’exécution, cette Task pourra être exécutée dans un thread différent du thread appelant mais ce n’est pas obligatoire.
  • Dans l’appel, await signifie que le code appelant va attendre la fin de l’exécution de la Task dans ExecuteAsynchronously() pour continuer l’exécution.

Exécution asynchrone ne signifie pas forcément que l’exécution s’effectue en parallèle:

  • Exécution asynchrone signifie que les exécutions de tâches sont effectuées sans qu’elles ne soient synchronisées. Une exécution de tâches asynchrones peut être effectuée par un seul thread de façon séquentielle.
  • Exécution en parallèle signifie que des tâches peuvent être exécutées éventuellement dans des threads différents et tirer parti d’un processeur multithreadé ou d’une machine multiprocesseurs.

Si on souhaite exécuter du code de façon concurrente avec le code dans ExecuteAsynchronously(), on peut utiliser la syntaxe suivante:

Task asyncTask = ExecuteAsynchronously(); 
// Code exécuté de façon concurrente 
// ... 

await asyncTask; 
// Code exécuté sous forme de continuation 
// ... 

On peut se rendre de l’aspect asynchrone de l’exécution de code avec await dans le cas d’une implémentation d’une interface graphique avec une boucle de messages comme en WPF ou en WinForms:

  • Si on lance des traitements de façon synchrone dans le thread graphique, la boucle de message de l’interface est bloquée et les éléments graphiques sont inaccessibles ou bloqués pendant le traitement.
  • Si on lance des traitements de façon asynchrone avec await dans le thread graphique, la boucle de message est toujours exécutée et les éléments graphiques restent accessibles pendant le traitement.

Disposer des objets de façon asynchrone

Le garbage collector (GC) permet de gérer la durée de vie des objets managés d’un processus .NET: lorsqu’un objet n’apparait plus dans le graphe des objets indiquant les dépendances entre les objets, il est “supprimé”. Cette suppression se fait en récupérant l’espace mémoire occupé lors de l’étape Collect.

Finalize() et Dispose()

Avant l’étape Collect, il peut être nécessaire d’exécuter du code pour libérer des dépendances de l’objet à collecter:

  • Méthode Finalize(): cette méthode n’est pas directement exposée au développeur. Le garbage collector l’exécute avant l’étape Collect. Une partie du code de cette méthode peut être personnalisé en implémentant un destructeur, par exemple pour libérer des dépendances non managées. En effet, le garbage collector n’intervient que sur les objets managés du processus, si le processus manipule des objets non managés, il faut les libérer explicitement.
  • Méthode Dispose(): cette méthode est implémentée dans les objets satisfaisant l’interface IDisposable:
    public class DisposableObject : IDisposable  
    {  
      public void Dispose()  
      {
        // ...
      }
    }
    

Le garbage collector n’exécute pas la méthode Dispose(), elle doit être appelée explicitement par du code de façon à libérer des dépendances managées ou non managées d’un objet. L’appel à Dispose() peut se faire en appelant directement cette méthode ou en utilisant using:

using (<objet satisfaisant IDisposable>)  
{  
  // ...
}  

A la fin du bloc de code using, la méthode Dispose() de l’objet satisfaisant IDisposable sera exécutée.

Par exemple:

var disposableObject = new DisposableObject(); 
using (disposableObject) 
{ 
  // ... 
} 
// ATTENTION: à ce stade disposableObject ne doit pas être utilisé car Dispose() a été exécuté 

Ou plus directement:

using (var disposableObject = new DisposableObject()) 
{ 
  // ... 
} 

L’exécution de la méthode Dispose() ne dispense pas le garbage collector d’exécuter la méthode Finalize() lorsque l’objet est libéré. Pour éviter au garbage collector d’exécuter inutilement Finalize(), on peut rajouter GC.SuppressFinalize() dans la méthode Dispose() de façon à indiquer au GC qu’il n’est pas nécessaire d’exécuter Finalize().

Dans le cas où il est nécessaire de libérer des dépendances d’un objet, une bonne implémentation doit:

  1. Satisfaire IDisposable
  2. L’implémentation de la méthode Dispose() doit indiquer au garbage collector de ne pas exécuter Finalize() avec GC.SuppressFinalize(),
  3. Eventuellement libérer des dépendances managées et non managées dans la méthode Dispose().
  4. Comporter un destructeur si des dépendances non managées doivent être libérées. L’implémentation du destructeur peut être nécessaire si la méthode Dispose() n’est pas appelée explicitement dans tous les cas de figure.
  5. Le destructeur peut libérer les dépendances non managées et supprimer les liens entre l’objet et d’autres objets managés en affectant null aux données membres. Supprimer ce lien permet au garbage collector de supprimer les instances inutiles dans le graphe des objets.
  6. L’appel à Dispose() ne doit être fait qu’une seule fois: l’implémentation doit être idempotente c’est-à-dire qu’elle doit permettre d’être exécutée plusieurs fois mais les objets ne doivent être libérés qu’une seule fois. Ainsi avant d’exécuter Dispose(), il est nécessaire de vérifier que l’exécution n’a pas été déjà effectuée (c’est l’intérêt du membre disposed dans l’implémentation de DisposableObject plus bas).

Dans la documentation, on peut trouver un exemple d’implémentation prenant en compte tous ces éléments. Si on prend l’exemple d’une dépendance de type SqlConnection à libérer (System.Data.SqlClient.SqlConnection dérive de System.Data.Common.DbConnection qui satisfait IDisposable):

class DisposableObject : IDisposable // (1) 
{  
  bool disposed = false; 
  private SqlConnection dependency = new SqlConnection("Test connection");  

  public void Dispose()  
  {  
    this.Dispose(true); // (3)

    GC.SuppressFinalize(this); // (2) 
  }  
  
  private void Dispose(bool disposing)  
  {  
    if (this.disposed) // (6) 
      return;  

    if (disposing) 
    {  
      // Libération de dépendances managées  
      if (this.dependency != null) 
        this.dependency.Dispose(); 
    }  

    // On supprime le lien entre DisposableObject et l’instance de SqlConnection. 
    this.dependency = null; // (5)

    // Libération de dépendances non managées  
    // ... 

    this.disposed = true;  
  }   

  // Personnalisation de Finalize() si nécessaire 
  ~DisposableObject() // (4)
  {  
    this.Dispose(false); // (5) 
  }  
}  

Avec la syntaxe using, l’exécution de Dispose() est synchrone.

IAsyncDisposable

C# 8.0

A partir de C# 8.0, il est possible d’exécuter du code permettant la libération de ressources de façon asynchrone en utilisant le pattern async/await. L’implémentation est similaire à celle avec IDisposable:

  • Il faut satisfaire IAsyncDisposable
  • Implémenter une méthode dont la signature est:
    public ValueTask DisposeAsync();  
    

    Par exemple:

    public class DisposableObject : IAsyncDisposable  
    {  
      public async ValueTask DisposeAsync()  
      {  
        // ...
      }  
    }  
    

    La méthode DisposeAsync() doit retourner un objet de type ValueTask qui est un objet de type valeur équivalent à l’objet Task.

  • Utiliser la syntaxe await using pour que l’exécution de la méthode DisposeAsync() soit asynchrone:
    await using (<objet satisfaisant IAsyncDisposable>)  
    {  
      // ...  
    }  
    

    Par exemple:

    var disposableObject = new DisposableObject(); 
    await using (disposableObject) 
    { 
      // ... 
    } 
    // ATTENTION: à ce stade disposableObject ne doit pas être utilisé car DisposeAsync() a été exécuté 
    

    Ou plus directement:

    await using (var disposableObject = new DisposableObject()) 
    { 
      // ... 
    } 
    

Une autre syntaxe utilisant ConfigureAwait(false) permet d’optimiser le code en évitant d’utiliser le contexte d’exécution d’origine lors de l’exécution de la continuation avec async/await:

await using (<objet satisfaisant IAsyncDisposable>.ConfigureAwait(false))  
{  
  // ...  
}  

Par exemple:

var disposableObject = new DisposableObject(); 
await using (disposableObject.ConfigureAwait(false)) 
{ 
  // ... 
} 

Ou

var disposableObject = new DisposableObject(); 
await using (System.Runtime.CompilerServices.ConfiguredAsyncDisposable configuredDisposable = 
  disposableObject.ConfigureAwait(false)) 
{ 
  // ... 
} 
// ATTENTION: à ce stade disposableObject ne doit pas être utilisé car DisposeAsync() a été exécuté 

Comme pour IDisposable, l’implémentation doit respecter quelques recommandations:

  1. Indiquer au garbage collector de ne pas exécuter Finalize() avec GC.SuppressFinalize(),
  2. De libérer des dépendances managées de façon asynchrone dans la méthode DisposeAsync().
  3. L’implémentation doit être idempotente.

Dans la documentation, on peut ainsi trouver un exemple d’implémentation. Si on prend l’exemple d’une dépendance de type SqlConnection à libérer (System.Data.SqlClient.SqlConnection dérive de System.Data.Common.DbConnection qui satisfait IAsyncDisposable):

class DisposableAsyncObject : IAsyncDisposable  
{  
  private SqlConnection dependency = new SqlConnection("Test connection");  
   
  public async void DisposeAsync()  
  {  
    await this.DisposeAsyncCore(); // (b)

    GC.SuppressFinalize(this); // (a) 
  }  

  private async ValueTask DisposeAsyncCore() 
  { 
    if (this.dependency != null) 
      await this.dependency.DisposeAsync(); 

    this.dependency = null; 
  } 
}  

Dans le cas d’une implémentation satisfaisant IDisposable et IAsyncDisposable:

class DisposableAsyncObject : IAsyncDisposable, IDisposable 
{  
  bool disposed = false;  
  private SqlConnection dependency = new SqlConnection("Test connection");  

  public async void DisposeAsync()  
  {  
    await this.DisposeAsyncCore(); // (b)
  
    this.Dispose(false); // false pour ne pas disposer les dépendances de façon synchrone. 
    GC.SuppressFinalize(this); // (a)  

  }  

  public void Dispose()  
  {  
    this.Dispose(true); // (3)

    GC.SuppressFinalize(this); // (2) 
  }

  private async ValueTask DisposeAsyncCore() 
  { 
    if (this.dependency != null) 
      await this.dependency.DisposeAsync(); 

    this.dependency = null; 
  }

  private void Dispose(bool disposing)  
  {  
    if (this.disposed) // (c) 
      return;  

    if (disposing) 
    {  
      // Libération de dépendances managées de façon synchrone 
      if (this.dependency != null) 
        this.dependency.Dispose(); 
    }  

    // On supprime le lien entre DisposableObject et l’instance de SqlConnection. 
    this.dependency = null; // (5)

    // Libération de dépendances non managées  
    // ...   

    this.disposed = true;  

  }  

  // Personnalisation de Finalize() si nécessaire 
  ~DisposableAsyncObject() // (4)
  {  
    this.Dispose(false); // (5)
  }  
}  

Quelques remarques supplémentaires concernant l’implémentation:

  • L’implémentation asynchrone avec IAsyncDisposable ne remplace pas l’implémentation avec IDisposable. La première méthode convient pour libérer des dépendances de façon asynchrone quand cela est possible. Si ce n’est pas possible, la meilleure implémentation est d’utiliser la version synchrone.
  • Si la classe implémente IAsyncDisposable, il n’est pas obligatoire d’implémenter IDisposable.
  • Si on libère des dépendances de façon synchrone dans DisposeAsync(), ces exécutions seront synchrones et il n’y aura pas de libération asynchrone des dépendances.

Utilisation de using sans {…}

C# 8.0

Avant C# 8.0, using devait obligatoirement être suivi d’un bloc de code:

using (<objet satisfaisant IDisposable>) 
{ 
  // ... 
} 

C# 8.0 permet d’utiliser using sans bloc de code. La portée de l’objet concerné par using correspond au bloc de code dans lequel se trouve using. La méthode Dispose() sera exécutée à la sortie de ce bloc de code.

Par exemple, dans le cas d’une méthode:

public void UseDisposableObject() 
{ 
  using var disposableObject = new DisposableObject(); 

  // Utilisation de disposableObject 
  // ... 

  // disposableObject.Dispose() est exécuté juste avant la sortie de la méthode 
} 

Dans le cas d’un bloc de code:

public void UseDisposableObject() 
{ 
  {
    using var disposableObject = new DisposableObject(); 
    // Utilisation de disposableObject 
    // ...

    // disposableObject.Dispose() est exécuté juste avant la sortie du bloc 
  } 

  // A ce niveau disposableObject est hors de portée 
} 

Avec await using

await using est compatible avec cette syntaxe:

public void UseDisposableObject() 
{ 
  await using var disposableAsyncObject = new DisposableAsyncObject(); 

  // Utilisation de disposableAsyncObject 
  // ... 

  // disposableAsyncObject.DisposeAsync() est exécuté juste avant la sortie de la méthode 
} 

Avec ConfigureAwait()

Avec ConfigureAwait(), on peut utiliser la syntaxe:

var disposableObject = new DisposableObject(); 
await using System.Runtime.CompilerServices.ConfiguredAsyncDisposable useless = 
  disposableObject.ConfigureAwait(false); 

Ou plus simplement:

await using var _ = disposableObject.ConfigureAwait(false); 
DisposeAsync() peut ne pas être exécuté en cas d’exception avec ConfigureAwait(false)

Suivant le pattern utilisé dans le cas où on utilise 2 objets satisfaisant IAsyncDisposable, DisposeAsync() peut ne pas être exécuté.

Par exemple, si on considère la classe suivante:

public class DisposableAsyncObject: IAsyncDisposable 
{ 
  public DisposableAsyncObject(int id) 
  { 
    this.Id = id; 
  } 

  public int Id { get; } 

  public async ValueTask DisposeAsync() 
  { 
    Console.WriteLine($"Instance {this.Id} of DisposableAsyncObject disposed"); 
  } 
} 

Si on exécute le code suivant dans lequel une exception survient:

var instanceOne = new DisposeAsyncObject(1); 
var instanceTwo = new DisposeAsyncObject(2); 

// On lance volontairement une exception 
if (instanceTwo.Id == 2) 
  throw new InvalidOperationException(); 

await using var uselessVarOne = instanceOne.ConfigureAwait(false); 
await using var uselessVarTwo = instanceTwo.ConfigureAwait(false); 

Console.WriteLine(instanceOne.Id); 
Console.WriteLine(instanceTwo.Id); 

A l’exécution, on peut se rendre compte qu’aucune des 2 instances n’est disposée car les lignes await ne sont pas atteintes au moment de l’exception:

Unhandled exception. SystemOperationException: ...

En utilisant await using juste après l’instanciation de l’objet instanceOne, DisposeAsync() sera exécuté même si l’exception survient:

var instanceOne = new DisposeAsyncObject(1); 
await using var uselessVarOne = instanceOne.ConfigureAwait(false); 

var instanceTwo = new DisposeAsyncObject(2); 

// On lance volontairement une exception 
if (instanceTwo.Id == 2) 
  throw new InvalidOperationException();  

await using var uselessVarTwo = instanceTwo.ConfigureAwait(false); 

Console.WriteLine(instanceOne.Id); 
Console.WriteLine(instanceTwo.Id); 

instanceOne.DisposeAsync() est exécutée:

Instance 1 of DisposableAsyncObject disposed
Unhandled exception. SystemOperationException: ... 

Imbriquer les blocs await using permet d’exécuter DisposeAsync() dans le cas des 2 instances:

var instanceOne = new DisposeAsyncObject(1); 
await using (var uselessVarOne = instanceOne.ConfigureAwait(false)) 
{ 
  var instanceTwo = new DisposeAsyncObject(2); 
  await using (var uselessVarTwo = instanceTwo.ConfigureAwait(false)) 
  { 
    // On lance volontairement une exception 
    if (instanceTwo.Id == 2) 
      throw new InvalidOperationException(); 

    Console.WriteLine(instanceOne.Id); 
    Console.WriteLine(instanceTwo.Id); 
  } 
} 

DisposeAsync() est exécuté pour les 2 instances:

Instance 1 of DisposableAsyncObject disposed
Instance 2 of DisposableAsyncObject disposed 
Unhandled exception. SystemOperationException: ... 

Share on RedditTweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Références nullables (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

Cette fonctionnalité fait partie des fonctionnalités les plus importantes de C# 8.0. Elle vise à éviter que la référence d’un objet soit nulle par inadvertance.

En C#, les objets de type référence sont manipulés en utilisant une référence permettant d’atteindre l’objet stocké dans le tas managé. Les opérations les plus courantes sur une référence sont:

  • L’initialisation d’une nouvelle référence faite par copie d’une autre référence (la référence est copiée mais pas l’objet en mémoire) ou en utilisant l’opérateur new pour instancier un nouvel objet:
    Circle circle = new Circle();
    

    La classe Circle est:

    public class Circle
    {
      public int Radius;
    
      public void UpdateRadius(int newRadius)
      {
        this.radius = newRadius;
      }
    }
    
  • Le passage d’une référence en argument d’une fonction: lors de l’appel de la fonction, une copie de la référence est effectuée si elle est passée en argument (sauf si on utilise le mot-clé ref).
  • Le déférencement: cette opération permet de déférencer une référence pour utiliser l’objet dont elle fait référence en mémoire. Le déférencement permet, par exemple, d’appeler une méthode de l’objet ou d’accéder à une propriété:
    Circle circle = new Circle(); 
    circle.UpdateRadius(3); // déférencement pour appeler une fonction
    circle.Radius = 5; // déférencement pour accéder à une propriété
    

En C#, l’initialisation, l’affectation, le passage de référence sont des opérations réalisables avec une référence nulle. Le déférencement ne l’est pas, la référence nulle ne pointant sur aucun objet en mémoire. C’est ce dernier cas qui provoque le plus d’erreurs d’inadvertance puisqu’elles provoquent des erreurs de type NullReferenceException. Tony Hoare, qui est le scientifique lauréat du prix Turing à l’origine des références nulles avait qualifié son invention d’erreur à un milliard de dollars à cause de toutes les erreurs que les références nulles ont pu provoquer.

Les références nullables est une fonctionnalité de C# 8.0 visant à empêcher l’utilisation de références nulles en générant des warnings à la compilation lorsque des références potentiellement nulles sont détectées dans le code. Une référence est potentiellement nulle si elle n’est pas initialisée avec une valeur différente de null ou si elle est affectée par la valeur null.

Transformer les warnings en erreurs de compilation

Les warnings générés par le compilateur à la suite d’utilisation de référence nulles ne sont pas bloquants. Il est possible de les rendre bloquant en transformant ces warnings en erreurs en activant l’option TreatWarningsAsErrors. Pour activer cette option:

  • Dans un projet .NET Core: il faut éditer le fichier .csproj du projet et ajouter le nœud <TreatWarningsAsErrors>:
    <Project Sdk="Microsoft.NET.Sdk">  
      <PropertyGroup>  
        <OutputType>Exe</OutputType>  
        <TargetFramework>netcoreapp3.1</TargetFramework>  
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>  
    </Project> 
    
  • Dans Visual Studio, il faut éditer les options du projet:
    Dans les propriétés du projet ⇒ Onglet “Build” ⇒ dans la partie “TreatWarningsAsErrors” ⇒ sélectionner “All”.

Cette option permettra de générer des erreurs bloquantes à la compilation au lieu des warnings.

Référence nullable vs référence non-nullable

Par défaut, le comportement du compilateur est le même que pour les versions précédents C# 8.0 c’est-à-dire:

  • Le compilateur n’affiche pas de warnings à la compilation dans le cas où une référence est assignée avec null ou si un déférencement est effectué pour une référence potentiellement nulle.
  • Les références nullables ne sont pas possibles: les objets de type valeur nullables (i.e. nullable value types) sont apparus en C# 2.0. Ils sont notés avec la syntaxe <type>? par exemple int? pour un entier nullable. Cette fonctionnalité n’était valable que pour les objets de type valeur et non les objets de type référence (puisqu’une référence peut être nulle).
C# 8.0

A partir de C# 8.0, il est possible de créer des références nullables de la même façon que les objets de type valeur avec la notation <type>?, par exemple:

Circle? Circle = null; 

Le compilateur considère ainsi 2 types de références:

  • Les références non-nullables: ce sont les références normales, elles sont appelées “non-nullables” toutefois, par défaut sans modification de la configuration, elles peuvent être assignées avec la valeur nulle.
  • Les références nullables: elles sont déclarées avec la notation <type>? et elles sont nullables comme les références non-nullables toutefois, par défaut, elles génèrent des warnings si l’option Nullable n’est pas activée:
    Warning CS8632: The annotation for nullable reference types should 
      only be used in code within a '#nullable' annotations context.
    

Ces 2 types de référence prennent leur sens si on active l’option Nullable.

Activer l’option de configuration Nullable

Cette option permet de changer le comportement du compilateur vis-à-vis des références nullables et non-nullables. Il existe plusieurs niveaux de d’activation de cette option. Pour l’activer, il faut ajouter enable, warnings ou annotations dans un nœud Nullable dans le fichier .csproj du projet:

<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>  
    <OutputType>Exe</OutputType>  
    <TargetFramework>netcoreapp3.1</TargetFramework>  
    <Nullable>enable</Nullable>
  </PropertyGroup>  
</Project>  

Les différents niveaux d’activation de cette option peuvent être résumés de cette façon:

Niveau Comportement général Référence nullable Référence non-nullable
enable Les références nullables sont utilisables et
des warnings sont générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables ne génère pas de warnings.
  • Le déférencement d’une référence nullable potientiellement nulle génère un warning (CS8602).
  • L’affectation de null à une référence non-nullable génère un warning (CS8600).
  • Le déférencement d’une référence non-nullable potentiellement nulle génère un warning (CS8602).
warnings Les références nullables génèrent des warnings et
des warnings sont générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables génère des warnings (CS8632).
  • Le déférencement d’une référence nullable potientiellement nulle génère un warning (CS8602).
annotations Les références nullables sont utilisables et
les warnings ne sont pas générés si des références sont potentiellement nulles
Pas de warnings Pas de warnings
disable
(valeur par défaut)
Les références nullables génèrent un warning et
des warnings ne sont pas générés si des références sont potentiellement nulles.
  • L’utilisation de références nullables génère des warnings (CS8632).
  • Le déférencement d’une référence nullable potientiellement nulle ne génère pas de warning.

Contexte nullable

Le contexte nullable correspond au contexte dans lequel le code est compilé. Ce contexte permet au compilateur de savoir:

  • quelles sont les règles à appliquer pour vérifier la syntaxe,
  • si des références sont nullables et
  • si des références peuvent contenir une valeur nulle et sont déférencées.

Le contexte nullable comporte 2 contextes sous-jacents:

  • Le contexte d’annotation nullable dans lequel l’utilisation des références nullables est possible et ne provoque pas de warnings.
  • Le contexte des warnings pour les références nulles dans lequel des warnings sont générés dans le cas où:
    • Une référence non-nullable est affectée avec la valeur null.
    • Une référence non-nullable est potentiellement nulle et est déférencée.

Il existe 2 méthodes pour indiquer ces contextes:

  • Par configuration: en indiquant le paramètre Nullable au niveau du fichier .csproj. De cette façon, on indique le contexte nullable dans tout le projet.

    Si on reprend le tableau plus haut, on obtient:

    Configuration Contexte d’annotation nullable Contexte des warnings pour les références nulles Référence nullable Référence non-nullable
    enable Activé Activé OK
    Warning en cas de déférencement d’une valeur nulle
    Warnings si nulle
    warnings Désactivé Non autorisé (provoque un warning)
    annotations Activé Désactivé OK
    Pas de warnings en cas de déférencement d’une valeur nulle
    Pas de warnings si nulle
    disable
    (valeur par défaut)
    Désactivé Non autorisé (provoque un warning)
  • Par code: la configuration permet d’indiquer un paramétrage pour tout le projet, ensuite il est possible d’affiner au niveau du code pour indiquer un contexte sur une portion de code. Dans le code, on peut indiquer un contexte sur une portion avec les annotations suivantes:
    Annotation Contexte d’annotation nullable Contexte des warnings pour les références nulles
    #nullable enable Activé Activé
    #nullable disable Désactivé Désactivé
    #nullable restore Les contextes sont ceux indiqués dans la configuration du projet
    #nullable enable warnings Pas de changement Activé
    #nullable disable warnings Désactivé
    #nullable restore warnings Contexte indiqué dans la configuration du projet
    #nullable enable annotations Activé Pas de changement
    #nullable disable annotations Désactivé
    #nullable restore annotations Contexte indiqué dans la configuration du projet

Se prémunir contre les valeurs nulles

La syntaxe C# prévoit quelques opérateurs pour se prémunir contre les valeurs nulles.

Opérateur !. (null-forgiving)

C# 8.0

Cet opérateur utilisable à partir de C# 8.0, est autorisé dans un contexte d’annotation nullable (c’est-à-dire quand il est possible d’utiliser des références nullables <nom variable>?). Il vise à éviter d’avoir le warning correspondant au déférencement d’une référence nullable potentiellement nulle et quand le compilateur ne peut pas déterminer si la référence est nulle ou non. Il n’a pas d’incidence sur l’exécution du code.

Par exemple, si on exécute le code suivant avec les warnings pour les références nulles activés:

int intValue = 5; 
Circle? nullableRef = null; 
if (intValue > 4) 
  nullableRef = new Circle{ Radius = 3 }; 

Console.WriteLine(nullableRef.Radius); // Warning 

A la compilation, ce code produit le warning suivant indiquant un déférencement de la référence nullableRef qui pourrait être nulle:

warning CS8602: Deference of a possible null reference. 

Pour éviter ce warning, on peut:

  • Désactiver les warnings pour les références nulles en modifiant le code de cette façon:
    #nullable disable warnings 
    
    Console.WriteLine(nullableRef.Radius); // Pas de warning
    
    #nullable enable warnings 
    
  • Utiliser l’opérateur null-forgiving:
    Console.WriteLine(nullableRef!.Radius);
    

Comme indiqué plus haut l’opérateur null-forgiving ne protège pas d’erreurs en cas de déférencement d’une référence nullable effectivement nulle:

Circle? nullableRef = null; 
Console.WriteLine(nullableRef!.Radius); // NullReferenceException 

D’autres opérateurs permettent d’éviter des erreurs provenant de références nulles.

Autres opérateurs contre les NullReferenceException

Pour se prémunir des NullReferenceException, on peut utiliser les opérateurs (la plupart de ces opérateurs existent avant C# 8.0):

Opérateur ?. (null-conditional)

L’opérateur null-conditional ?. (à partir de C# 6) s’utilise avec la syntaxe circle?.Radius.

Le comportement est:

  • si circle contient null alors la valeur de circle?.Radius est null.
  • si circle est différent de null alors circle?.Radius correspond à la valeur de Radius.

Opérateur ?[] (null-conditional)

L’opérateur null-conditional ?[] (à partir de C# 6) s’utilise avec des objets contenant des index.

Par exemple dans le cas d’un tableau:

Circle[] circles = new Circle[] { new Circle(), null }; 
Console.WriteLine(circles?[1].Radius); 

Le comportement est:

  • si circles[1] contient null alors la valeur de circles?[1].Radius est null.
  • si circles[1] est différent de null alors circles?[1].Radius correspond à la valeur de Radius.

Opérateur ?? (null-coalescing)

L’opérateur ?? permet d’évaluer si une variable est nulle.

Cet opérateur s’utilise de cette façon:

<variable à évaluer> ?? <expression si la variable est nulle> 

Ainsi:

  • Si <variable à évaluer> contient null alors l’expression retournée est <expression si la variable est nulle>.
  • Si <variable à évaluer> est différent de null alors l’expression retournée est la valeur de <variable à évaluer>.

Par exemple:

Circle firstCircle = new Circle{ Radius = 1}; 
Circle secondCircle = null; 

var radius = (secondCircle ?? firstCircle).Radius; 
Console.WriteLine(radius); // 1 c'est-à-dire la valeur de firstCircle.Radius

secondCircle = new Circle{ Radius = 2}; 

radius = (secondCircle ?? firstCircle).Radius; 
Console.WriteLine(radius); // 2 c'est-à-dire la valeur de secondCircle.Radius

A partir de C# 8.0, pour utiliser l’opérateur ?? la variable à évaluer ne peut pas être un objet de type valeur non nullable.

Opérateur ??=

C# 8.0

L’opérateur ??= permet d’affecter le résultat d’une expression à une variable si cette variable contient null (à partir de C# 8.0).

La syntaxe est du type <variable à évaluer> ??= <expression à affecter si variable null>.

Par exemple:

Circle firstCircle = new Circle{ Radius = 1 }; 
Circle? secondCircle = null; 

secondCircle ??= firstCircle; 
Console.WriteLine(secondCircle.Radius); // 1 

Dans ce code secondCircle ??= firstCircle est équivalent à:

if (secondCircle == null) 
  secondCircle = firstCircle;

Share on RedditTweet about this on TwitterShare on LinkedInEmail this to someonePrint this page

Membre d’une structure en lecture seule avec readonly (C# 8.0)

Cet article fait partie d’une série d’articles sur les apports fonctionnels de C# 8.0.

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 (ou readonly ref struct dans le cas d’une ref 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 si AddToRadius() 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;
    }
    
Ne pas confondre 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 type int 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.

Share on RedditTweet about this on TwitterShare on LinkedInEmail this to someonePrint this page