程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#創建常量、原子性的值類型

C#創建常量、原子性的值類型

編輯:C#入門知識

類型設計談起
從Class到Struct
假如我們要設計一個存儲收信人地址的類型(Type), 我們管這個類型叫 Address。它應該包含這樣幾個屬性:

Province   省
City       市
Zip        郵編

要求對Zip的格式進行控制(必須全為數字,且為6位),大家該如何設計呢?我想很多人會寫成這樣吧:

public class Address {
    private string province;
    private string city;
    private string zip;

    public string Province {
       get { return province; }
       set { province = value; }
    }

    public string City {
       get { return city; }
       set { city = value; }
    }

    public string Zip {
       get { return zip; }
       set {
           CheckZip(value);  // 驗證格式
           zip = value;
       }
    }

    // 檢測是不是正確的 zip
    private void CheckZip(string value) {
       string pattern = @"d{6}";
       if(!Regex.IsMatch(value, pattern))
           throw new Exception("Zip is invalid! ");
    }
    public override string ToString() {
       return String.Format("Province: {0}, City: {1}, Zip: {2}", province, city, zip);
    }
}

這裡已經存在第一個問題:當我們聲明一個類時,更多的是定義一系列相關的操作(或者叫行為、方法),當然類中也會包含字段和屬性,但這些字段通常都是為類的方法所使用,而屬性則常用於表示類的狀態(比如StringBuilder的Length),類的能力(比如StringBuilder的 Capacity),方法進行的狀態或者階段。而定義一個結構時,我們通常僅僅是用它來保存數據,而不提供方法,或者是僅提供對其自身進行操作或者轉換的方法,而非對其它類型提供服務的方法。

Address 不包含任何的方法,它僅僅是將Provice、City、Zip這樣的三個數據組織起來成為一個獨立的個體,所以最好將其聲明為一個Struct而非是一個Class。(這裡也有例外的情況:如果Address包含二十個或者更多的字段,則考慮將其聲明為Class,因為Class在參數傳遞時是傳引用,而Struct是傳值。在數據較小的情況下,傳值的效率更高一些;而在數據較大的時候,傳引用占據更小的內存空間。)

所以我們首先可以將Address聲明為一個Struct而非Class。

數據不一致的問題
我們接下來使用一下剛剛創建的Address類型:

Address a = new Address();
a.Province = "陝西";
a.City = "西安";
a.Zip = "710068";
Console.WriteLine(a.ToString()); // Province: 陝西, City: 西安, Zip: 710068

看上去是沒有問題的,但是回想下類型的定義,在給Zip屬性賦值時是有可能拋出異常的,所以我們還是把它放在一個Try Catch語句中,同時我們給Zip賦一個錯誤的值,看會發生什麼:

try {
    a.City = "青島";
    a.Zip = "12345";      // 這裡觸發異常
    a.Province = "山東";
} catch {
}
Console.WriteLine(a.ToString());//Province: 陝西, City: 青島, Zip: 710068

結果是出現了數據不一致的問題,當為Zip賦值的時候,因為引發了異常,所以對Zip以及其後的Province的賦值都失敗了,但是對City的賦值是成功的。結果就是出現了Provice是陝西,City卻是青島這種情況。

即是在賦值Zip時沒有引發異常,也會出現問題:在多線程情況下,當當前線程執行到修改了 City為“青島”,但還沒有修改 Zip 和 Province的時候(Zip仍為 “710068”、Province仍為“陝西”)。如果此時其他線程訪問類型實例a,那麼也將會讀取到不一致的數據。

常量性和原子性
我們現在已經知道了上面存在的問題,那麼接下來該如何改進呢?我們先來看看作者對常量性和原子性給的定義:

對象的原子性:對象的狀態是一個整體,如果一個字段改變,其他字段也要同時做出相應改變。簡單來說,就是要麼不改,要麼全改。
對象的常量性:對象的狀態一旦確定,就不能再次更改了。如果想再次更改,需要重新構造一個對象。
我們已經知道了對象的原子性和常量性這兩個概念,那麼接下來該如何去實施呢?對於原子性,我們實施的辦法是添加一個構造函數,在這個構造函數中為對象的所有字段賦值。而為了實施常量性,我們不允許在為對象賦值以後還能對對象狀態進行修改,所以我們將屬性中的set訪問器刪除掉,同時將字段聲明為readonly:

public struct Address {
    private readonly string province;
    private readonly string city;
    private readonly string zip;

    public Address(string province, string city, string zip) {
       this.city = city;           
       this.province = province;
       this.zip = zip;
    CheckZip(zip);     // 驗證格式
    }

    public string Province {
       get { return province; }
    }

    public string City {
       get { return city; }
    }

    public string Zip {
       get { return zip; }
    }
    // 其余略 ...
}

這樣,我們對Address對象的創建,將所有字段的賦值都在構造函數中作為一個整體來進行;而當我們需要改變單個字段的值時,也需要重新創建對象再賦值。我們看下下面的測試:

Address a = new Address("陝西", "西安", "710068");

try {
    a = new Address("青島", "山東", "22233");// 發生異常,對a重新賦值失敗,但狀態保持一致
} catch {
}

Console.WriteLine(a.ToString()); // 輸出:Province: 陝西, City: 西安, Zip: 710068

避免外部類型對類型內部的訪問
上面的方法解決了數據不一致的問題,但是還漏掉了一點:當類型內部維護著一個引用類型字段,比如說數組。盡管我們將它聲明為了readonly,類型外部還是可以對它進行訪問(如果你不清楚值類型和引用類型的區別,請參考 C#類型基礎)。現在我們修改Address 類,添加一個數組phones,存儲電話號碼:

private readonly string[] phones;

public Address(string province, string city, string zip, string[] phones) {  
    // 略...
    this.phones = phones;
}

public string[] Phones {
    get { return phones; }
}

我們接下來做個測試:

string[] phones = { "029-88401100", "029-88500321" };
Address a = new Address("陝西", "西安", "710068", phones);

Console.WriteLine(a.Phones[0]);     // 輸出: 029-88401100

string[] b = a.Phones;
b[0] = "029-XXXXXXXX";       // 通過b修改了 Address的內容

Console.WriteLine(a.Phones[0]); // 輸出: 029-XXXXXXXX

可以看到,盡管 phones字段聲明為了readonly,並且也只提供了get屬性訪問器。我們仍然可以通過 Address對象a外部的變量b,修改了a對象內部的內容。如何避免這種情況的發生呢?我們可以通過深度復制的方式來解決,在Phones的get屬性訪問器中添加如下代碼:

public string[] Phones {
    get {
      

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved