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 = &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 = &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

Leave a Reply