Réflexion sur les types génériques

Une chose étrange s’est produite sur les modèles convertis vers le Common Language Runtime (CLR) : ils ont perdu leur identité {type}. Cela ressemble à ce qui se passe avec les macros dans les programmes natifs. Tout comme les compilateurs C/C++ n’ont pas conscience des extensions de prétraitement des macros, le CLR n’est pas conscient des instanciations de modèle. Dans les deux cas, les extensions sont intégrées au flux de données traité et toute identité {type} unique est perdue. Il existe certaines ramifications de conception que j’aborderai dans un article suivant, lors de l’examen de STL/CLR, la bibliothèque de modèles standard (Standard Template Library) dont l’architecture a été repensée pour Visual C++ ®2005.)

À l’inverse, les génériques sont directement pris en charge par le runtime. Les extensions du CIL (Common Intermediate Language) prennent explicitement en charge la spécification d’un type générique, ainsi que ses paramètres de type, contraintes, etc. Une extension des fonctionnalités de type Reflection des bibliothèques de classes de base de l’espace de noms System ajoute des fonctionnalités complètes de réflexion sur les types génériques et les instances d’objet. En outre, le runtime gère automatiquement et de façon optimale l’instanciation des instances génériques. Cet article présente la réflexion et la prise en charge de la réflexion générique dans le .NET Framework 2.0.

Consultez l'article en anglais 

Sur cette page
Informations de type dans .NETInformations de type dans .NET
Requêtes IsA/HasARequêtes IsA/HasA
Extractions GetExtractions Get

Informations de type dans .NET

La plupart des auteurs de compilateurs, au cours de leur carrière, réalisent qu’ils passent une bonne partie de chaque journée à se demander comment convertir plus intelligemment un programme, à se demander comment générer une représentation complète pour prendre en charge cette conversion, et enfin à supprimer par la suite toutes ces informations. Dans .NET, cette représentation est persistante, sous la forme d’une spécification normalisée des métadonnées, et est disponible à la fois pour le runtime (qui dispose d’un accès complet au CIL et aux métadonnées générées) et pour vous lors de l’exécution via la découverte lors de l’exécution, appelée réflexion.

Par exemple, vous pouvez écrire ce qui suit :

enum class test { fail, succeed, untested, rerun };

La classe System::Enum sous-jacente fournit des méthodes statiques par l'intermédiaire desquelles vous pouvez accéder aux noms des énumérateurs, aux valeurs associées, etc. Pour afficher les noms des énumérateurs par ordre croissant de la valeur intégrale correspondante, vous pouvez écrire ceci :

for each( String ^s in Enum::GetNames(
status::typeid ))
Console::WriteLine( s );

Il est judicieux de prendre un moment pour comprendre ce qui se passe avec l’appel de Enum::GetNames, plutôt que d’ignorer la façon dont cela se passe. Vous souhaitez extraire le nom des énumérations de votre état enum. Il ne serait pas judicieux d’associer ces noms à une instance d’état, qui doit contenir uniquement une valeur intégrale. Les noms et valeurs des énumérateurs sont invariables et sont liés aux instances de l’énumération que vous avez définie. Cela représente une forme de métadonnées. Vous devez les stocker une seule fois, et la classe par l'intermédiaire de laquelle ces métadonnées sont stockées est System::Type (la classe System::Type associée à votre état enum).

Il existe différentes façons d’accéder à l’objet Type associé à un type. T::typeid est l’extension C++/CLI de l’opérateur typeid RTTI (C++ Run-Time Type Information) natif qui renvoie l’objet Type associé au type spécifié T (dans ce cas, l’énumération d’état). Comme indiqué précédemment, cette utilisation des métadonnées via l’accès lors de l’exécution aux objets Type est appelée réflexion.

Si vous avez un objet d’un certain type, son Type associé est extrait via la méthode de l’instance GetType. Par exemple, voici une méthode générale pour extraire le Type associé d’un objet et afficher le nom du type et son nom entièrement qualifié :

void DisplayType( Object^ o )
{ 
	if ( !o ) return;
	
	Type^ t = o->GetType(); 
	Console::WriteLine( "Type is {0} : {1}", 
		t->Name, t->FullName ); 
}

La figure 1 illustre le code qui transmet différents types à la méthode DisplayType, notamment un type générique de classe. Celui-ci génère la sortie illustrée à la figure 2. Notez que j’ai nettoyé un peu la sortie et annoté les résultats avec des commentaires. Rappelez-vous également que la sortie peut être différente de celle obtenue dans la version finale du produit. Notez que le nom entièrement qualifié d’un Type affiche l’opérateur de portée sous forme de point (.), l’opérateur de portée de langage intermédiaire (IL), plutôt que le double signe deux-points (::) C++. Notez également les informations système complètes disponibles pour le type générique.

Si vous n’avez pas d’objet par l'intermédiaire duquel récupérer l’objet Type associé, vous pouvez l’extraire explicitement en utilisant l’opérateur typeid, de la façon suivante :

Type^ ts = String::typeid; 
Type^ ti = Int32::typeid;

Bien entendu, si vous n’avez pas spécifié d’instruction using pour l’espace de noms approprié, vous devez qualifier le nom d’un type situé dans cet espace de noms. Par exemple :

Type^ tsb = System::Text::StringBuilder::typeid;

Si vous n’êtes pas certain que le type réel existe, vous ne pouvez pas utiliser réellement l’opérateur typeid explicite. Vous pouvez en revanche utiliser la méthode GetType statique de la classe Type en lui transmettant un littéral de type chaîne avec le nom du type. Vous pouvez par exemple faire cela pour lire un fichier ou une collection de noms de type pouvant ou non exister dans votre application (voir la figure 3).

Il existe différentes formes possibles pour le nom de type littéral. Si vous indiquez un nom non qualifié, tel que le suivant, le type est trouvé uniquement s’il a une portée globale :

// Unqualified name: not found if we intend System::Math …
DisplayType( "Math" );

Lorsque vous spécifiez un nom entièrement qualifié, vous devez utiliser l’opérateur de portée IL plutôt que celui de C++. Par exemple :

// A fully qualified name, but we can't use :: 
DisplayType( "System.Math" );

Notez que vous ne pouvez pas utiliser cette forme d’extraction pour un type générique de classe ou un type de modèle de classe. Cela tient au fait que la définition de modèle ou de générique ne représente pas un type réel, mais uniquement un modèle pour la génération d’instances basées sur les arguments de type réels.

Une troisième forme de littéral de type chaîne permettant d’extraire un nom de type consiste à qualifier entièrement le nom et à spécifier l’assembly dans lequel vous pensez qu’il se trouve. L’assembly est séparé du type par une virgule, comme dans l’exemple suivant :

// A fully qualified name, and assembly 
DisplayType( "System.Math, mscorlib" );

Si vous disposez d’un objet Assembly, vous pouvez soit extraire un tableau Type de tous les types définis dans cet assembly, soit interroger un type particulier, comme illustré ici :

array‹Type^›^ types = a1->GetTypes();
Type^ query = a1->GetType( "Query" );

Qu’en est-il si vous avez un Type et que vous souhaitez découvrir tous les autres types définis dans son assembly ? Ou si vous souhaitez, à partir d’un objet, découvrir l’assembly dans lequel est définie sa définition de type ? Ou qu’en est-il si vous souhaitez avoir d’autres informations sur le type : s’agit-il d’un tableau, d’une classe, d’un type générique, etc. ? Vous pouvez interroger et extraire presque toutes ces informations d’un objet Type.

La classe Type est en quelque sorte magique ; elle sert de clé pour déverrouiller la fonctionnalité de réflexion d’exécution. Par exemple, la figure 4 est un utilitaire d’exécution simple qui renvoie soit le nom de la classe de base du type, si elle existe, soit nullptr.

Un Type prend en charge un certain nombre de requêtes, par exemple pour savoir si le type est une classe (IsClass). Elle renvoie des informations complémentaires sur le type, par exemple le Type de sa classe de base, ou s’il doit en comporter un (BaseType). Elle fournit également les propriétés générales du type, par exemple son nom (Name,FullName). J’ai classé les services offerts par Type en plusieurs catégories générales, que j’aborderai plus loin.

Haut de pageHaut de page

Requêtes IsA/HasA

Les requêtes IsA/HasA indique le type. J’ai déjà utilisé IsClass. Les autres propriétés IsA sont IsAbstract, IsArray, IsNested, IsPublic, IsSealed, IsSerializable, IsValueType, etc. Les propriétés HasA incluent HasGenericArguments, qui renvoie true si le type comporte des arguments de type, et HasElementType, qui renvoie true si le type contient ou fait référence à un autre type, c'est-à-dire s’il s’agit d’un tableau, d’un pointeur ou s’il est transmis par référence. La figure 5 montre comment découvrir un type générique lors de l’exécution.

IsGenericTypeDefinition : renvoie true si le Type représente une définition générique. Renvoie false sur une instance du type générique, ainsi que sur n'importe quel type non générique, y compris celui d’un modèle. Par exemple, la figure 6 montre trois types transmis et la sortie associée.

HasGenericArguments : renvoie true si le Type représente une définition générique ou une instance d’un type générique. Par exemple, voici la sortie de l’exécution des trois mêmes types sur cette propriété HasA :

String^ is not a generic instance!
Container`1 is a generic instance!
Container`1 is a generic instance!

Étant donné que HasGenericArguments renvoie true sur une définition générique et une instance générique, si vous souhaitez simplement gérer une instance générique, vous avez besoin d’un test conditionnel, tel que le suivant :

if ( ! t->IsGenericTypeDefinition &&
t->HasGenericArguments )
// ok, a generic instance …

GetGenericTypeDefinition représente une deuxième catégorie de service : extraction d’un aspect spécial du type.

Haut de pageHaut de page

Extractions Get

Ces méthodes permettent d’extraire un type spécifique de collection ou des types pouvant être associés à cet objet Type. GetGenericTypeDefinition est l’une de ces méthodes. Pour illustrer quelques-unes de ces méthodes d’extraction (en rouge) associées aux types génériques, complétons DisplayGeneric pour une analyse plus profonde (voir figure 7).

GetGenericArguments renvoie un tableau d’objets Type représentant chacun des paramètres de type générique. À chaque élément de paramètre est associée une position (commençant à 0) ; il s’agit de la valeur renvoyée par GenericParameterPosition. GetGenericParameterConstraints renvoie les clauses de contrainte associées, si elles existent, en tant que tableau d’objets Type. Faute d’avoir été clair sur les contraintes de l’utilisation de cette méthode, je l’ai appliquée par inadvertance à la définition générique proprement dite plutôt qu’aux paramètres individuels. Il en résulte une exception d’exécution :

System.InvalidOperationException: Method may only be called on a Type 
for which Type.IsGenericParameter is true.

Dans tous les cas, j’ai changé les types à transmettre à cette méthode révisée. Dans le premier cas, j’ai ajouté trois contraintes à notre classe générique, comme illustré ci-après :

generic ‹class T›
where T : IComparable‹T›,
		System::Collections::IEnumerable, ICloneable
ref class Container{};

Lorsque j’appelle la méthode comme suit :

Container‹String^› ^cs = gcnew Container‹String^›;
Type^ gd = cs->GetType()->GetGenericTypeDefinition();
DisplayGeneric( gd );

la sortie suivante est générée :

Container`1 is a generic type definition!
Container`1 has 1 parameters.
		T at position 0
		T has 3 constraint(s):  IComparable`1 IEnumerable ICloneable

Le deuxième générique de classe est une instance du conteneur SortedDictionary qui se trouve dans l’espace de noms System::Collections::Generic. Voici à quoi ressemble l’appel :

SortedDictionary‹String^, String^›^ gds = 
	gcnew SortedDictionary‹String^, String^›;
Type^ gdst = gds->GetType()->GetGenericTypeDefinition();
DisplayGeneric( gdst );

La sortie est quelque peu surprenante (je m’attendais à un ensemble de contraintes plus volumineux et plus spécifique) :

SortedDictionary`2 is a generic type definition!
SortedDictionary`2 has 2 parameters.

		K at position 0
		K has 1 constraint(s) :  Object
		
		V at position 1
		V has 1 constraint(s) :  Object

Comme je l’indiquais au début de cet article, l’article suivant (et final) sur les types paramétrés sous Visual C++ 2005 examinera l’interopérabilité de conception des modèles et génériques avec notre bibliothèque STL/CLR comme modèle. D’ici là, espérons que vos programmes s’exécuteront sans bugs.


Haut de pageHaut de page