Comment utiliser la réflexion pour appeler une méthode générique?


1071

Quelle est la meilleure façon d'appeler une méthode générique lorsque le paramètre type n'est pas connu au moment de la compilation, mais est obtenu dynamiquement à l'exécution?

Considérez l'exemple de code suivant - à l'intérieur de la Example()méthode, quelle est la façon la plus concise d'invoquer en GenericMethod<T>()utilisant le Typestocké dans la myTypevariable?

public class Sample
{
    public void Example(string typeName)
    {
        Type myType = FindType(typeName);

        // What goes here to call GenericMethod<T>()?
        GenericMethod<myType>(); // This doesn't work

        // What changes to call StaticMethod<T>()?
        Sample.StaticMethod<myType>(); // This also doesn't work
    }

    public void GenericMethod<T>()
    {
        // ...
    }

    public static void StaticMethod<T>()
    {
        //...
    }
}

7
J'ai essayé la solution de Jon et je n'ai pas pu la faire fonctionner avant d'avoir rendu publique la méthode générique dans ma classe. Je sais qu'un autre Jon a répondu en disant que vous devez spécifier les indicateurs de reliure, mais cela n'a pas aidé.
naskew

12
Vous devez également BindingFlags.Instance, et pas seulement BindingFlags.NonPublic, obtenir la méthode privée / interne.
Lars Kemmann

2
Version moderne de cette question: stackoverflow.com/q/2433436/103167
Ben Voigt

@Peter Mortensen - fyi J'ai utilisé des espaces avant le '?' pour séparer les parties anglaises des parties non anglaises (C #); À mon humble avis, la suppression de l'espace donne l'impression? fait partie du code. S'il n'y avait pas de code, je serais certainement d'accord pour supprimer les espaces, mais dans ce cas ...
Bevan

Réponses:


1139

Vous devez utiliser la réflexion pour démarrer la méthode, puis la "construire" en fournissant des arguments de type avec MakeGenericMethod :

MethodInfo method = typeof(Sample).GetMethod(nameof(Sample.GenericMethod));
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

Pour une méthode statique, passez nullcomme premier argument à Invoke. Cela n'a rien à voir avec les méthodes génériques - c'est juste une réflexion normale.

Comme indiqué, cela est beaucoup plus simple à partir de C # 4 dynamic- si vous pouvez utiliser l'inférence de type, bien sûr. Cela n'aide pas dans les cas où l'inférence de type n'est pas disponible, comme l'exemple exact dans la question.


92
+1; Notez que GetMethod()prend en compte que les méthodes d'instance publiques par défaut, vous devrez peut - être BindingFlags.Staticet / ou BindingFlags.NonPublic.

20
La combinaison correcte de drapeaux est BindingFlags.NonPublic | BindingFlags.Instance(et éventuellement BindingFlags.Static).
Lars Kemmann

4
Une question qui devient dupe de cela se demande comment faire cela avec des méthodes statiques - et techniquement, la question ici aussi. Le premier paramètre de generic.Invoke () doit être nul lors de l'appel de méthodes statiques. Le premier paramètre n'est nécessaire que lors de l'appel de méthodes d'instance.
Chris Moschini

2
@ChrisMoschini: Ajouté cela à la réponse.
Jon Skeet

2
@gzou: J'ai ajouté quelque chose à la réponse - mais notez que pour appeler les méthodes génériques dans la question , cela dynamicn'aide pas car l'inférence de type n'est pas disponible. (Il n'y a aucun argument que le compilateur peut utiliser pour déterminer l'argument type.)
Jon Skeet

170

Juste un ajout à la réponse originale. Bien que cela fonctionne:

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

Il est également un peu dangereux dans la mesure où vous perdez la vérification au moment de la compilation GenericMethod. Si vous effectuez plus tard une refactorisation et renommezGenericMethod , ce code ne remarquera pas et échouera au moment de l'exécution. En outre, s'il y a un post-traitement de l'assembly (par exemple, obscurcir ou supprimer des méthodes / classes inutilisées), ce code peut également casser.

Donc, si vous connaissez la méthode à laquelle vous vous connectez au moment de la compilation, et que cela n'est pas appelé des millions de fois, la surcharge n'a donc pas d'importance, je changerais ce code comme suit:

Action<> GenMethod = GenericMethod<int>;  //change int by any base type 
                                          //accepted by GenericMethod
MethodInfo method = this.GetType().GetMethod(GenMethod.Method.Name);
MethodInfo generic = method.MakeGenericMethod(myType);
generic.Invoke(this, null);

Bien qu'il ne soit pas très joli, vous disposez d'une référence de temps de compilation GenericMethodici, et si vous refactorisez, supprimez ou faites quoi que ce soit GenericMethod, ce code continuera à fonctionner, ou au moins se cassera au moment de la compilation (si, par exemple, vous supprimez GenericMethod).

Une autre façon de faire de même serait de créer une nouvelle classe wrapper et de la créer via Activator. Je ne sais pas s'il y a une meilleure façon.


5
Dans les cas où la réflexion est utilisée pour appeler une méthode, il est habituel que le nom de la méthode soit lui-même découvert par une autre méthode. Connaître le nom de la méthode à l'avance n'est pas courant.
Bevan

13
Eh bien, je suis d'accord pour des utilisations courantes de la réflexion. Mais la question d'origine était de savoir comment appeler "GenericMethod <myType> ()" Si cette syntaxe était autorisée, nous n'aurions pas du tout besoin de GetMethod (). Mais pour la question "comment écrire" GenericMethod <myType> "? Je pense que la réponse devrait inclure un moyen d'éviter de perdre le lien de compilation avec GenericMethod. Maintenant, si cette question est courante ou non, je ne sais pas, mais Je sais que j'ai eu exactement ce problème hier, et c'est pourquoi j'ai atterri dans cette question.
Adrian Gallero

20
Vous pourriez faire GenMethod.Method.GetGenericMethodDefinition()au lieu de this.GetType().GetMethod(GenMethod.Method.Name). Il est légèrement plus propre et probablement plus sûr.
Daniel Cassidy

Que signifie "myType" dans votre échantillon?
Développeur

37
Maintenant, vous pouvez utilisernameof(GenericMethod)
dmigo

140

L'appel d'une méthode générique avec un paramètre de type connu uniquement au moment de l'exécution peut être grandement simplifié en utilisant un dynamic type au lieu de l'API de réflexion.

Pour utiliser cette technique, le type doit être connu à partir de l'objet réel (pas seulement une instance de la Typeclasse). Sinon, vous devez créer un objet de ce type ou utiliser la solution API de réflexion standard . Vous pouvez créer un objet à l'aide de l' activateur. méthode .

Si vous voulez appeler une méthode générique, qui en utilisation "normale" aurait eu son type déduit, alors il s'agit simplement de transtyper l'objet de type inconnu en dynamic. Voici un exemple:

class Alpha { }
class Beta { }
class Service
{
    public void Process<T>(T item)
    {
        Console.WriteLine("item.GetType(): " + item.GetType()
                          + "\ttypeof(T): " + typeof(T));
    }
}

class Program
{
    static void Main(string[] args)
    {
        var a = new Alpha();
        var b = new Beta();

        var service = new Service();
        service.Process(a); // Same as "service.Process<Alpha>(a)"
        service.Process(b); // Same as "service.Process<Beta>(b)"

        var objects = new object[] { a, b };
        foreach (var o in objects)
        {
            service.Process(o); // Same as "service.Process<object>(o)"
        }
        foreach (var o in objects)
        {
            dynamic dynObj = o;
            service.Process(dynObj); // Or write "service.Process((dynamic)o)"
        }
    }
}

Et voici la sortie de ce programme:

item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta
item.GetType(): Alpha    typeof(T): System.Object
item.GetType(): Beta     typeof(T): System.Object
item.GetType(): Alpha    typeof(T): Alpha
item.GetType(): Beta     typeof(T): Beta

Processest une méthode d'instance générique qui écrit le type réel de l'argument passé (en utilisant la GetType()méthode) et le type du paramètre générique (en utilisant l' typeofopérateur).

En convertissant l'argument objet en dynamictype, nous avons différé la fourniture du paramètre type jusqu'à l'exécution. Lorsque la Processméthode est appelée avec ledynamic argument, le compilateur ne se soucie pas du type de cet argument. Le compilateur génère du code qui, lors de l'exécution, vérifie les vrais types d'arguments passés (en utilisant la réflexion) et choisit la meilleure méthode à appeler. Ici, il n'y a qu'une seule méthode générique, elle est donc invoquée avec un paramètre de type approprié.

Dans cet exemple, la sortie est la même que si vous écriviez:

foreach (var o in objects)
{
    MethodInfo method = typeof(Service).GetMethod("Process");
    MethodInfo generic = method.MakeGenericMethod(o.GetType());
    generic.Invoke(service, new object[] { o });
}

La version avec un type dynamique est nettement plus courte et plus facile à écrire. Vous ne devez pas non plus vous soucier des performances de l'appel de cette fonction plusieurs fois. Le prochain appel avec des arguments du même type devrait être plus rapide grâce au mécanisme de mise en cache dans DLR. Bien sûr, vous pouvez écrire du code qui met en cache les délégués invoqués, mais en utilisant le dynamictype vous obtenez ce comportement gratuitement.

Si la méthode générique que vous souhaitez appeler n'a pas d'argument de type paramétré (de sorte que son paramètre de type ne peut pas être déduit), vous pouvez encapsuler l'invocation de la méthode générique dans une méthode d'assistance comme dans l'exemple suivant:

class Program
{
    static void Main(string[] args)
    {
        object obj = new Alpha();

        Helper((dynamic)obj);
    }

    public static void Helper<T>(T obj)
    {
        GenericMethod<T>();
    }

    public static void GenericMethod<T>()
    {
        Console.WriteLine("GenericMethod<" + typeof(T) + ">");
    }
}

Sécurité de type accrue

Ce qui est vraiment génial à propos de l'utilisation de l' dynamicobjet en remplacement de l'utilisation de l'API de réflexion, c'est que vous ne perdez que la vérification du temps de compilation de ce type particulier que vous ne connaissez pas jusqu'à l'exécution. Les autres arguments et le nom de la méthode sont analysés statiquement par le compilateur comme d'habitude. Si vous supprimez ou ajoutez des arguments, modifiez leurs types ou renommez le nom de la méthode, vous obtiendrez une erreur au moment de la compilation. Cela ne se produira pas si vous fournissez le nom de la méthode sous forme de chaîne Type.GetMethodet les arguments sous forme de tableau d'objets MethodInfo.Invoke.

Voici un exemple simple qui illustre comment certaines erreurs peuvent être détectées au moment de la compilation (code commenté) et d'autres au moment de l'exécution. Il montre également comment le DLR essaie de résoudre la méthode à appeler.

interface IItem { }
class FooItem : IItem { }
class BarItem : IItem { }
class Alpha { }

class Program
{
    static void Main(string[] args)
    {
        var objects = new object[] { new FooItem(), new BarItem(), new Alpha() };
        for (int i = 0; i < objects.Length; i++)
        {
            ProcessItem((dynamic)objects[i], "test" + i, i);

            //ProcesItm((dynamic)objects[i], "test" + i, i);
            //compiler error: The name 'ProcesItm' does not
            //exist in the current context

            //ProcessItem((dynamic)objects[i], "test" + i);
            //error: No overload for method 'ProcessItem' takes 2 arguments
        }
    }

    static string ProcessItem<T>(T item, string text, int number)
        where T : IItem
    {
        Console.WriteLine("Generic ProcessItem<{0}>, text {1}, number:{2}",
                          typeof(T), text, number);
        return "OK";
    }
    static void ProcessItem(BarItem item, string text, int number)
    {
        Console.WriteLine("ProcessItem with Bar, " + text + ", " + number);
    }
}

Ici, nous exécutons à nouveau une méthode en convertissant l'argument en dynamictype. Seule la vérification du type du premier argument est reportée à l'exécution. Vous obtiendrez une erreur de compilation si le nom de la méthode que vous appelez n'existe pas ou si d'autres arguments ne sont pas valides (mauvais nombre d'arguments ou mauvais types).

Lorsque vous passez l' dynamicargument à une méthode, cet appel est lié récemment . La résolution de surcharge de méthode se produit lors de l'exécution et essaie de choisir la meilleure surcharge. Donc, si vous appelez la ProcessItemméthode avec un objet de BarItemtype, vous appellerez alors la méthode non générique, car elle correspond mieux à ce type. Cependant, vous obtiendrez une erreur d'exécution lorsque vous passerez un argument du Alphatype car aucune méthode ne peut gérer cet objet (une méthode générique a la contrainte where T : IItemet la Alphaclasse n'implémente pas cette interface). Mais c'est tout. Le compilateur ne dispose pas d'informations sur la validité de cet appel. En tant que programmeur, vous le savez et vous devez vous assurer que ce code s'exécute sans erreur.

Type de retour gotcha

Lorsque vous appelez une méthode non vide avec un paramètre de type dynamique, son type de retour sera probablement être dynamictrop . Donc, si vous changez l'exemple précédent en ce code:

var result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

alors le type de l'objet résultat serait dynamic. En effet, le compilateur ne sait pas toujours quelle méthode sera appelée. Si vous connaissez le type de retour de l'appel de fonction, vous devez le convertir implicitement au type requis pour que le reste du code soit typé statiquement:

string result = ProcessItem((dynamic)testObjects[i], "test" + i, i);

Vous obtiendrez une erreur d'exécution si le type ne correspond pas.

En fait, si vous essayez d'obtenir la valeur de résultat dans l'exemple précédent, vous obtiendrez une erreur d'exécution dans la deuxième itération de boucle. Cela est dû au fait que vous avez essayé d'enregistrer la valeur de retour d'une fonction void.


Mariusz, confus par "Cependant, vous obtiendrez une erreur d'exécution lorsque vous passerez un argument de type Alpha car il n'y a pas de méthode capable de gérer cet objet." Si j'appelle var a = new Alpha () ProcessItem (a, "test" + i , i) Pourquoi la méthode générique ProcessItem ne traiterait-elle pas cela efficacement, produisant un "élément de processus général"?
Alex Edelstein

@AlexEdelstein J'ai modifié ma réponse pour clarifier un peu. C'est parce que la ProcessItemméthode générique a une contrainte générique et n'accepte que les objets qui implémentent l' IIteminterface. Lorsque vous appellerez ProcessItem(new Aplha(), "test" , 1);ou ProcessItem((object)(new Aplha()), "test" , 1);vous obtiendrez une erreur du compilateur, mais lors de la conversion, dynamicvous reporter cette vérification à l'exécution.
Mariusz Pawelski

Excellente réponse et explication, fonctionne parfaitement pour moi. Beaucoup mieux que la réponse acceptée, plus courte à écrire, plus performante et plus sûre.
ygoe

17

Avec C # 4.0, la réflexion n'est pas nécessaire car le DLR peut l'appeler à l'aide de types d'exécution. Étant donné que l'utilisation de la bibliothèque DLR est une sorte de douleur dynamiquement (au lieu du compilateur C # générant du code pour vous), le framework open source Dynamitey (.net standard 1.5) vous donne un accès d'exécution en cache facile aux mêmes appels que le compilateur générerait pour vous.

var name = InvokeMemberName.Create;
Dynamic.InvokeMemberAction(this, name("GenericMethod", new[]{myType}));


var staticContext = InvokeContext.CreateStatic;
Dynamic.InvokeMemberAction(staticContext(typeof(Sample)), name("StaticMethod", new[]{myType}));

13

Pour compléter la réponse d'Adrian Gallero :

L'appel d'une méthode générique à partir de type info implique trois étapes.

TLDR: L'appel d'une méthode générique connue avec un objet type peut être accompli par:

((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition()
    .MakeGenericMethod(typeof(string))
    .Invoke(this, null);

GenericMethod<object>est le nom de la méthode à appeler et tout type qui satisfait aux contraintes génériques.

(Action) correspond à la signature de la méthode à appeler, c'est-à-dire (Func<string,string,int> ou Action<bool>)

L'étape 1 obtient le MethodInfo pour la définition de méthode générique

Méthode 1: utilisez GetMethod () ou GetMethods () avec les types appropriés ou les indicateurs de liaison.

MethodInfo method = typeof(Sample).GetMethod("GenericMethod");

Méthode 2: créez un délégué, obtenez l'objet MethodInfo, puis appelez GetGenericMethodDefinition

De l'intérieur de la classe qui contient les méthodes:

MethodInfo method = ((Action)GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

De l'extérieur de la classe qui contient les méthodes:

MethodInfo method = ((Action)(new Sample())
    .GenericMethod<object>)
    .Method
    .GetGenericMethodDefinition();

MethodInfo method = ((Action)Sample.StaticMethod<object>)
    .Method
    .GetGenericMethodDefinition();

En C #, le nom d'une méthode, c'est-à-dire "ToString" ou "GenericMethod" fait en fait référence à un groupe de méthodes qui peut contenir une ou plusieurs méthodes. Jusqu'à ce que vous fournissiez les types des paramètres de méthode, on ne sait pas à quelle méthode vous faites référence.

((Action)GenericMethod<object>) fait référence au délégué pour une méthode spécifique. ((Func<string, int>)GenericMethod<object>) fait référence à une surcharge différente de GenericMethod

Méthode 3: créer une expression lambda contenant une expression d'appel de méthode, obtenir l'objet MethodInfo, puis GetGenericMethodDefinition

MethodInfo method = ((MethodCallExpression)((Expression<Action<Sample>>)(
    (Sample v) => v.GenericMethod<object>()
    )).Body).Method.GetGenericMethodDefinition();

Cela se décompose en

Créez une expression lambda où le corps est un appel à la méthode souhaitée.

Expression<Action<Sample>> expr = (Sample v) => v.GenericMethod<object>();

Extraire le corps et transtyper en MethodCallExpression

MethodCallExpression methodCallExpr = (MethodCallExpression)expr.Body;

Obtenez la définition de méthode générique à partir de la méthode

MethodInfo methodA = methodCallExpr.Method.GetGenericMethodDefinition();

L'étape 2 appelle MakeGenericMethod pour créer une méthode générique avec le ou les types appropriés.

MethodInfo generic = method.MakeGenericMethod(myType);

L'étape 3 appelle la méthode avec les arguments appropriés.

generic.Invoke(this, null);

8

Personne n'a fourni la solution " Reflection classique ", voici donc un exemple de code complet:

using System;
using System.Collections;
using System.Collections.Generic;

namespace DictionaryRuntime
{
    public class DynamicDictionaryFactory
    {
        /// <summary>
        /// Factory to create dynamically a generic Dictionary.
        /// </summary>
        public IDictionary CreateDynamicGenericInstance(Type keyType, Type valueType)
        {
            //Creating the Dictionary.
            Type typeDict = typeof(Dictionary<,>);

            //Creating KeyValue Type for Dictionary.
            Type[] typeArgs = { keyType, valueType };

            //Passing the Type and create Dictionary Type.
            Type genericType = typeDict.MakeGenericType(typeArgs);

            //Creating Instance for Dictionary<K,T>.
            IDictionary d = Activator.CreateInstance(genericType) as IDictionary;

            return d;

        }
    }
}

La DynamicDictionaryFactoryclasse ci - dessus a une méthode

CreateDynamicGenericInstance(Type keyType, Type valueType)

et il crée et renvoie une instance IDictionary, dont les types de clés et de valeurs sont exactement ceux spécifiés lors de l'appel keyTypeet valueType.

Voici un exemple complet comment appeler cette méthode pour instancier et utiliser un Dictionary<String, int>:

using System;
using System.Collections.Generic;

namespace DynamicDictionary
{
    class Test
    {
        static void Main(string[] args)
        {
            var factory = new DictionaryRuntime.DynamicDictionaryFactory();
            var dict = factory.CreateDynamicGenericInstance(typeof(String), typeof(int));

            var typedDict = dict as Dictionary<String, int>;

            if (typedDict != null)
            {
                Console.WriteLine("Dictionary<String, int>");

                typedDict.Add("One", 1);
                typedDict.Add("Two", 2);
                typedDict.Add("Three", 3);

                foreach(var kvp in typedDict)
                {
                    Console.WriteLine("\"" + kvp.Key + "\": " + kvp.Value);
                }
            }
            else
                Console.WriteLine("null");
        }
    }
}

Lorsque l'application console ci-dessus est exécutée, nous obtenons le résultat attendu correct:

Dictionary<String, int>
"One": 1
"Two": 2
"Three": 3

2

Ceci est mon 2 cents basé sur la réponse de Grax , mais avec deux paramètres requis pour une méthode générique.

Supposons que votre méthode soit définie comme suit dans une classe Helpers:

public class Helpers
{
    public static U ConvertCsvDataToCollection<U, T>(string csvData)
    where U : ObservableCollection<T>
    {
      //transform code here
    }
}

Dans mon cas, le type U est toujours une collection observable stockant un objet de type T.

Comme j'ai mes types prédéfinis, je crée d'abord les objets "factices" qui représentent la collection observable (U) et l'objet qui y est stocké (T) et qui seront utilisés ci-dessous pour obtenir leur type lors de l'appel de Make

object myCollection = Activator.CreateInstance(collectionType);
object myoObject = Activator.CreateInstance(objectType);

Appelez ensuite GetMethod pour trouver votre fonction générique:

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

Jusqu'à présent, l'appel ci-dessus est à peu près identique à ce qui a été expliqué ci-dessus, mais avec une petite différence lorsque vous devez lui passer plusieurs paramètres.

Vous devez passer un tableau Type [] à la fonction MakeGenericMethod qui contient les types d'objets "factices" qui ont été créés ci-dessus:

MethodInfo generic = method.MakeGenericMethod(
new Type[] {
   myCollection.GetType(),
   myObject.GetType()
});

Une fois cela fait, vous devez appeler la méthode Invoke comme mentionné ci-dessus.

generic.Invoke(null, new object[] { csvData });

Et tu as fini. Fonctionne un charme!

MISE À JOUR:

Comme @Bevan l'a souligné, je n'ai pas besoin de créer un tableau lors de l'appel de la fonction MakeGenericMethod car elle prend des paramètres et je n'ai pas besoin de créer un objet pour obtenir les types car je peux simplement passer les types directement à cette fonction. Dans mon cas, comme j'ai les types prédéfinis dans une autre classe, j'ai simplement changé mon code en:

object myCollection = null;

MethodInfo method = typeof(Helpers).
GetMethod("ConvertCsvDataToCollection");

MethodInfo generic = method.MakeGenericMethod(
   myClassInfo.CollectionType,
   myClassInfo.ObjectType
);

myCollection = generic.Invoke(null, new object[] { csvData });

myClassInfo contient 2 propriétés de type Typeque j'ai définies au moment de l'exécution en fonction d'une valeur d'énumération transmise au constructeur et me fournira les types pertinents que j'utilise ensuite dans MakeGenericMethod.

Merci encore d'avoir souligné ce @Bevan.


Les arguments pour MakeGenericMethod()avoir le mot-clé params afin que vous n'ayez pas besoin de créer un tableau; vous n'avez pas non plus besoin de créer d'instances pour obtenir les types - methodInfo.MakeGenericMethod(typeof(TCollection), typeof(TObject))serait suffisant.
Bevan

0

Inspiré par la réponse d' Enigmativity - supposons que vous avez deux (ou plus) classes, comme

public class Bar { }
public class Square { }

et vous voulez appeler la méthode Foo<T>avec Baret Square, qui est déclarée comme

public class myClass
{
    public void Foo<T>(T item)
    {
        Console.WriteLine(typeof(T).Name);
    }
}

Ensuite, vous pouvez implémenter une méthode d'extension comme:

public static class Extension
{
    public static void InvokeFoo<T>(this T t)
    {
        var fooMethod = typeof(myClass).GetMethod("Foo");
        var tType = typeof(T);
        var fooTMethod = fooMethod.MakeGenericMethod(new[] { tType });
        fooTMethod.Invoke(new myClass(), new object[] { t });
    }
}

Avec cela, vous pouvez simplement invoquer Foocomme:

var objSquare = new Square();
objSquare.InvokeFoo();

var objBar = new Bar();
objBar.InvokeFoo();

qui fonctionne pour chaque classe. Dans ce cas, il affichera:


Barre carrée

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.