.. _FonctionsAvancees: Fonctions avancées ****************** Surcharge d'opérateurs ====================== En C#, il est possible de redéfinir les opérateurs comme le signe ``+`` afin de pouvoir les utiliser avec d'autres types de données que ceux pris en charge par défaut (numériques et string). C'est un exemple de polymorphisme. Imaginons une classe ``Vecteur``, sans redéfinition d'opérateur, on est obligé de passer par un appel de méthode pour additionner 2 vecteurs: .. code-block:: csharp class Vecteur { double x, y; ... public Vecteur Add(Vecteur v) { return new Vecteur(x + v.x, y + v.y); } } ... Vecteur v1, v2, v3; ... v3 = v1.Add(v2); Avec la redéfinition d'opérateur on va pouvoir écrire les choses de manière plus intuitive: .. code-block:: csharp class Vecteur { double x, y; ... public static Vecteur operator+(Vecteur v1, Vecteur v2) { return new Vecteur(v1.x + v2.x, v1.y + v2.y); } } Vecteur v1, v2, v3; ... v3 = v1 + v2; Seuls les opérateurs suivants peuvent être redéfinis: * Opérateurs unaires: ``+``, ``-``, ``!``, ``~``, ``++``, ``--`` * Opérateurs binaires ``+``, ``-``, ``*``, ``/``, ``%``, ``&``, ``|``, ``^``, ``<<``, ``>>`` * Opérateurs de comparaisons (binaires) ``==``, ``!=``, ``<``, ``>``, ``<=``, ``>=`` Une **redéfinition d'opérateur** est une **méthode statique** nommée **operator** suivie du **symbole de l'opérateur** à redéfinir qui doit respecter les règles suivantes : * Opérateur unaire : la méthode prend un unique argument du type de la classe dans laquelle l'opérateur est redéfini et renvoi une valeur du même type. * Opérateur binaire : la méthode prend deux arguments dont au moins un doit être du type de la classe dans laquelle l'opérateur est redéfini. Le type de retour est libre. Notons que la commutativité des opérateurs n'est pas gérée par défaut. Par exemple, avec le code suivant qui redéfinit l'opérateur ``*`` entre un vecteur et un scalaire : .. code-block:: csharp class Vecteur { double x, y; ... public static Vecteur operator * (Vecteur v1, double k) { return new Vecteur(v1.x * k, v1.y * k); } } ... Vecteur v1,v2; v2 = v1 * 2.0; // ok Vecteur * double est défini // v2 = 2.0 * v1; ERREUR : double * Vecteur n'est pas défini Pour cela il faut redéfinir une deuxième fois l'opérateur ``*`` en inversant les arguments: .. code-block:: csharp class Vecteur { ... public static Vecteur operator * (double k, Vecteur v1) { return v1*k; } // redéfinition e1n utilisant la première définition } .. quiz:: classeobj-operator :title: Redéfinition d'opérateurs Dans la classe ``A`` (actuellement vide), quelles déclarations d'opérateur sont correctes: 1) :quiz:`{"type":"TF","answer":"T"}` ``static A operator++(A a){...}`` #) :quiz:`{"type":"TF","answer":"F"}` ``static double operator++(A a){...}`` #) :quiz:`{"type":"TF","answer":"T"}` ``static A operator+(A a1, A a2){...}`` #) :quiz:`{"type":"TF","answer":"T"}` ``static double operator+(A a1, A a2){...}`` #) :quiz:`{"type":"TF","answer":"T"}` ``static A operator+(A a1, double d){...}`` #) :quiz:`{"type":"TF","answer":"T"}` ``static double operator+(A a1, double d){...}`` Valeurs par défaut et arguments nommés ====================================== Il est possible de donner des valeurs par défaut aux paramètres d'une fonction, il n'est alors plus nécessaire de donner une valeur à ces arguments lors de l'appel à la fonction. Cette approche peut remplacer avantageusement la définition de fonctions polymorphes dans certains cas. .. code-block:: csharp int F(int a = 1 , int b = 0){ return a * a + b ; } F(2,3); // a = 2 , b = 3 F(2); // a = 2, b = 0 (valeur par défaut pour b) F(); // a = 1 et b = 0 (valeurs par défaut pour a et b) Les règles à respecter sont les suivantes : * Tous les arguments sans valeur par défaut doivent être à gauche des arguments avec valeur par défaut. * Il est interdit de sauter des arguments lors de l'appel de la fonction (on ne peut omettre que les derniers arguments). Pour dépasser cette dernière limitation, on peut utiliser les arguments nommés. Les arguments nommés permettent d'appeler une fonction sans respecter l'ordre de déclaration des arguments en nommant explicitement les arguments: .. code-block:: csharp F(a:2, b:3); // a = 2 , b = 3 F(b:2, a:3); // a = 3 , b = 2 F(a:2); // a = 2, b = 0 (valeur par défaut pour b) F(b:3); // a = 1, b = 3 (valeur par défaut pour a) En cas d'ambigüité sur la fonction à utiliser, c'est celle avec le moins d'argument déclaré qui est retenue. .. code-block:: csharp public void F() { ... } public void F(int a =0) { ... } ... F(); // appel la 1ère définition, signature (F). .. quiz:: classeobj-functioncallnameparameters :title: Appels de fonction La classe ``A`` possède une unique méthode statique ``public static void G(int a, String s = "",int b = 1){}``, quelles sont les instructions correctes ? 1) :quiz:`{"type":"TF","answer":"F"}` ``A.G();`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.G(2);`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.G(1,"a",2);`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.G(1,"a");`` #) :quiz:`{"type":"TF","answer":"F"}` ``A.G(,"a");`` #) :quiz:`{"type":"TF","answer":"F"}` ``A.G(,"a",2);`` #) :quiz:`{"type":"TF","answer":"T"}` ``A.G(b:2, a:3);`` #) :quiz:`{"type":"TF","answer":"F"}` ``A.G(a=1, b=3);`` Arguments en nombre variable ============================ Il est parfois impossible de connaitre à l'avance le nombre exacte d'arguments d'une fonction. Par exemple supposons que l'on veuille écrire une fonction ``Max`` qui calcule le maximum d'un ensemble de nombres entiers. On pourrait définir par polymorphisme une version avec 2 arguments, 3 arguments, 4 arguments et ainsi de suite. .. code-block:: csharp int Max(int a, int b) {...} int Max(int a, int b, int c) {...} int Max(int a, int b, int c, int d) {...} Cela n'est bien entendu pas très efficace et limité. Une autre solution serait de définir une version basée sur un tableau d'entier. .. code-block:: csharp int Max(int [] t) {...} Mais on est alors obligé de mettre les nombres dans un tableau pour pouvoir utiliser la fonction: .. code-block:: csharp int a, b, c, d; ... d = Max(new int[]{a,b,c,d}); Le principe des arguments variables permet de conserver le meilleur des deux mondes. Pour déclarer une méthode avec un nombre d'arguments variable, on utilise le mot clé **params**. A l'intérieur de la méthode l'argument est alors vu comme un tableau mais la fonction peut être utilisée comme si il y avait effectivement un nombre variable d'arguments. .. code-block:: csharp int Max(params int[] t){ int m = Int32.MinValue; for (int v in t) if(v>m) m=v; return m; } Max(); // <=> max( new int[]{} ) tableau vide ! Max(1); // <=> max( new int[]{1} ) Max(1, 2, 3); // <=> max( new int[]{1,2,3} ) Max(3, 2, 4, 5, 3, 4, 3, 5, 3, 5); // ... int [] t=... Max(t); Chaque méthode peut utiliser un seul argument de taille variable qui doit être le dernier argument déclaré. .. code-block:: csharp void F(int a, string b, params char[] ct) { ... } void G(params char[] ct, params string[] cs) { ... } // erreur 1 argument de longueur variable au plus void H(params char[] ct, int a) { ... } // erreur l'argument de taille variable doit être en dernière position .. quiz:: classeobj-functionparameters :title: Paramètres de fonction Dans la classe ``A`` (actuellement vide), quelles déclarations de fonction sont correctes: 1) :quiz:`{"type":"TF","answer":"F"}` ``void F(int a=2, int b){}`` #) :quiz:`{"type":"TF","answer":"F"}` ``int F(int b, float b=3){}`` #) :quiz:`{"type":"TF","answer":"F"}` ``F(int c=1, double b=1.0){}`` #) :quiz:`{"type":"TF","answer":"T"}` ``void F(params double [][] d){}`` #) :quiz:`{"type":"TF","answer":"F"}` ``void F(params double d){}`` #) :quiz:`{"type":"TF","answer":"T"}` ``void F(int a, params double [] d){}`` #) :quiz:`{"type":"TF","answer":"F"}` ``void F(params double d, int a=1){}`` .. quiz:: fonctionValDefault :title: Résolution de nom Soit le programme suivant: .. code-block:: csharp :linenos: class A{ public static void F(){} // 1ère public static int F(int i=0){return i;} // 2ème public static double F(int i, int j){return i;} // 3ème public static double F(int i, double j=0.0){return j;} // 4ème public static double F(double i, int j=1){return i;} // 5ème public static void F(string b){} // 6ème public static void F(string b, string a){} // 7ème } Pour chaque chaque appel de fonction, indiquez quelle définition de ``F`` est utilisée ou "Erreur" si l'appel n'est pas valide. .. csv-table:: :header: Appel de fonction, Définition utilisée, Appel de fonction, Définition utilisée :widths: 10, 10, 10, 10 :delim: ! ``F(1)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"2ème"}` ! ``F("2", 1)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"Erreur"}` ``F(2.0)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"5ème"}` ! ``F("", 2 + ".0")`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"7ème"}` ``F("" + 2.0)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"6ème"}` ! ``F(true)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"Erreur"}` ``F(1, 2)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"3ème"}` ! ``F("" + false)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"6ème"}` ``F(1.0, 2)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"5ème"}` ! ``F()`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"1ère"}` ``F(1.0, 2.0)`` ! :quiz:`{"type":"SC", "values":"Erreur,1ère,2ème,3ème,4ème,5ème,6ème,7ème","answer":"Erreur"}` ! Indexeurs ========= Un indexeur permet d'accéder aux éléments d'un objet comme si il s'agissait d'un tableau. Ainsi, si l'on dispose d'une classe ``Matrice`` (tableau 2D de double avec des opérations mathématiques), sans indexeur on écrit des choses du genre : .. code-block:: csharp Matrice m = ... m.SetValeur(1,2, m.GetValeur(0,1) + 2.2); La même chose avec indexeur : .. code-block:: csharp Matrice m = ... m[1,2] = m[0,1] + 2.2; Mais les indexeurs sont beaucoup plus souples que ce que proposent les tableaux car il est possible de définir des indexeurs sur autre chose que des entiers. Par exemple, on peut définir des tableaux relationnels (indicé sur autre chose que des entiers, ``Dictionary`` en C#): .. code-block:: csharp TabRel t = ... t["foo"] = "bar"; string s = t["truc"]; Pour définir un indexeur: il faut définir une propriété dont le nom est **this** suivi entre entre crochets du type de l'indexeur, la définition du getter et du setter est alors classique (bien sûr un indexeur ne peut pas être une propriété auto-implémentée). Exemple : la classe Annuaire permet de d'associer des noms à des numéros de téléphone. Elle offre deux indexeurs, le premier à partir d'un nombre entier permet de récupérer ou modifier le numéro du i-ème contact, le second permet de chercher modifier un numéro à partir d'un nom. .. code-block:: csharp class Contact { public string nom, numero; } class Annuaire { List list; public string this[int n]{ // indexeur sur des entiers get { return list[n].numero ; } set { list[n].numero = value ; } } public string this[string n]{ // indexeur sur des strings get { return list[Find(n)].numero ; } set { list[Find(n)].numero = value ; } } private int Find(string n){ for(int i=0;i {1.0,-2.0,3.0} Map(t, new FonctionNumerique(Math.Abs)); // t => {1.0,2.0,3.0} Fonctions anonymes ------------------ Les fonctions que l'on associe à un objet délégué sont rarement utilisées autrement qu'à travers ce délégué. Comme une fonction n'est jamais appelée directement, il est inutile de la nommer. On peut alors recourir à une déclaration compacte avec le mot clé **delegate**. Exemple: .. code-block:: csharp delegate int T (int n) ; // type délégué T d = new T( delegate (int n){ return n++ ; } // fonction anonyme ); Ce qui revient à la déclaration longue : .. code-block:: csharp delegate int T (int n) ; // type délégué int MaFonction(int n){ return n++; } T d = new T(MaFonction); Il est bien sûr possible de mettre plusieurs instructions dans le corps de la fonction anonyme, de déclarer des variables, d'appeler d'autres fonctions... .. code-block:: csharp double[] t = {5,50,500} ; Map(t, new FonctionNumerique( delegate (double x){ double tmp = x*2 ; return Math.Log10(tmp) ; } )) ; // t=> {1,2,3} Expressions lambda ------------------ Les **expressions lambda** (vient de la notion de lambda calcul) sont une autre façon d’écrire des fonctions anonymes. Par exemple : .. code-block:: csharp x => x + 10 est une fonction qui prend un paramètre ``x`` et renvoie la valeur de ``x + 10``. Remarquez qu'il n'y aucune information de typage explicite. On peut également mettre plusieurs instructions dans une lambda expression, dans ce cas il faut préciser le ``return``: .. code-block:: csharp x => {double y=x*x ; return Math.Log(y) ;} ainsi que prendre plusieurs paramètres : .. code-block:: csharp (a,b) => a {a++, b--, return a*b ;} On peut maintenant simplifier les exemples précédents: .. code-block:: csharp delegate int T (int n) ; // type délégué T d = n => n++; Ou en reprenant l’exemple de la fonction Map : .. code-block:: csharp double[] t = {5,50,500} ; Map(t, x=>{double tmp = x*2 ; return Math.Log10(tmp) ;} ) ; // t=> {1,2,3} Exercices --------- .. quiz:: qcmDelegate :title: QCM Délégués 1) :quiz:`{"type":"TF","answer":"T"}` Le mot clef ``delegate`` permet de déclarer un nouveau type de données. #) :quiz:`{"type":"TF","answer":"T"}` Le mot clef ``delegate`` permet de déclarer une fonction anonyme. #) :quiz:`{"type":"TF","answer":"T"}` Une variable de type délégué peut référencer une méthode statique. #) :quiz:`{"type":"TF","answer":"T"}` Une variable de type délégué peut référencer une méthode d'instance. #) :quiz:`{"type":"TF","answer":"T"}` Une fonction anonyme peut prendre plusieurs paramètres. #) :quiz:`{"type":"TF","answer":"T"}` Une fonction anonyme peut être composée de plusieurs instructions. #) :quiz:`{"type":"TF","answer":"F"}` Une fonction anonyme ne contient pas d'information de typage. #) :quiz:`{"type":"TF","answer":"T"}` Une lambda expression peut prendre plusieurs paramètres. #) :quiz:`{"type":"TF","answer":"T"}` Une lambda expression peut être composée de plusieurs instructions. #) :quiz:`{"type":"TF","answer":"T"}` Une lambda expression ne contient pas d'information de typage. #) :quiz:`{"type":"TF","answer":"T"}` Une lambda expression peut servir à initialiser une variable de type délégué. .. quiz:: ExDelegate :title: Délégués, premières manipulations Dans un nouveu projet Console, dans la classe ``Program``: * Déclarez le type délégué ``IntAction`` qui prend un ``int`` en argument et ne retourne rien. * Déclarez une méthode statique ``PrintInt`` qui prend un argument de type entier, ne retourne rien et affiche l'entier reçu en paramètre sur la console. * Déclarez une méthode statique ``PrintIntSquare`` qui prend un argument de type entier, ne retourne rien et affiche le carré de l'entier reçu en paramètre sur la console. * Déclarez une méthode statique ``Perform`` prenant comme paramètre un élément de type ``IntAction``, un nombre variables d'éléments de type ``int`` et qui applique le délégué ``IntAction`` à chacun des éléments entiers au moyen d'une boucle ``foreach``. * Dans la méthode ``Main``, réalisez les instructions suivantes: * Déclarez une variable ``act`` de type ``IntAction``. * Affectez la méthode ``PrintInt`` à la variable ``act``. * Appelez le délégué ``act`` avec la valeur 42. * Déclarez un tableau d'entiers ``tab`` contenant les valeurs ``{1,2,3,4}``. * Appelez la méthode ``Perform`` avec pour paramètre la variable ``act`` et le tableau ``tab``. * Appelez la méthode ``Perform`` avec pour paramètre la variable ``act`` et les valeurs ``9,8,7,6,5`` (sans déclarer de nouveau tableau). * Affectez la méthode ``PrintIntSquare`` au délégué ``act`` * Appelez la méthode ``Perform`` avec pour paramètre la variable ``act`` et le tableau ``tab``: qu'affiche le programme? .. \subsection{Délégué \contenu{delegate, fonction anonyme, lambda}} Le but de cet exercice est d'utiliser les délégués afin d'écrire une fonction de tri capable de trier un tableau de nombre suivant un ordre donné par une fonction déléguée. Il est en effet possible de définir plusieurs façon d'ordonner les nombres, par exemple par ordre croissant ou décroissant. Or que l'on veuille trier les nombres d'un tableau dans l'ordre croissant ou décroissant, la procédure de tri reste la même et seule l'instruction utilisée pour comparée deux nombres change. L'idée est alors d'écrire une fonction de tri qui prend en paramètre une fonction qui correspond à cette instruction de comparaison. .. Dans une classe \verb!B!: \begin{itemize} \item Déclarez le type délégué \verb!DoubleCompare! qui prend deux double en argument et retourne un entier. Un délégué de ce type correspond à une fonction qui permet de comparer deux nombre $a$ et $b$. \item Déclarez une méthode statique \verb!Tri! qui prend en paramètres un élément \verb!dc! de type \verb!DoubleCompare! et un tableau de double et renvoie un tableau de double. On suppose que étant donné 2 nombres \verb!a! et \verb!b!, \verb!dc(a,b)! égale -1 si \verb!a! est inférieur à \verb!b!, 1 si \verb!a! est supérieur à \verb!b! et 0 si \verb!a! et \verb!b! sont égaux. Cette méthode doit trier les éléments du tableau en accord avec l'ordre défini par \verb!dc! et placez le résultat dans un nouveau tableau qu'elle renvoie (utilisez l'algorithme de tri que vous voulez, les performances ne sont pas importantes ici). \item Écrivez une méthode \verb!CompareCroissant! qui correspond à une fonction de type \verb!DoubleCompare!. Cette fonction prend en paramètre deux nombres double \verb!a! et \verb!b! et retourne -1 si \verb!a