天天看點

C#中的群集, 泛型和計時類

C#中的群集, 泛型和計時類

大家好,我是蘇州程式大白,今天跟大家講講C#中資料結構體與算法。内容有點多。我這裡會持續更新,希望大家關注我、支援我,謝謝大家。不廢話了下面我們開始

群集, 泛型和計時類介紹

(注:群集指Collection)

本文章介紹如何使用C#開發和實作資料結構和算法, 期間用到的資料結構在. NETFramework庫的System. Collections中. 在本章首先将讨論如何使用數組實作自制的群集類, 然後學習. NETFramework的群集類, 最終幫助我們了解群集的概念.

泛型是C#2. 0的一個重要補充. 泛型允許C#程式員不必因不同的資料類型而多次重載函數. C#2. 0提供了一個特殊的庫, System. Collections. Generic, 它為一些System. Collections中的資料結構提供泛型支援. 本章将向讀者介紹泛型程式設計.

本章最後, 介紹了一個自定義的類, Timing類, 我們将在幾章中使用它來衡量資料結構或算法的性能. 它将取代大O分析, 這不是因為大O分析不重要, 而是因為這本書對資料結構和算法的研究采取了更偏向實踐的方法。

定義群集

群集是一種結構化的資料類型, 用于儲存和操作資料, 能夠完成資料的添加, 删除, 與修改, 并能指派和讀取群集的各種設定屬性。

群集可以分為兩種類型:線性和非線性. 線性群集指, 群集中的元素順序排列, 彼此之間具有前後關系. 線性群集中的元素通常按照位置排序. 現實中, 貨物清單就是線性群集的一個例子;在計算機世界中, Array被設計為線性群集。

非線性群集中的元素彼此之間沒有位置關系. 組織結構圖是非線性群集的一個例子, 就像金字塔的形狀那樣. 在計算機世界中, tree, heap, graph和set都是非線性群集。

無論是哪一種群集, 都有屬性定義, 描述它們本身和可以對它們進行的操作. 比如Count屬性, 它表示群集中元素的數量. 對群集的操作, 稱之為方法, 比如用于添加元素的Add方法, 用于移除指定元素的Remove方法, 用于移除所有元素的Clear方法, 用于檢查某個元素是否存在于群集中的Contains方法, 以及用于檢查指定元素在群集中索引的IndexOf方法等。

描述群集

兩個主要群集類型還有一些子類别. 線性群集既可以是直接通路的也可以是順序通路的;非線性群集既可以是分組的也可以是分級的, 本節對它們都會進行介紹.

可直接通路群集

可直接通路群集的代表是Array, Array中元素的資料類型相同, 并可以通過整數索引直接通路, 如下圖:

C#中的群集, 泛型和計時類

Array的可以是靜态的, 表示在聲明Array時指定的元素總數在程式中固定不變;或者Array也可以是動态的, 動态Array的元素數量可以通過ReDim或ReDimPreserve語句增加.

在C#中, Array不是一種基本資料類型, 而是類. 本節後面探究Array更多的細節時, 會讨論Array是如何作為類使用的。

我們可以使用Array存儲線性群集. 簡單的向第一個或最後一個空位放置就可以為Array增加元素. 而向Array中間插入元素則不太容易和有效率, 因為我們需要将插入位置後面的所有元素向後移動, 以便為新元素讓出空位. 同樣的原因, 在首位或末尾移除Array元素并不難, 而在中間移除元素則不太容易并效率低下. 根本原因是我們對Array做的任何操作都需要保持元素之間的連續性, 關于這部分的細節将在本節後面讨論. . NETFramework提供了專門的ArrayList類來使線性群集的程式設計更加容易, 第三章将探讨這部分内容

另一種可以直接通路的群集類型是string, 它是字元串的群集, 也可以通過索引通路每個元素. string在C#中也以類的形式實作, 該類提供了一大批用于對字元串操作的方法, 如連接配接字元串, 傳回子字元串, 插入字元, 移除字元等, 會在後面詳細介紹

C#的字元串是不可變的, 初始化後不能改變, 修改字元串時, 實際将建立字元串的副本, 而不是修改原始的字元串. 這種機制在一些情況下會降低程式運作的性能, 是以. NETFramework提供了StringBuilder類來提供使用可變字元串的途徑. 這部分内容也放在了後面詳細介紹

最後一種可直接方法的群集類型是Struct, 它是一種包含許多不同資料類型的複合資料類型. 比如, 一名員工的資訊包括姓名(字元串), 工資(數字), 身份證号(數字或字元串)等等. 由于這些資料分散的存儲在單獨的變量中不友善管理, 是以程式設計語言提供了Struct用于存儲這種情況的資料組合

C#中Struct的一個強大之處是, 在其内部可以定義方法, 這使它表現的像是類不過它并不能繼承或派生新類型:

using System;
public struct Name {
    private string fname, mname, lname;
    public Name(string first, string middle, string last) {
        fname = first;
        mname = middle;
        lname = last;
    }
    public string firstName {
        get {return fname;}
        set {fname = firstName;}
    }
    public string middleName {
        get {return mname;}
        set {mname = middleName;}
    }
    public string lastName {
        get {return lname;}
        set {lname = lastName;}
    }
    public override string ToString() {
        return (String.Format("{0} {1} {2}", fname, mname,lname));
    }
    public string Initials() {
        return (String.Format("{0}{1}{2}",fname.Substring(0,1), mname.Substring(0,1), lname.Substring(0,1)));
    }
}
public class NameTest {
    static void Main() {
        Name myName = new Name("Michael", "Mason", "McMillan");
        string fullName, inits;
        fullName = myName.ToString();
        inits = myName.Initials();
        Console.WriteLine("My name is {0}.", fullName);
        Console.WriteLine("My initials are {0}.", inits);}
}           

複制

. NET環境下的許多内容都被實作為了類, 但有幾種基本類型是使用Struct實作的, 比如說整數類型Int32就是一種Struct類型, 該Struct類型提供的方法之一Parse方法, 可以将代表數字的字元串轉類型換為整數類型:

using System;
public class IntStruct {
    static void Main() {
        int num;
        string snum;
        Console.Write("Enter a number: ");
        snum = Console.ReadLine();
        num = Int32.Parse(snum);
        Console.WriteLine(num);
    } 
}           

複制

按順序通路的群集

可順序通路的群集就是一種按照順序存儲元素的清單, 是以叫這種群集為線性表. 線性表建立時并不需要限制其大小, 也就是說它可以動态的擴充或收縮. 線性表中的項不能被直接通路, 它們由在清單中的位置引用, 第一個元素在頭, 最後一個元素在尾, 如下圖:

C#中的群集, 泛型和計時類

由于不能直接存取線性表的元素, 為了通路某個元素就需要周遊線性表直到到達要找元素的 位置為止. 線性表的實作通常允許兩種周遊表的方法:一種是單向從前往後周遊, 而另一種 則是雙向周遊, 即從前向後和從後向前周遊。

線性表的一個簡單執行個體就是購物清單. 順次寫下要購買的全部商品就會形成一張購物清單. 在購物時一旦找到某種商品就把它從清單中劃掉。

線性表可以是有序的, 也可以是無序的. 有序清單的順序具有特定的含義, 比如下列稱謂:

少林寺駐武當山辦事處大神父王喇嘛

而無序線性表則是由無序元素組成的. 在第2章對二叉查找算法與簡單線性查找算法進行 讨論時就會發現線性表的順序會在查找表中資料時産生很大的差異.

線性表的某些類型限制通路資料元素. 這類線性表有堆棧(Stack)和隊列. 堆棧是一種隻允許在表頭 (或頂端)存取資料的表. 在表的頂端放置資料項, 而且也隻能從表的頂端移出資料項. 正是基于這種原因, 堆棧也被稱為後進先出結構. 這裡把向堆棧添加資料項的操作稱為入堆棧(push), 而把從堆棧移出資料項的操作稱為出堆棧(pop). 如圖展示了堆棧的這兩種操作。

C#中的群集, 泛型和計時類

堆棧是非常常見的一種資料結構, 特别是在計算機系統程式設計中尤為普遍. 在堆棧的衆多應用 中, 它常用于算術表達式的計算和平衡符号.

隊列是一種隻允許在表尾進行資料項添加和隻能在表頭進行移出操作的表. 它也被稱為是先進先出結構. 這 裡把向隊列添加資料項稱為入隊(EnQueue), 而把從隊列移出資料項稱為離隊(DeQueue). 如圖展示了隊列的這兩種操作。

C#中的群集, 泛型和計時類

隊列既可用于排程作業系統任務的系統程式設計, 也可用于模拟研究的程式設計. 在每一種能想象到的少量情況下, 隊列都可以為模拟等待隊列産生極好的結構. 優先隊列是隊列的一種特殊類 型. 它允許具有最高優先級的資料項被最先移出隊列. 例如, 優先隊列可以用來研究醫院急 診室的操作, 這裡應該對心髒病突發患者先進行救護, 然後再處理手臂骨折患者.

最後要讨論的一類線性群集被稱為通用的索引群集. 這類群集的第一種就是哈希表. 它存儲 了一組與關鍵字相關聯的資料值. 在哈希表中有一個被稱為哈希函數的特殊函數. 此函數會 取走一個資料值, 并且把此資料值(稱為關鍵字)轉換成用來取回資料的整數索引. 然後此 索引會用來存取通路與關鍵字相關聯的資料記錄. 例如, 一條雇員記錄可能由雇員姓名、薪 水、工作年限以及所工作的部門組成. 此結構如圖所示. 此資料記錄的關鍵字就是雇員 的姓名. C#有一個稱為HashTable的類用來存儲哈希表的資料. 會在後面文章讨論此結構。

C#中的群集, 泛型和計時類

另外一種通用的索引群集就是字典. 它是由一系列鍵值對構成的. 此結 構與詞典書籍類似, 詞典中的詞是關鍵字, 而詞的定義則是與關鍵字相關聯的值. 關鍵字就是與 其相關聯的值内的索引. 雖然索引不需要就是整數, 但是由于上述這種索引方案, 是以還是 常把字典稱為關聯數組. 後面文章會對. NET架構内容的幾種字典類進行讨論。

層次群集

非線性群集分為兩大主要類型:層次群叢集組群集. 層次群集是一組劃分了層次的資料項集 合. 位于某一層的資料項可能會有位于下一較低層上的後繼資料項. 樹是一種常見的層次群集. 樹群集看上去像是一棵倒立的樹, 其中一個資料項作為根, 而其 他資料值則作為葉子挂在根的下面. 樹的元素被稱為節點, 而且在特定節點下面的元素被稱 為是此節點的孩子. 如圖展示了一棵執行個體樹.

C#中的群集, 泛型和計時類

樹在幾種不同的領域都有應用. 大多數現代作業系統的檔案系統都是采用樹群集設計而成 的, 其中一個目錄作為根, 而其他子目錄則作為根的孩子們.

二叉樹是樹群集的一種特殊類型, 樹中每個節點最多隻有兩個孩子. 二叉樹可以變成二叉查 找樹, 這樣做可以極大地提高查找大量資料的效率. 實作的方法是依據從根到要存儲資料的 節點的路徑為最短路徑的方式來放置節點.

還有一種樹類型就是堆. 堆始終把最小資料值放置在根節點上. 在删 除時會移除根節點. 此外, 堆的插入和删除操作總是會導緻堆的重組, 因為隻有這樣才能把 最小值放在根節點上. 我們經常會用堆來排序, 這被稱為是堆排序. 通過反複删除根節點以及重組堆的方式就可以對存儲在堆内的資料元素進行排序.

後面文章将對幾種不同類型的樹進行讨論.

組群集

資料項為無序的非線性群集被稱為組. 集合set、圖graph和網絡network是組群集的三種主要類型. 集合是一種無序資料值的群集, 并且集合中每一個資料值都是唯一的. 就像整數一樣, 班級中學生的清單就是一個集合的執行個體. 在集合上執行的操作包括聯合和交叉. 如圖顯示 了集合操作的執行個體.

![在這裡插入圖檔描述](https://img-blog.csdnimg.cn/20210428112848971.png

C#中的群集, 泛型和計時類

圖是由節點集合以及與節點相連的邊集合組成的. 圖用來對必須通路圖中每個節點的情況進行模組化, 而且有些時候還要按照特定順序進行通路. 這樣做的目的是為了找到“周遊”圖的 最有效的方法. 圖可用于物流及工作的排程, 也可用于計算機科學和數學研究領域. 大家可能聽說過“旅行商”問題. 這就是圖問題的一種特殊類型. 此問題要求在旅行預算允許的條件下為需要拜訪路線 中所有城市的商人确定最有效的完整旅行路線. 此問題的執行個體圖表示在圖中.

C#中的群集, 泛型和計時類

此問題是被稱為NP-完備問題的其中一部分内容. 這就意味着針對此類型的大量問題是無 法知道确切解決方案的. 例如, 為了找到圖上所示問題的解決方案, 需要檢查10的階乘 這麼多條線路, 這等于是3628800條線路. 如果把問題擴充為100座城市, 就需要檢查 100的階乘條線路. 就目前方法而言是無法用現在方法實作的. 是以需要找到一種近似的 解決方案.

網絡是圖的一種特殊類型. 網絡的每一條邊都被賦予了權. 權同使用某邊從一個節點移動到 另一個節點所花費的代價相關. 如圖描述了帶權的城市網絡, 其中這裡的權是兩座城市(節 點)之間的英裡數。

C#中的群集, 泛型和計時類

至此已經對将要在本書中讨論的不同群集類型做了總體的概述. 下面就準備實際看一看這些 群集是如何用C#實作的了. 首先會看到如何用來自. NET架構的抽象類CollectionBase 類來建構一個Collection類。

CollectionBase類

. NET架構庫不包括用于存儲資料的通用Collection類, 但是大家可以使用一種抽象的類 CollectionBase類來構造屬于自己的Collection類. CollectionBase類為程式員提供了實作 定制Collection類的能力. CollectionBase類隐含實作了兩個為構造Collection類所必需的 接口, 即ICollection和IEnumerable, 而留給程式員要做的工作就隻是對這些作為Collection類特殊内容的方法的實作.

用ArrayLists實作Collection類

本節将要說明如何用C#來實作自身的Collection類. 這是出于幾種目的考慮. 首先, 如果大家不是很熟悉面向對象程式設計(OOP), 那麼這個實作将會展示一些簡單的用C# 進行面向對象程式設計的技巧. 其次, 就如同讨論各種C#資料結構一樣, 此節内容還可用于讨 論一些将要出現的性能問題. 最後, 就像本書中其他實作章節一樣, 本節内容也會使人獲益 良多, 因為僅僅用語言自身的元素就能重新實作已存在的資料結構實在是充滿樂趣的事. 正 如DonKunth(計算機科學的先驅之一)所說的那樣, 也許隻有把一些知識教給計算機後才算真正的學到了它們. 是以, 比起使用日常程式設計庫中選取現成的類來使用, 通過講解C#如何實作不 同資料結構的過程将會使大家學會更多關于這些結構的知識.

定義Collection類

在C#中定義一個Collection類最簡單的方法就是把在System. Collections庫中的抽象類CollectionBase作為基礎類. 此類提供了一套可以實作構造自身群集的抽象方 法集合. CollectionBase類還提供了一種基礎的資料結構——InnerList(一個ArrayList). 此結構可以用作自身類的基礎. 本章節會看到如何使用CollectionBase來構造Collection 類.

實作Collection類

彌補Collection類的全部方法包括一些與類的基礎資料結構InnerList互相動的類型. 本節第一部分要實作的方法是Add方法、Remove方法、Count方法和Clear方法. 盡管還有其他方法可以使類更有用, 但是上述這些方法是類的絕對基本要素. 首先從Add方法開始. 該方法有一個參數, 即Object變量. 此變量用來儲存群集要添加 的資料項. 代碼如下所示:

using System;
using System.Collections;

public class Collection : CollectionBase{
    public void Add(Object item)    {
        InnerList.Add(item);
    }
}           

複制

ArrayList把資料作為對象(即Object資料類型)來存儲. 這就是把參數item聲明為Object 的原因. 第2章将會學到更多有關ArrayList的内容.

Remove方法的實作與上述類似:

public void Remove(Object item) 
{ 
    InnerList.Remove(item); 
}           

複制

接下來是Count方法. Count最常作為屬性來實作, 但是這裡更喜歡把它用作方法. 而且, 由于是在基礎類CollectionBase中實作Count, 是以必須用new關鍵字來隐藏在CollectionBase中已有的Count定義:

public new int Count() 
{      
    return InnerList.Count; 
}            

複制

Clear方法應該把全部資料項從InnerList中移除. 這裡也需要在方法定義中使用new關鍵字:

public new void Clear()
{ 
    InnerList.Clear(); 
}            

複制

這些方法目前對于Collection類已經足夠了. 接着來看一段使用了Collection類的程式代碼:

using System;
using System.Collections;

public class Collection : CollectionBase {
    public void Add(Object item)
    {
        InnerList.Add(item);
    }

    public void Remove(Object item)
    {
        InnerList.Remove(item);
    }

    public new void Clear()
    {
        InnerList.Clear();
    }

    public new int Count()
    {
        return InnerList.Count;
    }
}

class chapter1{
    static void Main()
    {
        Collection names = new Collection();
        names.Add("David");
        names.Add("Bernica");
        names.Add("Raymond");
        names.Add("Clayton");
        foreach (Object name in names) {
            Console.WriteLine(name);
        }        
        Console.WriteLine("名字的總數是: " + names.Count());
        names.Remove("Raymond");
        Console.WriteLine("名字的總數是: " + names.Count());
        names.Clear();
        Console.WriteLine("名字的總數是: " + names.Count());
    }
}           

複制

為了建立一個更加有用的Collection類, 還可以實作幾種其他的方法. 大家可以在練習中實 現一些這樣的方法.

泛型程式設計

面向對象程式設計的問題之一就是所謂“代碼膨脹”. 為了說明方法參數所有可能的資料 類型而需要重載某種方法或重載一系列方法時, 就會發生使得代碼的數量急劇膨脹. 代碼膨脹的解決方案之一就是使某個值呈現多種資料類型的能力, 同時僅提供此值的一種定義. 這種程式設計方法被稱為泛型程式設計.

泛型程式設計提供資料類型“占位符”. 它在編譯時由特定的資料類型填充. 這個占位符用一對 尖括号<>和放在括号間的辨別符來表示.

下面來看一個執行個體. 泛型程式設計第一個規範執行個體就是Swap函數. 下面是C#中泛型Swap函數的定義:

static void Swap<T>(ref T val1, ref T val2) 
{ 
    T temp; 
    temp = val1; 
    val1 = val2; 
    val2 = temp; 
}            

複制

資料類型占位符直接放置在函數名後邊. 在方法調用的時候使用所需類型替換掉泛型辨別符T, 這樣被标記為T的資料類型就會按照指定的類型生效. 下面就是一個測試此代碼的程式:

using System; 
class chapter1 
{ 
    static void Main() 
    { 
        int num1 = 100; 
        int num2 = 200; 
        Console.WriteLine("num1: " + num1); 
        Console.WriteLine("num2: " + num2); 
        Swap<int>(ref num1, ref num2); 
        Console.WriteLine("num1: " + num1); 
        Console.WriteLine("num2: " + num2); 
        string str1 = "Sam"; 
        string str2 = "Tom"; 
        Console.WriteLine("String 1: " + str1); 
        Console.WriteLine("String 2: " + str2); 
        Swap<string>(ref str1, ref str2); 
        Console.WriteLine("String 1: " + str1); 
        Console.WriteLine("String 2: " + str2); 
    } 

    static void Swap<T>(ref T val1, ref T val2) 
    { 
        T temp; 
        temp = val1; 
        val1 = val2; 
        val2 = temp; 
    } 
}            

複制

程式的輸出如下所示:

C#中的群集, 泛型和計時類

除了泛型函數, 還可以建立泛型類. 泛型類的定義包括一個跟在類名後邊的 泛型類型占位符. 任何定義中引用類名的時候都必須提供類型占位符. 下面的類定義說明了建立泛型類的方法:

public class Node<T> 
{ 
      T data; 
      Node<T> link; 

      public Node(T data, Node<T> link) 
      { 
            this.data = data; 
            this.link = link; 
      } 
}            

複制

可以按照如下形式使用此類:

Node<string> node1 = new Node<string>("Mike", null); 
Node<string> node2 = new Node<string>("Raymond", node1);            

複制

本文章讨論到的幾種資料結構都将采用Node類.

因為泛型程式設計十分有用的, 是以C#提供了可以直接使用的泛型資料結構庫. 在System. Collection. Generics命名空間中可以找到它們, 本書在介紹此命 名空間中的資料結構時, 還将探究它們的使用方法.

時間測試

文章采用了一種實用的方法來分析資料結構與算法檢測, 是以這裡避開使用大O分析法, 而采用運作簡單基準測試的方式來代替. 這種測試将會說明運作一段代碼需要多少時間.

基準法測試是用時間測試的方式來衡量運作完整算法所花費的時間長度. , 基 準測試既是藝術也是科學, 為了獲得準确的分析結果, 要仔細設計測試代碼. 下面就來進行詳細讨論.

一個簡單的時間測試

首先時間測試需要一些代碼. 出于簡單的考慮, 這裡将測試一個控制台列印數組内容的子程式(subroutine). 代碼如下所示:

static void DisplayNums(int[] arr) 
{ 
    for (int i = 0; i <= arr.GetUpperBound(0); i++) 
        Console.Write(arr[i] + " "); 
}            

複制

arr數組參數的初始化放在了程式的另外一部分, 這部分稍後再進行研究. 為了測試這個子程式, 需要建立一個變量, 并且把子程式調用時的系統時間指派給此變量. 此外, 還需要一個變量用來存儲子程式結束時的時間. 根據這些内容寫出了下述這段代碼:

DateTime startTime; 
TimeSpan endTime; 
startTime = DateTime.Now; 
DisplayNums();
DisplayNums();
DisplayNums();
endTime = DateTime.Now.Subtract(startTime);           

複制

在筆記本中(運作環境:機器主頻1. 4mHz, 作業系統WindowsXP專業版)上運作此 代碼時, 子程式的運作時間大約為5秒左右(4. 9917秒). 雖然這段代碼執行的時間測試 好像很有道理, 但是在. NET環境下運作時間代碼是完全不合适的. 為什麼呢?

首先, 代碼測量的是從子程式調用開始到子程式傳回主程式之間流失的時間. 但是測試所測 量的時間也包含了與C#程式同時運作的其他程序所用的時間.

其次, 時間代碼不考慮. NET環境下執行的GC(garbagecollection). 在類似. NET這樣的運作時間環 境中, 系統可能在執行GC的任何時刻暫停. 時間代碼執行個體沒有考慮GC時間, 是以時間測試結果很容易受GC影響. 那麼到底應該怎麼做呢?

用于. NET環境的時間測試

在. NET環境中編寫時間測試代碼時, 需要考慮到程式運作所處的線程, 以及GC可能在任何時候發生.

先來看一下如何處理GC. 首先讨論一下GC的用途. C#中的引用類型(例如字元串、數組以及類)被配置設定在記憶體的堆(heap)中, 堆是用來儲存前面提到的類型的記憶體區域. 諸如普通變量這樣的值類型則存儲在堆棧中. 對引用類型的引用也存儲在堆棧中, 但是引用所指向的實際的資料則存儲在堆中.

當聲明變量的子程式完全執行結束時就可以釋放掉存儲在堆棧中的變量. 而另一方面, 存儲 在堆中的變量則會一直保留到調用GC過程的時候, 不過隻有那些已經不在任何地方被引用的資料才會被GC過程從堆中釋放掉

程式執行過程中, GC可以發生在任何時刻. 而我們要確定的是在時間測試代碼運作期間, 不會發生GC. 可以通過手動調用GC過程來阻止GC随意發生. . NET環境為調用GC提供了專門的對象——GC. 為了使 系統執行GC, 一種簡單的辦法是使用如下語句:

GC.Collect();            

複制

但隻這麼做還不夠. 存儲在堆中的每一個對象都有一個稱為finalizer的方法. finalizer方法是在删除對象之前執行的最後一步. 有關finalizer方法的問題是, 這些方法不 是按照系統方式運作的. 實際上我們甚至完全無法知道對象的finalizer是否執行了, 但是有一點可以肯定的是, 對象被删除之前一定會執行自身的finalizer方法. 是以, 我們添加了一行代碼來告訴程式等待堆上所有對象的finalizer方法都運作後再繼續:

GC.WaitForPendingFinalizers( );

GC.WaitForPendingFinalizers( );            

複制

已經解決了計時過程中發生GC的問題, 還剩下一個問題——采用正确的線程(thread). 在. NET環境中, 程式 運作在程序(process)中, 也被叫做應用程式域(applicationdomain). 它允許作業系統在同一時間内分開運作每個不同的程式. 在程序内, 全部或部分程式是線上程内運作的. 作業系統通過線程來配置設定程式的執 行時間. 在用時間測試程式代碼時, 需要確定時間測試的代碼就在為自身程式配置設定 的程序中運作, 而不是被作業系統執行的其他任務的程序中.

在. NET架構下通過使用Process類可以做到這一點. 通過Process類的多種方法, 可以擷取正在運作目前程式的程序、擷取正在運作目前程式的線程, 以及擷取線程此時的執行時間. 以上方法可以合并成一個調用. 此調用會把它的傳回值指派給一個變量(TimeSpan對象)用來存儲開始時間. 如下列代碼所示(沒錯, 就是兩行代碼):

//将用于存儲指定線程的開始時間
TimeSpan startingTime; 
startingTime = Process.GetCurrentProcess().Threads[0].UserProcessorTime;            

複制

剩下要做的就是在進行時間測試的代碼段停止時記錄代碼執行耗費的時間. 做法如下:

duration = Process.GetCurrentProcess().Threads[0].UserProcessorTime.Subtract(startingTime);            

複制

現在根據上述資訊, 建立基于線程的時間測試的完整代碼如下:

using System; 
using System.Diagnostics; 

class chapter1 
{ 
    static void Main() 
    { 
        int[] nums = new int[100000]; 
        BuildArray(nums); //原文沒有, 我加的為了對比兩種方法差異
        DateTime startTime;//原文沒有, 我加的為了對比兩種方法差異
        TimeSpan endTime;//原文沒有, 我加的為了對比兩種方法差異
        startTime = DateTime.Now;
        TimeSpan duration; 
        DisplayNums(nums); 
        DisplayNums(nums); 
        DisplayNums(nums); 
        duration = Process.GetCurrentProcess().TotalProcessorTime;  
        endTime = DateTime.Now.Subtract(startTime);//原文沒有, 我加的為了對比兩種方法差異
        Console.WriteLine("普通計時: " + endTime.TotalSeconds);//原文沒有, 我加的為了對比兩種方法差異
        Console.WriteLine("線程計時: " + duration.TotalSeconds);        
        Console.ReadLine();//原文沒有, 我加的, 不然控制台自動關閉無法觀察結果
    } 

    static void BuildArray(int[] arr) 
    { 
        for (int i = 0; i <= 99999; i++) 
            arr[i] = i; 
    } 

    static void DisplayNums(int[] arr) 
    { 
        for (int i = 0; i <= arr.GetUpperBound(0); i++) 
            Console.Write(arr[i] + " "); 
    } 
}            

複制

采用新改進的時間測試代碼後, 程式的傳回值為0. 2526. 把此數值與先前第一版時間測試 代碼傳回的将近5秒的數值進行比較. 很明顯, 這兩種時間測試方法之間存在顯著差異. 因而. NET環境中的時間測試代碼應該使用. NET方法來做.

(校對補充:我win10下測試結果如下, 代碼與上方一緻, 不知道為什麼差異那麼大。)

C#中的群集, 泛型和計時類

TimingTest類

雖然不需要一個類來運作時間測試代碼, 但是把代碼作為類來重寫是有意義的, 主要原因是 如果能夠減少測試的代碼行數量, 就能保證代碼的清晰.

Timing類需要下列資料成員:

• startingTime——用來存儲正在測試的代碼的開始時間.

• duration——用來存儲正在測試的代碼的終止時間.

straingTime和duration這兩個成員用來存儲時間, 資料類型是TimeSpan. 在構造方法中把這兩個屬性代表的時間都設定為0. Timing類代碼如下:

//如果過你建立單獨的類檔案,需要這倆命名空間, 原文未寫
using System;
using System.Diagnostics;

//用于進行基于線程的代碼執行計時
public class Timing 
{ 
    //記錄開始時間
    TimeSpan startingTime; 
    //記錄經過時間
    TimeSpan duration; 
    public Timing() 
    { 
        startingTime = new TimeSpan(0); 
        duration = new TimeSpan(0); 
    } 

    public void StopTime() 
    { 
        //獲得指定線程從startingTime開始, 經過的時間
        duration = Process.GetCurrentProcess().Threads[0].UserProcessorTime.Subtract(startingTime); 
    } 

    public void startTime() 
    { 
        //手動調用GC過程, 降低程式運作期間發生GC的可能性
        GC.Collect(); 
        //等待GC結束的信号 再繼續執行代碼
        GC.WaitForPendingFinalizers(); 
        //記錄指定線程的目前時間
        startingTime = Process.GetCurrentProcess().Threads[0].UserProcessorTime; 
    } 
    public TimeSpan Result() 
    { 
        //傳回計時結果
        return duration; 
    } 
}            

複制

讓我們結合Timing類, 改寫之前測試DisplayNums子程式執行時間的代碼, 如下所示:

using System; 
using System.Diagnostics; 
using System.Threading; 

public class Timing 
{ 
    TimeSpan duration; 
    public Timing() 
    { 
        duration = new TimeSpan(0); 
    } 

    public void stopTime() 
    { 
        duration = Process.GetCurrentProcess().TotalProcessorTime; 
    } 

    public void startTime() 
    { 
        GC.Collect(); 
        GC.WaitForPendingFinalizers(); 
    } 

    public TimeSpan Result() 
    { 
        return duration; 
    } 
} 

class chapter1 
{ 
    static void Main() 
    { 
        int[] nums = new int[100000]; 
        BuildArray(nums); 
        Timing tObj = new Timing(); 
        tObj.startTime(); 
        DisplayNums(nums); 
        tObj.stopTime(); 
        Console.WriteLine(".NET計時: " + tObj.Result().TotalSeconds); 
    } 

    static void BuildArray(int[] arr) 
    { 
        for (int i = 0; i < 100000; i++) 
            arr[i] = i; 
    } 

    static void DisplayNums(int[] arr) 
    { 
        for (int i = 0; i <= arr.GetUpperBound(0); i++) 
            Console.Write(arr[i] + " "); 
    } 
}            

複制

結合了Timing類後, 減少了主函數中的幾行代碼, 不過比起精簡代碼更重要的使其是, 降低了主函數的代碼複雜程度. 如果沒有Timing類, 那麼就需要像下面這樣書寫記錄開始時間的代碼:

startTime = Process.GetCurrentProcess( ).Threads[0].UserProcessorTime;            

複制

而如果使用Timing類, 那麼把記錄開始開始時間隻需要調用Timing. startTime方法, 如下所示:

tObj.startTime( );            

複制

通過把冗長的指派語句封裝到類方法内, 可以使得代碼更便于閱讀而且降低出錯的可能。

下面總結下本文章講解的:

經常會用到的三種重要技術進行了回顧與介紹. 盡管不需要編寫整個程式, 但是一些程式的代碼以及要讨論的庫都采用面向對象的方式來編寫. 自行開發的Collection類說明了 許多基本面向對象的概念, 而且這些概念看似貫穿全書. 泛型程式設計允許程式員通過限制需要編寫或重載的方法的數量來簡化幾種資料結構的定義. Timing類提供了簡單有效的方法來 衡量所要學習的資料結構與算法的性能.

• 我們将要編寫的一些程式, 以及教程将要講解的類庫, 都是以面向對象的規範來編寫的. 通過本章中書寫的Collection類, 展示了面向對象程式設計的一些基本概念.

• 泛型程式設計允許程式員通過限制需要方法數量的方式來簡化一些資料結構的定義

• Timing類提供了簡單有效的途徑, 幫助我們衡量接下來要學習的資料結構與算法的性能.

關注蘇州程式大白,持續更新技術分享。謝謝大家支援