C# comprend les génériques

黄舟
Libérer: 2017-02-07 16:59:25
original
1094 Les gens l'ont consulté

Introduction

L'une des fonctionnalités les plus attendues (et peut-être la plus intimidante) de Visual C# 2.0 est la prise en charge des génériques. Cet article vous expliquera quels types de problèmes les génériques sont utilisés pour résoudre, comment les utiliser pour améliorer la qualité de votre code et pourquoi vous n'avez pas à avoir peur des génériques.

Que sont les génériques ?

Beaucoup de gens trouvent les génériques difficiles à comprendre. Je pense que c'est parce qu'ils reçoivent souvent beaucoup de théorie et d'exemples avant de comprendre quel problème les génériques sont utilisés pour résoudre. Le résultat est que vous avez une solution, mais aucun problème nécessitant une solution.

Cet article va tenter de changer ce processus d’apprentissage. Nous commencerons par une question simple : à quoi servent les génériques ? La réponse est : sans génériques, il serait difficile de créer des collections de type sécurisé.

C# est un langage de type sécurisé. La sécurité des types permet au compilateur de détecter (de manière fiable) les erreurs potentielles au lieu de les découvrir lorsque le programme est en cours d'exécution (de manière peu fiable, ce qui arrive souvent lorsque vous vendez le produit) après !) . Par conséquent, en C#, toutes les variables ont un type défini ; lorsque vous affectez un objet à cette variable, le compilateur vérifie si l'affectation est correcte et donnera un message d'erreur en cas de problème.

Dans .Net version 1.1 (2003), cette sécurité de type est rompue lorsque vous utilisez des collections. Toutes les classes de collection fournies par la bibliothèque de classes .Net sont utilisées pour stocker les types de base (Object), et tout dans .Net est hérité de la classe de base Object, de sorte que tous les types peuvent être placés dans une collection. Par conséquent, cela équivaut à aucune détection de type.

Pour aggraver les choses, chaque fois que vous retirez un objet de la collection, vous devez le convertir dans le type correct. Cette conversion aura un impact sur les performances et produira du code détaillé (si vous oubliez de le faire). effectuez la conversion, une exception sera levée). De plus, si vous ajoutez un type de valeur à la collection (par exemple, une variable entière), la variable entière est implicitement encadrée (ce qui réduit encore les performances), et lorsque vous la supprimez de la collection Lorsqu'elle est utilisée, un déballage explicite sera effectué encore une fois (une autre réduction des performances et une conversion de type).

Pour plus d'informations sur le boxing et le unboxing, veuillez visiter Trap 4 et méfiez-vous du boxing et du unboxing implicites.

Créer une simple liste chaînée linéaire

Pour donner une idée vivante de ces problèmes, nous allons créer une liste chaînée linéaire aussi simple que possible. Pour ceux d'entre vous qui lisent ceci et qui n'ont jamais créé de liste chaînée linéaire. Vous pouvez considérer une liste chaînée linéaire comme une chaîne de cases (appelée nœud). Chaque case contient des données et une référence à la case suivante de la chaîne (sauf pour la dernière case, bien sûr, la référence de cette case). la case suivante est définie sur NULL).

Afin de créer notre simple liste chaînée linéaire, nous avons besoin des trois classes suivantes :

1. Classe Node, qui contient des données et une référence au nœud suivant.

2. Classe LinkedList, contient le premier nœud de la liste chaînée et toute information supplémentaire sur la liste chaînée.

3. Programme de test, utilisé pour tester la classe LinkedList.

Pour voir comment fonctionne la liste chaînée, nous ajoutons deux types d'objets à la liste chaînée : les types entier et employé. Vous pouvez considérer le type Employé comme une classe qui contient toutes les informations sur un employé de l'entreprise. À des fins de démonstration, la classe Employee est très simple.

public class Employee{
private string name;
  public Employee (string name){
    this.name = name;
  }

  public override string ToString(){
   return this.name;
  }
}
Copier après la connexion

Cette classe contient uniquement un type de chaîne qui représente le nom de l'employé, un constructeur qui définit le nom de l'employé et une méthode ToString() qui renvoie le nom de l'employé.

La liste chaînée elle-même est composée de nombreux nœuds. Ces notes, comme mentionné ci-dessus, doivent contenir des données (entier et employé) et une référence au nœud suivant dans la liste chaînée.

public class Node{
    Object data;
    Node next;

    public Node(Object data){
       this.data = data;
       this.next = null;
    }

    public Object Data{ 
       get { return this.data; }
       set { data = value; }
    }

    public Node Next{
       get { return this.next; }
       set { this.next = value; }
    }
}
Copier après la connexion

Notez que le constructeur définit les données membres privées sur l'objet transmis et définit le champ suivant sur null.

Cette classe comprend également une méthode, Append, qui accepte un paramètre de type Node Nous ajouterons le Node passé à la dernière position de la liste. Le processus est le suivant : vérifiez d'abord le champ suivant du nœud actuel pour voir s'il est nul. Si tel est le cas, alors le nœud actuel est le dernier nœud et nous pointons l'attribut suivant du nœud actuel vers le nouveau nœud transmis. De cette façon, nous insérons le nouveau nœud à la fin de la liste chaînée.

Si le champ suivant du nœud actuel n'est pas nul, cela signifie que le nœud actuel n'est pas le dernier nœud de la liste chaînée. Comme le type du champ suivant est également node, nous appelons la méthode Append du champ suivant (remarque : appel récursif) et transmettons à nouveau le paramètre Node, et cela continue jusqu'à ce que le dernier Node soit trouvé.

public void Append(Node newNode){
    if ( this.next == null ){
       this.next = newNode;
    }else{
       next.Append(newNode);
   }
}
Copier après la connexion

La méthode ToString() de la classe Node est également remplacée, utilisée pour afficher la valeur dans les données et appeler la méthode ToString() du Node suivant (Annotation : un autre appel récursif).

public override string ToString(){
    string output = data.ToString();

    if ( next != null ){
       output += ", " + next.ToString();
    }

    return output;
}
Copier après la connexion

De cette façon, lorsque vous appelez la méthode ToString() du premier nœud, les valeurs de tous les nœuds de la liste chaînée seront imprimées.

La classe LinkedList elle-même ne contient qu'une référence à un seul nœud. Ce nœud est appelé HeadNode, qui est le premier nœud de la liste chaînée et est initialisé à null.

public class LinkedList{
    Node headNode = null;
}
Copier après la connexion

LinkedList 类不需要构造函数(使用编译器创建的默认构造函数),但是我们需要创建一个公共方法,Add(),这个方法把 data存储到线性链表中。这个方法首先检查headNode是不是null,如果是,它将使用data创建结点,并将这个结点作为headNode,如果不是null,它将创建一个新的包含data的结点,并调用headNode的Append方法,如下面的代码所示:

public void Add(Object data){
    if ( headNode == null ){
       headNode = new Node(data);
    }else{
       headNode.Append(new Node(data));
    }
}
Copier après la connexion

为了提供一点集合的感觉,我们为线性链表创建一个索引器。

public object this[ int index ]{
    get{
       int ctr = 0;
       Node node = headNode;
       while ( node != null  &&ctr <= index ){
           if ( ctr == index ){
              return node.Data;
           }else{
              node = node.Next;
           }
           ctr++;
       }
    return null;
    }
}
Copier après la connexion

最后,ToString()方法再一次被覆盖,用以调用headNode的ToString()方法。

public override string ToString(){
    if ( this.headNode != null ){
       return this.headNode.ToString();
    }else{
       return string.Empty;
    }
}
Copier après la connexion

测试线性链表

我们可以添加一些整型值到链表中进行测试:

public void Run(){
    LinkedList ll = new LinkedList();
    for ( int i = 0; i < 10; i ++ ){
       ll.Add(i);
    }

    Console.WriteLine(ll);
    Console.WriteLine("  Done.Adding employees...");
}
Copier après la connexion

如果你对这段代码进行测试,它会如预计的那样工作:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...
Copier après la connexion

然而,因为这是一个Object类型的集合,所以你同样可以将Employee类型添加到集合中。

ll.Add(new Employee("John"));
ll.Add(new Employee("Paul"));
ll.Add(new Employee("George"));
ll.Add(new Employee("Ringo"));

Console.WriteLine(ll);
Console.WriteLine(" Done.");
Copier après la connexion

输出的结果证实了,整型值和Employee类型都被存储在了同一个集合中。

0, 1, 2, 3, 4, 5, 6, 7, 8, 9
  Done. Adding employees...
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, John, Paul, George, Ringo
Done.
Copier après la connexion

虽然看上去这样很方便,但是负面影响是,你失去了所有类型安全的特性。因为线性链表需要的是一个Object类型,每一个添加到集合中的整型值都被隐式装箱了,如同 IL 代码所示:

IL_000c:  box        [mscorlib]System.Int32
IL_0011:  callvirt   instance void ObjectLinkedList.LinkedList::Add(object)
Copier après la connexion

同样,如果上面所说,当你从你的列表中取出项目的时候,这些整型必须被显式地拆箱(强制转换成整型),Employee类型必须被强制转换成Employee类型。

Console.WriteLine("The fourth integer is " +Convert.ToInt32(ll[3]));
Employee d = (Employee) ll[11];
Console.WriteLine("The second Employee is " + d);
Copier après la connexion

这些问题的解决方案是创建一个类型安全的集合。一个 Employee 线性链表将不能接受 Object 类型;它只接受 Employee类的实例(或者继承自Employee类的实例)。这样将会是类型安全的,并且不再需要类型转换。一个整型的 线性链表,这个链表将不再需要装箱和拆箱的操作(因为它只能接受整型值)。

作为示例,你将创建一个 EmployeeNode,该结点知道它的data的类型是Employee。

public class EmployeeNode {
    Employee employeedata;
    EmployeeNode employeeNext;
}
Copier après la connexion

Append 方法现在接受一个 EmployeeNode 类型的参数。你同样需要创建一个新的EmployeeLinkedList ,这个链表接受一个新的 EmployeeNode:

public class EmployeeLinkedList{
    EmployeeNode headNode = null;
}
Copier après la connexion

EmployeeLinkedList.Add()方法不再接受一个 Object,而是接受一个Employee:

public void Add(Employee data){
    if ( headNode == null ){
       headNode = new EmployeeNode(data);}
    else{
       headNode.Append(new EmployeeNode(data));
    }
}
Copier après la connexion

类似的,索引器必须被修改成接受 EmployeeNode 类型,等等。这样确实解决了装箱、拆箱的问题,并且加入了类型安全的特性。你现在可以添加Employee(但不是整型)到你新的线性链表中了,并且当你从中取出Employee的时候,不再需要类型转换了。

EmployeeLinkedList employees = new EmployeeLinkedList();
employees.Add(new Employee("Stephen King"));
employees.Add(new Employee("James Joyce"));
employees.Add(new Employee("William Faulkner"));
/* employees.Add(5);  // try toadd an integer - won&#39;t compile */
Console.WriteLine(employees);
Employee e = employees[1];
Console.WriteLine("The second Employee is " + e);
Copier après la connexion

这样多好啊,当有一个整型试图隐式地转换到Employee类型时,代码甚至连编译器都不能通过!

但它不好的地方是:每次你需要创建一个类型安全的列表时,你都需要做很多的复制/粘贴 。一点也不够好,一点也没有代码重用。同时,如果你是这个类的作者,你甚至不能提前欲知这个链接列表所应该接受的类型是什么,所以,你不得不将添加类型安全这一机制的工作交给类的使用者---你的用户。

使用泛型来达到代码重用

解决方案,如同你所猜想的那样,就是使用泛型。通过泛型,你重新获得了链接列表的   代码通用(对于所有类型只用实现一次),而当你初始化链表的时候你告诉链表所能接受的类型。这个实现是非常简单的,让我们重新回到Node类:

public class Node{
    Object data;
    ...
Copier après la connexion

注意到 data 的类型是Object,(在EmployeeNode中,它是Employee)。我们将把它变成一个泛型(通常,由一个大写的T代表)。我们同样定义Node类,表示它可以被泛型化,以接受一个T类型。

public class Node <T>{
    T data;
    ...
Copier après la connexion

读作:T类型的Node。T代表了当Node被初始化时,Node所接受的类型。T可以是Object,也可能是整型或者是Employee。这个在Node被初始化的时候才能确定。

注意:使用T作为标识只是一种约定俗成,你可以使用其他的字母组合来代替,比如这样:

public class Node <UnknownType>{
    UnknownType data;
    ...
Copier après la connexion

通过使用T作为未知类型,next字段(下一个结点的引用)必须被声明为T类型的Node(意思是说接受一个T类型的泛型化Node)。

    Node<T> next;
Copier après la connexion

构造函数接受一个T类型的简单参数:

public Node(T data)
{
    this.data = data;
    this.next = null;
}
Copier après la connexion

Node 类的其余部分是很简单的,所有你需要使用Object的地方,你现在都需要使用T。LinkedList类现在接受一个 T类型的Node,而不是一个简单的Node作为头结点。

public class LinkedList<T>{
    Node<T> headNode = null;
Copier après la connexion

再来一遍,转换是很直白的。任何地方你需要使用Object的,现在改做T,任何需要使用Node的地方,现在改做Node。下面的代码初始化了两个链接表。一个是整型的。

LinkedList<int> ll = new LinkedList<int>();
Copier après la connexion

另一个是Employee类型的:

LinkedList<Employee> employees = new LinkedList<Employee>();
Copier après la connexion

剩下的代码与第一个版本没有区别,除了没有装箱、拆箱,而且也不可能将错误的类型保存到集合中。

LinkedList<int> ll = new LinkedList<int>();
for ( int i = 0; i < 10; i ++ )
{
    ll.Add(i);
}

Console.WriteLine(ll);
Console.WriteLine(" Done.");

LinkedList<Employee> employees = new LinkedList<Employee>();
employees.Add(new Employee("John"));
employees.Add(new Employee("Paul"));
employees.Add(new Employee("George"));
employees.Add(new Employee("Ringo"));

Console.WriteLine(employees); 
Console.WriteLine(" Done.");
Console.WriteLine("The fourth integer is " + ll[3]);
Employee d = employees[1];
Console.WriteLine("The second Employee is " + d);
Copier après la connexion

泛型允许你不用复制/粘贴冗长的代码就实现类型安全的集合。而且,因为泛型是在运行时才被扩展成特殊类型。Just In Time编译器可以在不同的实例之间共享代码,最后,它显著地减少了你需要编写的代码。


以上就是C#理解泛型的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal