• Aucun résultat trouvé

Les classes, interfaces, méthodes génériques

2 Classes, Stuctures, Interfaces

1. public class C : IEnumerator{ 2

2.8 Les classes, interfaces, méthodes génériques

Supposons qu'on veuille écrire une méthode permutant deux nombres entiers. Cette méthode pourrait être la suivante : 1. public static void Echanger1(ref int value1, ref int value2){

2. // on échange les références value1 et value2 3. int temp = value2;

4. value2 = value1;

5. value1 = temp;

Maintenant, si on voulait permuter deux références sur des objets Personne, on écrirait :

1. public static void Echanger2(ref Personne value1, ref Personne value2){ 2. // on échange les références value1 et value2

3. Personne temp = value2; 4. value2 = value1;

5. value1 = temp; 6. }

Ce qui différencie les deux méthodes, c'est le type T des paramètres : int dans Echanger1, Personne dans Echanger2. Les classes et interfaces génériques répondent au besoin de méthodes qui ne diffèrent que par le type de certains de leurs paramètres.

Avec une classe générique, la méthode Echanger pourrait être réécrite de la façon suivante : 1. namespace Chap2 {

2. class Generic1<T> {

3. public static void Echanger(ref T value1, ref T value2){ 4. // on échange les références value1 et value2

5. T temp = value2; 6. value2 = value1; 7. value1 = temp; 8. } 9. } 10. }

ligne 2 : la classe Generic1 est paramétrée par un type noté T. On peut lui donner le nom que l'on veut. Ce type T est ensuite réutilisé dans la classe aux lignes 3 et 5. On dit que la classe Generic1 est une classe générique.

• ligne 3 : définit les deux références sur un type T à permuter • ligne 5 : la variable temporaire temp a le type T.

Un programme de test de la classe pourrait être le suivant : 1. using System;

2.

3. namespace Chap2 { 4. class Program {

5. static void Main(string[] args) {

6. // int

7. int i1 = 1, i2 = 2;

8. Generic1<int>.Echanger(ref i1, ref i2); 9. Console.WriteLine("i1={0},i2={1}", i1, i2); 10. // string

11. string s1 = "s1", s2 = "s2";

12. Generic1<string>.Echanger(ref s1, ref s2); 13. Console.WriteLine("s1={0},s2={1}", s1, s2); 14. // Personne

15. Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55); 16. Generic1<Personne>.Echanger(ref p1, ref p2);

17. Console.WriteLine("p1={0},p2={1}", p1, p2); 18.

19. }

20. } 21. }

• ligne 8 : lorsqu'on utilise une classe générique paramétrée par des types T1, T2, ... ces derniers doivent être "instanciés". Ligne 8, on utilise la méthode statique Echanger du type Generic1<int> pour indiquer que les références passées à la méthode Echanger sont de type int.

ligne 12 : on utilise la méthode statique Echanger du type Generic1<string> pour indiquer que les références passées à la méthode Echanger sont de type string.

ligne 16 : on utilise la méthode statique Echanger du type Generic1<Personne> pour indiquer que les références passées à la méthode Echanger sont de type Personne.

Les résultats de l'exécution sont les suivants : 1. i1=2,i2=1

2. s1=s2,s2=s1

3. p1=[pauline, dard, 55],p2=[jean, clu, 20]

1. namespace Chap2 { 2. class Generic2 {

3. public static void Echanger<T>(ref T value1, ref T value2){ 4. // on échange les références value1 et value2

5. T temp = value2; 6. value2 = value1; 7. value1 = temp; 8. } 9. } 10. }

ligne 2 : la classe Generic2 n'est plus génériqueligne 3 : la méthode statique Echanger est générique Le programme de test devient alors le suivant :

1. using System; 2.

3. namespace Chap2 { 4. class Program2 {

5. static void Main(string[] args) {

6. // int

7. int i1 = 1, i2 = 2;

8. Generic2.Echanger<int>(ref i1, ref i2); 9. Console.WriteLine("i1={0},i2={1}", i1, i2); 10. // string

11. string s1 = "s1", s2 = "s2";

12. Generic2.Echanger<string>(ref s1, ref s2); 13. Console.WriteLine("s1={0},s2={1}", s1, s2); 14. // Personne

15. Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55); 16. Generic2.Echanger<Personne>(ref p1, ref p2);

17. Console.WriteLine("p1={0},p2={1}", p1, p2);

18. }

19. } 20. }

lignes 8, 12 et 16 : on appelle la méthode Echanger en précisant dans <> le type des paramètres. En fait, le compilateur est capable de déduire d'après le type des paramètres effectifs, la variante de la méthode Echanger à utiliser. Aussi, l'écriture suivante est-elle légale :

1. Generic2.Echanger(ref i1, ref i2); 2. ...

3. Generic2.Echanger(ref s1, ref s2); 4. ...

5. Generic2.Echanger(ref p1, ref p2);

Lignes 1, 3 et 5 : la variante de la méthode Echanger appelée n'est plus précisée. Le compilateur est capable de la déduire de la nature des paramètres effectifs utilisés.

On peut mettre des contraintes sur les paramètres génériques :

Considérons la nouvelle méthode générique Echanger suivante : 1. namespace Chap2 {

2. class Generic3 {

4. // on échange les références value1 et value2 5. T temp = value2; 6. value2 = value1; 7. value1 = temp; 8. } 9. } 10. }

• ligne 3 : on exige que le type T soit une référence (classe, interface) Considérons le programme de test suivant :

1. using System; 2.

3. namespace Chap2 { 4. class Program4 {

5. static void Main(string[] args) {

6. // int

7. int i1 = 1, i2 = 2;

8. Generic3.Echanger<int>(ref i1, ref i2); 9. Console.WriteLine("i1={0},i2={1}", i1, i2); 10. // string

11. string s1 = "s1", s2 = "s2"; 12. Generic3.Echanger(ref s1, ref s2);

13. Console.WriteLine("s1={0},s2={1}", s1, s2); 14. // Personne

15. Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55); 16. Generic3.Echanger(ref p1, ref p2);

17. Console.WriteLine("p1={0},p2={1}", p1, p2); 18.

19. }

20. } 21. }

Le compilateur déclare une erreur sur la ligne 8 car le type int n'est pas une classe ou une interface, c'est une structure :

Considérons la nouvelle méthode générique Echanger suivante : 1. namespace Chap2 {

2. class Generic4 {

3. public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 { 4. // on récupère la valeur des 2 éléments

5. int value1 = element1.Value(); 6. int value2 = element2.Value();

7. // si 1er élément > 2ième élément, on échange les éléments 8. if (value1 > value2) { 9. T temp = element2; 10. element2 = element1; 11. element1 = temp; 12. } 13. } 14. } 15. }

ligne 3 : le type T doit implémenter l'interface Interface1. Celle-ci a une méthode Value, utilisée lignes 5 et 6, qui donne la valeur de l'objet de type T.

lignes 8-12 : les deux références element1 et element2 ne sont échangées que si la valeur de element1 est supérieure à la valeur de element2.

L'interface Interface1 est la suivante : 1. namespace Chap2 { 2. interface Interface1 { 3. int Value(); 4. }

Elle est implémentée par la classe Class1 suivante : 1. using System;

2. using System.Threading; 3.

4. namespace Chap2 {

5. class Class1 : Interface1 { 6. // valeur de l'objet 7. private int value; 8.

9. // constructeur 10. public Class1() { 11. // attente 1 ms

12. Thread.Sleep(1);

13. // valeur aléatoire entre 0 et 99

14. value = new Random(DateTime.Now.Millisecond).Next(100);

15. }

16.

17. // accesseur champ privé value 18. public int Value() {

19. return value;

20. }

21.

22. // état de l'instance

23. public override string ToString() { 24. return value.ToString();

25. }

26. } 27. }

ligne 5 : Class1 implémente l'interface Interface1ligne 7 : la valeur d'une instance de Class1

lignes 10-14 : le champ value est initialisé avec une valeur aléatoire entre 0 et 99lignes 18-20 : la méthode Value de l'interface Interface1

lignes 23-25 : la méthode ToString de la classe

L'interface Interface1 est également implémentée par la classe Class2 : 1. using System;

2.

3. namespace Chap2 {

4. class Class2 : Interface1 { 5. // valeurs de l'objet 6. private int value; 7. private String s; 8.

9. // constructeur

10. public Class2(String s) { 11. this.s = s;

12. value = s.Length;

13. }

14.

15. // accesseur champ privé value 16. public int Value() {

17. return value;

18. }

19.

20. // état de l'instance

21. public override string ToString() { 22. return s;

23. }

24. } 25. }

ligne 4 : Class2 implémente l'interface Interface1ligne 6 : la valeur d'une instance de Class2

lignes 10-13 : le champ value est initialisé avec la longueur de la chaîne de caractères passée au constructeurlignes 16-18 : la méthode Value de l'interface Interface1

lignes 21-22 : la méthode ToString de la classe Un programme de test pourrait être le suivant :

1. using System; 2.

3. namespace Chap2 { 4. class Program5 {

5. static void Main(string[] args) {

6. // échanger des instances de type Class1

7. Class1 c1, c2;

8. for (int i = 0; i < 5; i++) { 9. c1 = new Class1();

10. c2 = new Class1();

11. Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);

12. Generic4.Echanger(ref c1, ref c2);

13. Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);

14. }

15. // échanger des instances de type Class2

16. Class2 c3, c4;

17. c3 = new Class2("xxxxxxxxxxxxxx"); 18. c4 = new Class2("xx");

19. Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4); 20. Generic4.Echanger(ref c3, ref c4);

21. Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);

22. }

23. } 24. }

lignes 8-14 : des instances de type Class1 sont échangéeslignes 16-22 : des instances de type Class2 sont échangées Les résultats de l'exécution sont les suivants :

1. Avant échange --> c1=43,c2=79 2. Après échange --> c1=43,c2=79 3. Avant échange --> c1=72,c2=56 4. Après échange --> c1=56,c2=72 5. Avant échange --> c1=92,c2=75 6. Après échange --> c1=75,c2=92 7. Avant échange --> c1=11,c2=47 8. Après échange --> c1=11,c2=47 9. Avant échange --> c1=31,c2=67 10. Après échange --> c1=31,c2=67 11. Avant échange --> c3=xxxxxxxxxxxxxx,c4=xx 12. Après échange --> c3=xx,c4=xxxxxxxxxxxxxx

Pour illustrer la notion d'interface générique, nous allons trier un tableau de personnes d'abord sur leurs noms, puis sur leurs ages. La méthode qui nous permet de trier un tableau est la méthode statique Sort de la classe Array :

On rappelle qu'une méthode statique s'utilise en préfixant la méthode par le nom de la classe et non par celui d'une instance de la classe. La méthode Sort a différentes signatures (elle est surchargée). Nous utiliserons la signature suivante :

public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

Sort une méthode générique où T désigne un type quelconque. La méthode reçoit deux paramètres :

T[] tableau : le tableau d'éléments de type T à trier

IComparer<T> comparateur : une référence d'objet implémentant l'interface IComparer<T>. IComparer<T> est une interface générique définie comme suit :

1. public interface IComparer<T>{ 2. int Compare(T t1, T t2); 3. }

L'interface IComparer<T> n'a qu'une unique méthode. La méthode Compare :reçoit en paramètres deux éléments t1 et t2 de type T

• rend 1 si t1>t2, 0 si t1==t2, -1 si t1<t2. C'est au développeur de donner une signification aux opérateurs <, ==, >. Par exemple, si p1 et p2 sont deux objets Personne, on pourra dire que p1>p2 si le nom de p1 précède le nom de p2 dans l'ordre alphabétique. On aura alors un tri croissant selon le nom des personnes. Si on veut un tri selon l'âge, on dira que p1>p2 si l'âge de p1 est supérieur à l'âge de p2.

• pour avoir un tri dans l'ordre décroissant, il suffit d'inverser les résultats +1 et -1 Nous en savons assez pour trier un tabeau de personnes. Le programme est le suivant :

1. using System;

2. using System.Collections.Generic; 3.

4. namespace Chap2 { 5. class Program6 {

6. static void Main(string[] args) { 7. // un tableau de personnes

8. Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine",

"germain", 35), new Personne("paul", "germain", 32) }; 9. // affichage

10. Affiche("Tableau à trier", personnes1); 11. // tri selon le nom

12. Array.Sort(personnes1, new CompareNoms()); 13. // affichage

14. Affiche("Tableau après le tri selon les nom et prénom", personnes1); 15. // tri selon l'âge

16. Array.Sort(personnes1, new CompareAges()); 17. // affichage

18. Affiche("Tableau après le tri selon l'âge", personnes1);

19. }

20.

21. static void Affiche(string texte, Personne[] personnes) { 22. Console.WriteLine(texte.PadRight(50, '-'));

23. foreach (Personne p in personnes) {

24. Console.WriteLine(p);

25. }

26. }

27. } 28.

29. // classe de comparaison des noms et prénoms des personnes 30. class CompareNoms : IComparer<Personne> {

31. public int Compare(Personne p1, Personne p2) { 32. // on compare les noms

33. int i = p1.Nom.CompareTo(p2.Nom); 34. if (i != 0)

35. return i;

36. // égalité des noms - on compare les prénoms 37. return p1.Prenom.CompareTo(p2.Prenom);

38. }

39. } 40.

41. // classe de comparaison des ages des personnes 42. class CompareAges : IComparer<Personne> {

43. public int Compare(Personne p1, Personne p2) { 44. // on compare les ages

45. if (p1.Age > p2.Age) 46. return 1;

47. else if (p1.Age == p2.Age) 48. return 0; 49. else 50. return -1; 51. } 52. } 53. 54. }

• ligne 8 : le tableau de personnes

ligne 12 : le tri du tableau de personnes selon les nom et prénom. Le 2ième paramètre de la méthode générique Sort est une instance d'une classe CompareNoms implémentant l'interface générique IComparer<Personne>.

lignes 30-39 : la classe CompareNoms implémentant l'interface générique IComparer<Personne>.

lignes 31-38 : implémentation de la méthode générique int CompareTo(T,T) de l'interface IComparer<T>. La méthode utilise la méthode String.CompareTo, présentée page 21, pour comparer deux chaînes de caractères.

ligne 16 : le tri du tableau de personnes selon les âges. Le 2ième paramètre de la méthode générique Sort est une instance d'une classe CompareAges implémentant l'interface générique IComparer<Personne> et définie lignes 42-51.

Les résultats de l'exécution sont les suivants :

1. Tableau à trier--- 2. [claude, pollon, 25]

3. [valentine, germain, 35] 4. [paul, germain, 32]

5. Tableau après le tri selon les nom et prénom--- 6. [paul, germain, 32]

7. [valentine, germain, 35] 8. [claude, pollon, 25]

9. Tableau après le tri selon l'âge--- 10. [claude, pollon, 25]

11. [paul, germain, 32] 12. [valentine, germain, 35]