C#圖解教程 第二十五章 其他主題


其他主題

概述


在本章中,我會介紹使用C#時的一些重要而又不適合放到其他章節的主題,包括字符串操作、可空類型、Main方法、文檔注釋以及嵌套類型。

字符串


對於內部計算來說0和1很適合,但是對於人類可讀的輸入和輸出,我們需要字符串。BCL提供了很多能讓字符串操作變得更簡單的類。
C#預定義的string類型代表了.NET的System.String類。對於字符串,最需要理解的概念如下。

  • 字符串是Unicode字符串數組
  • 字符串是不可變的(immutable ) 它們不能被修改

string類型有很多有用的字符串操作成員,包括允許我們進行諸如檢測長度、改變大小寫、連接字符串等一些有用的操作。下表列出了其中一些最有用的成員。

從上表中的大多數方法的名字來看,好像它們都會改變字符串對象。其實,它們不會改變字符串而是返回了新的副本。對於一個string,任何“改變”都會分配一個新的恆定字符串。
例如,下面的代碼聲明並初始化了一個叫做s的字符串。第一個WriteLine語句調用了s的ToUpper方法,它返回了字符串中所有字母為大寫形式的副本。最后一行輸出了s的值,可以看到,字符串並沒有改變。

string s = "Hi there.";
Console.WriteLine("{0}",s.ToUpper());    //輸出所有字母為大寫的副本
Console.WriteLine("{0}", s);    //字符串沒有變

筆者自己編碼時,發現上表中很有用的一個方法是Split。它將一個字符串分隔為若干子字符串,並將它們以數組的形式返回。將一組按預定位置分隔字符串的分隔符傳給Split方法,就可以指定如何處理輸出數組中的空元素。當然,原始字符串依然不會改變。
下面的代碼顯示了一個使用Split方法的示例。在這個示例中,分隔符由空字符和4個標點符號組成。

class Program
{
    static void Main()
    {
        string s1="hi there!this,is:a string.";
        char[] delimiters={' ','!',',',':','.'};
        string[] words=s1.Split(delimiters,StringSplitOption.RemoveEmptyEntries);
        Console.WriteLine("Word Count:{0}\n\rThe Words…",words.Length);
        foreach(string s in words)
        {
           Console.WriteLine("    {0}",s); 
        }
    }
}

使用 StringBuilder類


StringBuilder類可以幫助你動態、有效地產生字符串,並且避免創建許多副本。

  • StringBuilder類是BCL的成員,位於System.Text命名空間中
  • StringBuilder對象是Unicode字符的可變數組

例如,下面的代碼聲明並初始化了一個StringBuilder類型的字符串,然后輸出了它的值。第四行代碼通過替換初始字符串的一部分改變了其實際對象。當輸出它的值,隱式調用ToString時,我們可以看到,和string類型的對象不同,StringBuilder對象確實被修改了。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        StringBuilder sb = new StringBuilder( "Hi there.");
        Console.WriteLine( "{0}", sb.ToString());

        sb.Replace( "Hi", "Hello");
        Console.WriteLine( "{0}", sb.ToString());
    }
}


當依據給定的字符串創建了StringBuilder對象之后,類分配了一個比當前字符串長度更長的緩沖區。只要緩沖區能容納對字符串的改變就不會分配新的內存。如果對字符串的改變需要的空間比緩沖區中的可用空間多,就會分配更大的緩沖區,並把字符串復制到其中。和原來的緩沖區一樣,新的緩沖區也有額外的空間。
要獲取StringBuilder對應的字符串內容,我們只需要調用它的ToString方法即可。

把字符串解析為數據值


字符串都是Unicode字符的數組。例如,字符串"25.873"是6個字符而不是一個數字。盡管它看上去像數字,但是我們不能對它使用數學函數。把兩個字符串進行“相加”只會串聯它們。

  • 解析允許我們接受表示值的字符串,並且把它轉換為實際值
  • 所有預定義的簡單類型都有一個叫做Parse的靜態方法,它接受一個表示這個類型的字符串值,並且把它轉換為類型的實際值

以下語句給出了一個使用Parse方法語法的示例。注意,Parse是靜態的,所以我們需要通過目標類型名來調用它。

double dl = double.Parse("25.873");
               ↑             ↑
            目標類型    要轉換的字符串

以下代碼給出了一個把兩個字符串解析為double型值並把它們相加的示例:

static void Main()
{
    string s1 = "25.873";
    string s2 = "36.240";

    double dl = double.Parse(s1);
    double d2 = double.Parse(s2);

    double total = dl + d2;
    Console.WriteLine("Total: {0}", total);
}

這段代碼產生了如下輸出:

關於Parse有一個常見的誤解,由於它是在操作字符串,會被認為是string類的成員。其實不是,Parse根本不是一個方法,而是由目標類型實現的很多個方法。

Parse方法的缺點是如果不能把string成功轉換為目標類型的話會拋出一個異常。異常是昂貴的操作,應該盡可能在編程中避免異常。TryParse方法可以避免這個問題。有關TryParse需要知道的亟要事項如下。

  • 每一個具有Parse方法的內置類型同樣都有一個TryParse方法
  • TryParse方法接受兩個參數並且返回一個bool值
    • 第一個參數是你希望轉換的字符串
    • 第二個是指向目標類型變量的引用的out參數
    • 如果TryParse成功,返回true,否則返回false

如下代碼演示了使用int.TryParse方法的例子:

class Program
{
    static void Main()
    {
        string parseResultSummary;
        string stringFirst = "28";
        int intFirst;
                                     輸入字符串       輸出變置
                                         ↓              ↓
        bool success = int.TryParse( stringFirst, out intFirst );

        parseResultSummary = success
                            ? "was successfully parsed"
                            :"was not successfully parsed";
        Console.WriteLine( "String {0} {1}", stringFirst, parseResultSummary );

        string stringSecond = "vt750";
        int intSecond;          輸入字符串         輸出變最
                                    ↓                ↓
        success = int.TryParse( stringSecond, out intSecond );
        parseResultSummary = success
                            ? "was successfully parsed"
                            :"was not successfully parsed";
        Console.WriteLine( "String {0} {1}", stringSecond, parseResultSummary );
    }
}

關於可空類型的更多內容

在第3章中我們已經介紹過了可空類型。你應該記得,可空類型允許我們創建一個值類型變量並且可以標記為有效或無效,這樣我們就可以有效地把值類型設置為"null"。我本來想在第3章中介紹可空類型及其他內置類型,但是既然現在你對C#有了更深入的了解,現在正是時候介紹其更復雜的方面。
復習一下,可空類型總是基於另外一個叫做基礎類型(underlying type)的已經被聲明的類型。

  • 可以從任何值類型創建可空類型,包括預定義的簡單類型
  • 不能從引用類型或其他可空類型創建可空類型
  • 不能在代碼中顯式聲明可空類型,只能聲明可空類型的變童。之后我們會看到,編譯器會使用泛型隱式地創建可空類型

要創建可空類型的變量,只需要在變量聲明中的基礎類型的名字后面加一個問號。
例如,以下代碼聲明了一個可空int類型的變量。注意,后綴附加到類型名--而不是變量名稱。

  后綴
   ↓
int? myInt=28;

有了這樣的聲明語句,編譯器就會產生可空類型並關聯變量類型。可空類型的結構如下圖所示。

  • 基礎類型的實例
  • 幾個重要的只讀屬性
    • HasValue屬性是bool類型,並且指示值是否有效
    • Value屬性是和基礎類型相同的類型並且返回變最的值--如果變量有效的話


使用可空類型基本與使用其他類型的變量一樣。讀取可空類型的變量返回其值。但是你必須確保變量不是null的,嘗試讀取一個null的變量會產生異常。

  • 跟任何變量一樣,要獲取可空類型變量的值,使用名字即可
  • 要檢測可空類型是否具有值,可以將它和null比較或者檢查它的HasValue屬性
int? myInt1=15;

if(myInt1!=null)
{
    Console.WriteLine("{0}",myInt1);
}

你可以像下面那樣顯式使用兩個只讀屬性。讀取可空類型的變量返回其值。但是你必須確保變量不是null的,嘗試讀取一個null的變量會產生異常。
可空類型和相應的非可空類型之間可輕松實現轉換。有關可空類型轉換的重要事項如下:

  • 非可空類型和相應的可空版本之間的轉換是隱式的,也就是說,不需要強制轉換
  • 可空類型和相應的可空版本之間的轉換是顯式的

例如,下面的代碼行顯示了兩個方向上的轉換。第一行int類型的字面量隱式轉換為int?類型的值,並用於初始化可空類型的變量。第二行,變量顯式轉換為它的非可空版本。

int? myInt1 = 15// 將int隱式轉換為 int?

int regInt = (int) myInt1;    // 將int?顯式轉換為int

為可空類型賦值

可以將以下三種類型的值賦給可空類型的變量:

  • 基礎類型的值
  • 同一可空類型的值
  • Null值

以下代碼分別給出了三種類型賦值的示例:

int? myI1,myI2,myI3

myI1 = 28;            //基礎類型的值
myI2 = myI1;          //可空類型的值
myI3 = null//null

Console.WriteLine("myI1: {0}, myI2: {1}", myI1, myI2);

使用空接合運算符

標准算術運算符和比較運算符同樣也能處理可空類型。還有一個特別的運算符叫做空接合運算符(null coalescing operator),它允許我們在可空類型變量為null時返回一個值給表達式。
空接合運算符由兩個連續的問號組成,它有兩個操作數。

  • 第一個操作數是可空類型的變量
  • 第二個是相同基礎類型的不可空值
  • 在運行時,如果第一個操作數運算后為null,那么第二個操作數就會被返回作為運算結果
int? myI4 = null;
                                空接合運算符
                                    ↓
Console.WriteLine("myI4: {0}", myI4 ?? -l);

myI4 = 10;
Console.WriteLine("myI4: {0}", myI4 ?? -1);


如果你比較兩個相同可空類型的值,並且都設置為null,那么相等比較運算符會認為它們是相等的(==和!=)。
例如,在下面的代碼中,兩個可空的int被設置為null,相等比較運算符會聲 明它們是相等的。

int? i1 = null,i2 = null;    //都為空

if (i1 == i2)    //返回true
{
    Console.WriteLine("Equal");
}

使用可空用戶自定義類型

至此,我們已經看到了預定義的簡單類型的可空形式。我們還可以創建用戶自定義值類型的可空形式。這就引出了在使用簡單類型時沒有遇到的其他問題。
主要問題是訪問封裝的基礎類型的成員。一個可空類型不直接暴露基礎類型的任何成員。例如,來看看下面的代碼和下圖中它的表示形式。代碼聲明了一個叫做MyStruct的結構(值類型),它有兩個公共字段。

  • 由於結構的字段是公共的,所以它可以被結構的任何實例所訪問到,如圖左部分所示。
  • 然而,結構的可空形式只通過Value屬件暴露基礎類型,它不直接暴露它的任何成員。盡管這些成員對結構來說是公共的,但是它們對可空類型來說不是公共的,如圖右部分所示
struct MyStruct
{
    public int X;
    public int Y;
    public MyStruct(int xVal,int yVal)
    {
        X=xVal;
        Y=yVal;
    }
}
class Program
{
    static void Main()
    {
        MyStruct? mSNull=new MyStruct(5,10);
        …
    }
}


例如,以下代碼使用之前聲明的結構並創建了結構和它對應的可空類型的變量。在代碼的第三行和第四行中,我們直接讀取結構變量的值。在第五行和第六行中,就必須從可空類型的Value屬性返回的值中進行讀取。

MyStruct mSStruct=new MyStruct(6,11);
MyStruct? mSNull=new MyStruct(5,10);

Console.WriteLine("mSStruct.X: {0}",mSStruct.X);
Console.WriteLine("mSStruct.Y: {0}",mSStruct.Y);

Console.WriteLine("mSNull.X: {0}",mSNull.Value.X);
Console.WriteLine("mSNull.Y: {0}",mSNull.Value.Y);

Nullable<T>
可空類型通過一個叫做System.Nullable<T>的.NET類型來實現,它使用了C#的泛型特性。C#可空類型的問號語法是創建Nullable<T>類型變量的快捷語法,在這里T是基礎類型。Nullable<T>接受了基礎類型並把它嵌入結構中,同時給結構提供可空類型的屬性、方法和構造函數。
我們可以使用Nullable<T>這種泛型語法,也可以使用C#的快捷語法。快捷語法更容易書寫和理解,並且也減少了出錯的可能性。以下代碼使用Nullable<T>語法為之前示例中聲明的 MyStruct 結構創建一個叫做mSNull的Nullable<MyStruct>類型。

Nullable<MyStruct> mSNull = new Nullable<MyStruct>();

下面的代碼使用了問號語法,完全等同於Nullable<T>語法:

MyStruc? mSNull=new MyStruct();

Main 方法


每一個C#程序都必須有一個入口點--一個必須叫做Main的方法。
在貫穿本書的示例代碼中,都使用了一個不接受參數並且也不返回值的Main方法。然而,一共有4種形式的Main可以作為程序的入口點。這些形式如下:

  • static void Main {…}
  • static void Main(string[] args) {…}
  • static int Main() {…}
  • static int Main(string[] args) {…}

前面兩種形式在程序終止后都不返回值給執行環境。后面兩種形式則返回int值。如果使用返回值,通常用於報告程序的成功或失敗,0通常用於表示成功。
第二種和第四種形式允許我們在程序啟動時從命令行向程序傳入實參,也叫做參數。命令行參數的一些重要特性如下。

  • 可以有0個或多個命令行參數。即使沒有參數,args參數也不會是null,而是一個沒有元素的數組
  • 參數由空格或制表符隔開
  • 每一個參數都被程序解釋為是字符串,但是你無須在命令行中為參數加上引號

例如,下面叫做CommandLineArgs的程序接受了命令行參數並打印了每一個提供的參數:

class Program
{
    static void Main(string[] args)
    {
        foreach (string s in args)
        {
            Console.WriteLine(s);
        }
    }
}

如下命令行使用5個參數執行CommandLineArgs程序。

CommandLineArgs Jon Peter Beth Julia Tammi
      ↑                  ↑
 可執行程序名             參數

前面的程序和命令行產生了如下的輸出:

其他需要了解的有關Main的重要事項如下。

  • Main必須總是聲明為static
  • Main可以被聲明為類或結構

一個程序只可以包含Main的4種可用入口點形式中的一種聲明。當然,如果你聲明其他方法的名稱為Main,只要它們不是4種入口點形式中的一種就是合法--但是,這樣做是非常容易混淆的。
Main的可訪問性
Main可以被聲明為public或private。

  • 如果Main被聲明為private,其他程序集就不能訪問它,只有執行環境才能啟動程序
  • 如果Main被聲明為public,其他程序集就可以調用它

然而,無論Main聲明的訪問級或所屬類或結構的訪問級別是什么,執行環境總是能訪問Main。
默認情況下,當Visual Studio創建了一個項目時,它就創建了一個程序框,其中的Main是隱式private。如果需要,你隨時可以添加public修飾符。

文檔注釋


文檔注釋特性允許我們以XML元素的形式在程序中包含文檔(第19章介紹XML)。Visual Studio會幫助我們插入元素,以及從源文件中讀取它們並復制到獨立的XML文件中。
下圖給出了一個使用XML注釋的概要。這包括如下步驟。

  • 你可以使用Visual Studio來產生帶有嵌人了XML的源文件。Visual Studio會自動插入大多數重要的XML元素
  • Visual Studio從源文件中讀取XML並且復制XML代碼到新的文件
  • 另外一個叫做文檔編譯器的程序可以獲取XML文件並且從它產生各種類型的文件


之前的Visual Studio版本包含了基本的文檔編譯器,但是它在Visual Studio 2005發布之前被刪除了。微軟公司正在開發一個叫做Sandcastle的新文檔編譯器,它已經被用來生成.NET框架的文檔。從http://sandcastle.codeplex.com 可更詳細地了解以及免費下載這個軟件。

插入文檔注釋

文檔注釋從3個連續的正斜杠開始。

  • 前兩個斜杠指示編譯器這是一行注釋,並且需要從程序的解析中忽略
  • 第三個斜杠指示這是一個文檔注釋

例如,以下代碼中前4行就是有關類定義的文檔注釋。這里使用<summary>XML標簽。在字段聲明之上有3行來說明這個字段--還是使用<summary>標簽。

///<summary>    ← 類的開始XML標簽
/// This is class MyClass, which does the following wonderful things, using
/// the following algorithm. …Besides those, it does these additional
/// amazing things.
///</summary>    ← 關閉 XML 標簽
class MyClass
{
    ///<summary>
    /// Field1 is used to hold the value of …
    ///</summary>
    public int Field1 = 10;
    …
}

每一個XML元素都是當我們在語言特性(比如類或類成員)的聲明上輸入3條斜杠時,ViSual Studio 自動增加的。
例如,從下面的代碼可以看到,在MyClass類聲明之上的2條斜杠:

//
class MyClass
{…}

只要我們增加了第三條斜杠,Visual Studio會立即擴展注釋為下面的代碼,而我們無須做任何事情。然后我們就可以在標簽之間輸入任何希望注釋的行了。

/// <summary>    自動插入
///              自動插入
/// </summary>   自動插入
class MyClass
{…}

使用其他XML標簽

在之前的示例中,我們看到了summay XML標簽的使用。C#可識別的標簽還有很多。下表列出了最重要的一些。

嵌套類型


我們通常直接在命名空間中聲明類型。然而,我們還可以在類或結構中聲明類型。

  • 在另一個類型聲明中聲明的類型叫做嵌套類型。和所有類型聲明一樣,嵌套類型是類型實例的模板
  • 嵌套類型像封閉類型(enclosing type)的成員一樣聲明
  • 嵌套類型可以是任意類型
  • 嵌套類型可以是類或結構

例如,以下代碼顯示了MyClass類,其中有一個叫做MyCounter的嵌套類。

class MyClass      //封閉類
{
    class MyCounter//嵌套類
    {…}
    …
}

如果一個類型只是作為幫助方法並且只對封閉類型有意義,可能就需要聲明為嵌套類型了。不要跟嵌套這個術語混淆。嵌套指聲明的位置--而不是任何實例的位置。盡管嵌套類型的聲明在封閉類型的聲明之內,但嵌套類型的對象並不一定封閉在封閉類型的對象之內。嵌套類型的對象(如果創建了的話)和它沒有在另一個類型中聲明時所在的位置一樣。
例如,下圖顯示了前面代碼框架中的MyClass對象和MyCounter對象。另外還顯式了MyClass類中的一個叫做Counter的字段,這就是指向嵌套類型對象的引用,它在堆的另一處。

嵌套類的示例

以下代碼把MyClass和MyCounter完善成了完整的程序。MyCounter實現了一個整數計數器,從0開始並且使用++運算符來遞增。當MyClass的構造函數被調用時,它創建嵌套類的實例並且為字段分配引用,下圖演示了代碼中對象的結構。

class MyClass
{
    class MyCounter
    {
        public int Count{get;private set;}
        public static MyCounter operator ++(MyCounter current)
        {
            current.Count++;
            return current;
        }
    }

    private MyCounter counter;

    public MyClass(){counter=new MyCounter();}

    public int Incr(){return (counter++).Count;}
    public int GetValue(){return counter.Count;}
}
class Program
{
    static void Main()
    {
        var mc=new MyClass();

        mc.Incr();mc.Incr();mc.Incr();
        mc.Incr();mc.Incr();mc.Incr();

        Console.WriteLine("Total:  {0}",mc.GetValue());
    }
}

可見性和嵌套類型

在第7章中,我們已經了解到類和類型通常有public或internal的訪問級別。然而,嵌套類型的不同之處在於,它們有成員訪問級別而不是類型訪問級別。因此,下面的命題是成立的。

  • 在類內部聲明的嵌套類型可以有5種類成員訪問級別中的任何一種:public、protected、private、internal或protected internal
  • 在結構內部聲明的嵌套類型可以有3種結構成員訪問級別中的任何一種:public、internal或private

在這兩種情況下,嵌套類型的默認訪問級別都是private,也就是說不能被封閉類型以外的對象所見。
封閉類和嵌套類的成員之間的關系是很容易理解的,如下圖所示。不管封閉類型的成員聲明了怎樣的訪問級別,包括private和protected,嵌套類型都能訪問這些成員。
然而,它們之間的關系不是對稱的。盡管封閉類型的成員總是可見嵌套類型的聲明並且能創建它的變量及實例,但是它們不能完全訪問嵌套類型的成員。相反,這種訪問權限受限於嵌套類成員聲明的訪問級別--就好像嵌套類型是一個獨立的類型一樣。也就是說,它們可以訪問public或internal的成員,但是不能訪問嵌套類型的private或protected成員。

我們可以把這種關系總結如下。

  • 嵌套類型的成員對封閉類型的成員總是有完全訪問權限
  • 封閉類型的成員
    • 總是可以訪問嵌套類型本身
    • 只能訪問聲明了有訪問權限的嵌套類型成員

嵌套類型的可見性還會影響基類成員的繼承。如果封閉類型是一個派生類,嵌套類型就可以通過使用相同的名字來隱藏基類成員。可以在嵌套類型的聲明上使用new修飾符來顯式隱藏。
嵌套類型中的this引用指的是嵌套類型的對象--不是封閉類型的對象。如果嵌套類型的對象需要訪問封閉類型,它必須持有封閉類型的引用。如以下代碼所示,我們可以把封閉對象提供的this引用作為參數傳給嵌套類型的構造函數:

class SomeClass                      //封閉類
{
    int Field1=15,Field2=20;         //封閉類的字段
    MyNested mn=null;                //嵌套類的引用

    public void PrintMyMembers()
    {
        mn.PrintOuterMembers();      //調用嵌套類中的方法
    }

    public SomeClass()               //構造函數
    {
        mn=new MyNested(this);       //創建嵌套類的實例
    }

    class MyNested                   //嵌套類聲明
    {
        SomeClass sc=null;           //封閉類的引用

        public MyNested(SomeClass SC)//嵌套類構造函數
        {
            sc=SC;                   //存儲嵌套類的引用
        }

        public void PrintOuterMembers()
        {
            Console.WriteLine("Field1: {0}",sc.Field1);//封閉字段
            Console.WriteLine("Field2: {0}",sc.Field2);//封閉字段
        }
    }                                 //嵌套類結束
}
class Program
{
    static void Main()
    {
        var MySC=new SomeClass();
        MySC.PrintOuterMembers();
    }
}

析構函數和dispose模式


第6章介紹了創建類對象的構造函數。類還可以擁有析構函數(destructor),它可以在一個類的實例不再被引用的時候執行一些操作,以清除或釋放非托管資源。非托管資源是指類似用Win32 API或非托管內存塊獲取的文件句柄這樣的資源。使用.NET資源是無法獲取它們的,因此如果我們只用.NET類,是不需要編寫太多析構函數的。
關於析構函數要注意以下幾點。

  • 每個類只能有一個析構函數
  • 析構函數不能有參數
  • 析構函數不能有訪問修飾符
  • 析構函數名稱與類名相同,但要在前面加一個波浪符
  • 析構函數只能作用於類的實例。因此沒有靜態析構函數
  • 不能在代碼中顯式調用析構函教。相反,當垃圾同收器分析代碼並認為代碼中不存在指向該對象的可能路徑時,系統會在垃圾回收過程中調用析構函數

例如,下面的代碼通過類Class1演示了析構函數的語法:

Class1
{
    ~Class1()
    {
        CleanupCode
    }
    …
}

使用析構函數時一些重要的原則如下:

  • 不要在不需要時實現析構函數,這會嚴重影響性能
  • 析構函數應該只釋放對象擁有的外部資源
  • 析構函數不應該訪問其他對象,因為無法認定這些對象是否已經被銷毀

在C#3.0發布之前,析構函數有時也叫終結器(finalizer)。你可能會經常在文本或.NET API方法名中遇到這個術語。

標准dispose模式

與C++析構函數不同,C#析構函數不會在實例超出作用域時立即調用。事實上,你無法知道何時會調用析構函數。此外,如前所述,你也不能顯式調用析構函數。你所能知道的只是,系統會在對象從托管堆上移除之前的某個時刻調用析構函數。
如果你的代碼中包含的非托管資源越快釋放越好,就不能將這個任務留給析構函數,因為無法保證它會何時執行。相反,你應該采用標准dispose模式。
標准dispose模式包含以下特點。

  • 包含非托管資源的類應該實現IDisposable接口,后者包含單一方法Dispose。Dispose包含釋放資源的清除代碼
  • 如果代碼使用完了這些資源並且希望將它們釋放,應該在程序代碼中調用Dispose方法。注意,這是在你的代碼中(不是系統中)調用Dispose
  • 你的類還應該實現一個析構函數,在其中調用Dispose方法,以防止之前沒有調用該方法。

可能會有點混亂,所以我們再總結一下。你想將所有清除代碼放到Dispose方法中,並在使用完資源時調用。以防萬一Dispose沒有調用,類的析構函數也應該調用Dispose。而另一方面如果調用了Dispose,你就希望通知垃圾回收器不要再調用析構函數,因為已經由Dispose執行了清除操作。析構函數和Dispose代碼應該遵循以下原則。

  • 析構函數和Dispose方法的邏輯應該是,如果由於某種原因代碼沒有調用Dispose,那么析構函數應該調用它,並釋放資源
  • 在Dispose方法的最后應該調用GC.SuppressFinalize方法,通知CLR不要調用該對象的析構函數,因為清除工作已經完成
  • 在Dispose中實現這些代碼,這樣多次調用該方法是安全的。也就是說代碼要這樣寫:如果該方法已經被調用,那么任何后續調用都不會執行額外的工作,也不會拋出任何異常

下面的代碼展示了標准的dispose模式,下圖對其進行了闡釋。這段代碼的要點如下:

  • Dispose方法有兩個重載:一個是public的,一個是protected的。protected的重載包含實際的清除代碼
  • public版本可以在代碼中顯式調用以執行清除工作。它會調用protected版本
  • 析構函數調用protected版本
  • protected版本的bool參數通知方法是被析構函數或是其他代碼調用。這一點很重要,因為結果不同所執行的操作會略有不同。細節如下面的代碼所示

比較構造函數和析構函數

下表對何時調用構造函數和析構函數進行了總結和比較。

和COM的互操作


盡管本書不介紹COM編程,但是C#4.0專門增加了幾個語法改變,使得COM編程更容易。其中的一個改變叫做“省略ref”特性,允許不需要使用方法返回值的情況下,無需ref關鍵字即可調用COM方法。
例如,如果程序所在的機器上安裝了微軟Word,你就可以在自己的程序中使用Word的拼寫檢査功能。這個方法是 Microsoft.Office.Tools.Word 命名空間的Document類中的CheckSpelling方法。這個方法有12個參數,且都是ref參數。也就是說,之前即使你不需要為方法傳入數據或是從方法取回數據,也只能為每一個參數提供一個引用變量。省略ref關鍵字只能用於COM方法, 否則就仍然會收到編譯錯誤。
代碼差不多應該如下,對於這段代碼注意幾點。

  • 我只使用第二個和第三個參數,都是布爾型。但是我不得不創建兩個變量,object類型的ignoreCase和alwaysSuggest來保存值,因為方法需要ref參數
  • 我創建了叫做optional的object變量用於其他10個參數
object ignoreCase=true;
object alwaysSuggest=false;
object optional=Missing.Value;
tempDoc.CheckSpelling(ref optional,ref ignoreCase,ref alwaysSuggest,
    ref optional,ref optional,ref optional,ref optional,ref optional,
    ref optional,ref optional,ref optional,ref optional);

有了“省略ref”特性,我們的代碼就干凈多了,因為對於不需要輸出的參數,我們不再需要使用ref關鍵字,只需要為我們關心的兩個參數使用內聯的bool。簡化后的代碼如下:

object optional=Missing.Value;
tempDoc.CheckSpelling(optional,true,false,
    optional,optional,optional,optional,optional,
    optional,optional,optional,optional);

除了“省略ref”特性,對於可選的參數我們可以使用C#4.0的可選參數特性,比之前的又簡單很多,如下所示:

tempDoc.CheckSpelling( Missing.Value, true, false );

如下代碼是一個包含這個方法的完整程序。要編譯這段代碼,你需要在本機上安裝 Visual Studio Tools for Office(VSTO)並且必須為項目添加 Microsoft.Office.Interop.Word 程序集的引用。要運行這段編譯的代碼,必須在本機上安裝 Microsoft Word。

using System;
using System.Reflection;
using Microsoft.Office.Interop.Word;

class Program
{
    static void Main()
    {
        Console.WriteLine("Enter a string to spell-check");
        string stringToSpellCheck=Console.ReadLine();

        string spellingResults;
        int errors=0;
        if(stringToSpellCheck.Length==0)
        {
            spellingResults="No string to check";
        }
        else
        {
            Microsoft.Office.Interop.Word.Application app=
                new Microsoft.Office.Interop.Word.Application();

            Console.WriteLine("\nChecking the string for misspellings …");
            app.Visible=false;

            Microsoft.Office.Interop.Word._Document tempDoc=app.Document.Add();

            tempDoc.Words.First.InsertBefore(stringToSpellCheck);
            Microsoft.Office.Interop.Word.ProofreadingErrors spellErrorsColl=
                tempDoc.SpellingErrors;
            errors=spellErrorsColl.Count;

            // 1.不是用可選參數
            // object ignoreCase=true;
            // object alwaysSuggest=false;
            // object optional=Missing.Value;
            // tempDoc.CheckSpelling(ref optional,ref ignoreCase,ref alwaysSuggest,
            //     ref optional,ref optional,ref optional,ref optional,ref optional,
            //     ref optional,ref optional,ref optional,ref optional);

            // 2.使用C#4.0的“省略ref”特性
            object optional=Missing.Value;
            tempDoc.CheckSpelling(optional,true,false,
                optional,optional,optional,optional,optional,
                optional,optional,optional,optional);

            //3.使用“省略ref”和可選參數特性
            app.Quit(false);
            spellingResults=errors+" errors found";
        }

        Console.WriteLine(spellingResults);
        Console.WriteLine("\nPress <Enter> to exit program");
        Console.WriteLine();
    }
}

如果你運行這段代碼,會得到如圖25-8所示的一個控制台窗口,它會要求你輸入希望進行拼寫檢査的字符串。在收到宇符串之后它會打開Word然后運行拼寫檢査。此時,你會看到出現了一個Word的拼寫檢査窗口,如圖25-9所示。

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM