Home  >  Article  >  Backend Development  >  C# understands generics

C# understands generics

黄舟
黄舟Original
2017-02-07 16:59:25958browse

Introduction

One of the most anticipated (and perhaps the most daunting) features of Visual C# 2.0 is support for generics. This article will tell you what kind of problems generics are used to solve, how to use them to improve the quality of your code, and why you don't have to be afraid of generics.

What are generics?

Many people find generics difficult to understand. I believe this is because they are often fed a lot of theory and examples before they understand what problem generics are used to solve. The result is that you have a solution, but no problem that requires the solution.

This article will try to change this learning process. We will start with a simple question: What are generics used for? The answer is: without generics, it would be difficult to create type-safe collections.

C# is a type-safe language. Type safety allows the compiler to (reliably) catch potential errors instead of discovering them when the program is running (unreliably, which often happens when you sell the product) after!). Therefore, in C#, all variables have a defined type; when you assign an object to that variable, the compiler checks whether the assignment is correct and will give an error message if there is a problem.

In .Net version 1.1 (2003), this type safety is broken when you use collections. All collection classes provided by the .Net class library are used to store base types (Object), and everything in .Net is inherited from the Object base class, so all types can be put into a collection. middle. Therefore, it is equivalent to no type detection at all.

What's worse, every time you take an Object out of the collection, you have to cast it to the correct type. This conversion will have a performance impact and produce verbose code (if you If you forget to do the conversion, an exception will be thrown). Furthermore, if you add a value type to the collection (for example, an integer variable), the integer variable is implicitly boxed (again reducing performance), and when you remove it from the collection When it is used, explicit unboxing will be performed again (another performance reduction and type conversion).

For more information about boxing and unboxing, please visit Trap 4. Be wary of implicit boxing and unboxing.

Create a simple linear linked list

In order to feel these problems vividly, we will create a linear linked list as simple as possible. For those of you reading this who have never created a linear linked list. You can think of a linear linked list as a chain of boxes (called a node). Each box contains some data and a reference to the next box in the chain (except for the last box, of course). , this box's reference to the next box is set to NULL).

In order to create our simple linear linked list, we need the following three classes:

1. Node class, which contains data and a reference to the next Node.

2. The LinkedList class contains the first Node in the linked list and any additional information about the linked list.

3. Test program, used to test the LinkedList class.

To see how the linked list works, we add two types of Objects to the linked list: integer and Employee types. You can think of the Employee type as a class that contains all the information about an employee in the company. For demonstration purposes, the Employee class is very simple.

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

  public override string ToString(){
   return this.name;
  }
}

This class only contains a string type that represents the employee's name, a constructor that sets the employee's name, and a ToString() method that returns the employee's name.

The linked list itself is composed of many Nodes. These Notes, as mentioned above, must contain data (integer and Employee) and a reference to the next Node in the linked list.

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; }
    }
}

Note that the constructor sets the private data members to the passed in object and sets the next field to null.

This class also includes a method, Append, which accepts a Node type parameter. We will add the passed Node to the last position in the list. The process is like this: first check the next field of the current Node to see if it is null. If it is, then the current Node is the last Node, and we point the next attribute of the current Node to the new node passed in. In this way, we insert the new Node into the end of the linked list.

If the next field of the current Node is not null, it means that the current node is not the last node in the linked list. Because the type of the next field is also node, we call the Append method of the next field (note: recursive call) and pass the Node parameter again, and this continues until the last Node is found.

public void Append(Node newNode){
    if ( this.next == null ){
       this.next = newNode;
    }else{
       next.Append(newNode);
   }
}

The ToString() method in the Node class is also overridden, used to output the value in data, and call the ToString() method of the next Node (Annotation: another recursive call).

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

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

    return output;
}

In this way, when you call the ToString() method of the first Node, the values ​​of all Nodes on the linked list will be printed out.

The LinkedList class itself only contains a reference to one Node. This Node is called HeadNode, which is the first Node in the linked list and is initialized to null.

public class LinkedList{
    Node headNode = null;
}

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));
    }
}

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

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;
    }
}

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

public override string ToString(){
    if ( this.headNode != null ){
       return this.headNode.ToString();
    }else{
       return string.Empty;
    }
}

测试线性链表

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

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...");
}

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

0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...

然而,因为这是一个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.");

输出的结果证实了,整型值和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.

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

IL_000c:  box        [mscorlib]System.Int32
IL_0011:  callvirt   instance void ObjectLinkedList.LinkedList::Add(object)

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

Console.WriteLine("The fourth integer is " +Convert.ToInt32(ll[3]));
Employee d = (Employee) ll[11];
Console.WriteLine("The second Employee is " + d);

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

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

public class EmployeeNode {
    Employee employeedata;
    EmployeeNode employeeNext;
}

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

public class EmployeeLinkedList{
    EmployeeNode headNode = null;
}

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

public void Add(Employee data){
    if ( headNode == null ){
       headNode = new EmployeeNode(data);}
    else{
       headNode.Append(new EmployeeNode(data));
    }
}

类似的,索引器必须被修改成接受 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't compile */
Console.WriteLine(employees);
Employee e = employees[1];
Console.WriteLine("The second Employee is " + e);

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

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

使用泛型来达到代码重用

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

public class Node{
    Object data;
    ...

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

public class Node {
    T data;
    ...

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

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

public class Node {
    UnknownType data;
    ...

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

    Node next;

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

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

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

public class LinkedList{
    Node headNode = null;

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

LinkedList ll = new LinkedList();

另一个是Employee类型的:

LinkedList employees = new LinkedList();

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

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

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

LinkedList employees = new LinkedList();
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);

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


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


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Previous article:Using gRPC in C#Next article:Using gRPC in C#