首頁  >  文章  >  後端開發  >  C#理解泛型

C#理解泛型

黄舟
黄舟原創
2017-02-07 16:59:251050瀏覽

簡介

Visual C# 2.0 的一個最受期待的(或許也是最讓人畏懼)的一個特性就是對於泛型的支持。這篇文章將告訴你泛型用來解決什麼樣的問題,以及如何使用它們來提高你的程式碼質量,還有你不必恐懼泛型的原因。

泛型是什麼?

很多人覺得泛型很難理解。我相信這是因為他們通常在了解泛型是用來解決什麼問題之前,就被灌輸了大量的理論和範例。結果就是你有了一個解決方案,但是卻沒有需要使用這個解決方案的問題。

這篇文章將嘗試改變這個學習流程,我們將以一個簡單的問題作為開始:泛型是用來做什麼的?答案是:沒有泛型,將會很難建立型別安全的集合。

C# 是一種類型安全的語言,類型安全允許編譯器(可信賴地)捕獲潛在的錯誤,而不是在程序運行時才發現(不可信賴地,往往發生在你將產品出售了以後!)。因此,在C#中,所有的變數都有一個定義了的型別;當你將一個物件賦值給那個變數的時候,編譯器會檢查這個賦值是否正確,如果有問題,將會給出錯誤訊息。

在 .Net 1.1 版本(2003)中,當你在使用集合時,這種類型安全就失效了。由.Net 類別庫提供的所有關於集合的類別全是用來儲存基底類型(Object)的,而.Net中所有的一切都是由Object基類別繼承下來的,因此所有類型都可以放到一個集合中。於是,相當於根本就沒有了型別檢測。

更糟的是,每次你從集合中取出一個Object,你都必須將它強制轉換成正確的類型,這一轉換將對性能造成影響,並且產生冗長的代碼(如果你忘了進行轉換,將會拋出異常)。更進一步地講,如果你給集合中添加一個值類型(比如,一個整數變量),這個整數變量就被隱式地裝箱了(再一次降低了性能),而當你從集合中取出它的時候,又會進行一次顯式地拆箱(又一次性能的降低和型別轉換)。

關於裝箱、拆箱的更多內容,請造訪 陷阱4,警惕隱式的裝箱、拆箱。

創建一個簡單的線性鍊錶

為了生動地感受一下這些問題,我們將創建一個盡可能簡單的線性鍊錶。對於閱讀本文的那些從未創建過線性鍊錶的人。你可以將線性鍊錶想像成有一條鍊子栓在一起的盒子(稱作一個結點),每個盒子裡包含著一些數據和鏈接到這個鍊子上的下一個盒子的引用(當然,除了最後一個盒子,這個盒子對於下一個盒子的引用被設定為NULL)。

為了創建我們的簡單線性鍊錶,我們需要下面三個類別:

1、Node類,包含資料以及下一個Node的引用。

2、LinkedList類,包含鍊錶中的第一個Node,以及關於鍊錶的任何附加資訊。

3、測試程序,用於測試 LinkedList 類別。

為了查看連結表如何運作,我們將Objects的兩種類型添加到鍊錶中:整型 和 Employee類型。你可以將Employee類型想像成一個包含關於公司中某一個員工所有資訊的類別。出於演示的目的,Employee類別非常的簡單。

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

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

這個類別只包含一個表示員工名字的字串類型,一個設定員工名字的建構函數,一個傳回Employee名字的ToString()方法。

連結表本身是由許多的Node構成,這些Note,如上面所說,必須包含資料(整數 和 Employee)和鍊錶中下一個Node的參考。

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

注意建構函式將私有的資料成員設定成傳遞進來的對象,並且將 next 欄位設為null。

這個類別還包含一個方法,Append,這個方法接受一個Node類型的參數,我們將把傳遞進來的Node加到列表中的最後位置。這個過程是這樣的:先偵測目前Node的next字段,看它是不是null。如果是,那麼目前Node就是最後一個Node,我們將目前Node的next屬性指向傳遞進來的新結點,這樣,我們就把新Node插入到了鍊錶的尾部。

如果目前Node的next欄位不是null,表示目前node不是鍊錶中的最後一個node。因為next欄位的型別也是node,所以我們呼叫next欄位的Append方法(註:遞歸呼叫),再一次傳遞Node參數,這樣繼續下去,直到找到最後一個Node為止。

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

Node 類別中的 ToString() 方法也被覆寫了,用於輸出 data 中的值,並且調用下一個 Node 的 ToString()方法(譯註:再一次遞歸呼叫)。

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

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

    return output;
}

這樣,當你呼叫第一個Node的ToString()方法時,將會印出所有鍊錶上Node的值。

LinkedList 類別本身只包含一個Node的引用,這個Node稱為HeadNode,是鍊錶中的第一個Node,初始化為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&#39;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>{
    T data;
    ...

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

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

public class Node <UnknownType>{
    UnknownType data;
    ...

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

    Node<T> next;

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

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

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

public class LinkedList<T>{
    Node<T> headNode = null;

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

LinkedList<int> ll = new LinkedList<int>();

另一个是Employee类型的:

LinkedList<Employee> employees = new LinkedList<Employee>();

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

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

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


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


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
上一篇:C#中使用gRPC下一篇:C#中使用gRPC