程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#銳利體驗(七)

C#銳利體驗(七)

編輯:關於C#

第七講 域與屬性

域(Field)又稱成員變量(Member Variable),它表示存儲位置,是C#中類不可缺少的一部分。域的類型可以是C#中任何數據類型。但對於除去string類型的其他引用類型由於在初始化時涉及到一些類的構造器的操作,我們這裡將不提及,我們把這一部分內容作為“類的嵌套”放在“接口 繼承與多態”一講內來闡述。

域分為實例域和靜態域。實例域屬於具體的對象,為特定的對象所專有。靜態域屬於類,為所有對象所共用。C#嚴格規定實例域只能通過對象來獲取,靜態域只能通過類來獲取。例如我們有一個類型為MyClass的對象MyObject,MyClass內的實例域instanceField(存取限制為public)只能這樣獲取:MyObject. instanceField。而MyClass的靜態域staticField(存取限制為public)只能這樣獲取:MyClass.staticField。注意靜態域不能像傳統C++那樣通過對象獲取,也就是說MyObject.staticField的用法是錯誤的,不能通過編譯器編譯。

域的存取限制集中體現了面向對象編程的封裝原則。如前所述,C#中的存取限制修飾符有5種,這5種對域都適用。C#只是用internal擴展了C++原來的friend修飾符。在有必要使兩個類的某些域互相可見時,我們將這些類的域聲明為internal,然後將它們放在一個組合體內編譯即可。如果需要對它們的繼承子類也可見的話,聲明為protected internal即可。實際上這也是組合體的本來意思--將邏輯相關的類組合封裝在一起。

C#引入了readonly修飾符來表示只讀域,const來表示不變常量。顧名思義對只讀域不能進行寫操作,不變常量不能被修改,這兩者到底有什麼區別呢?只讀域只能在初始化--聲明初始化或構造器初始化--的過程中賦值,其他地方不能進行對只讀域的賦值操作,否則編譯器會報錯。只讀域可以是實例域也可以是靜態域。只讀域的類型可以是C#語言的任何類型。但const修飾的常量必須在聲明的同時賦值,而且要求編譯器能夠在編譯時期計算出這個確定的值。const修飾的常量為靜態變量,不能夠為對象所獲取。const修飾的值的類型也有限制,它只能為下列類型之一(或能夠轉換為下列類型的):sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string, enum類型, 或引用類型。值得注意的是這裡的引用類型,由於除去string類型外,所有的類型出去null值以外在編譯時期都不能由編譯器計算出他們的確切的值,所以我們能夠聲明為const的引用類型只能為string或值為null的其他引用類型。顯然當我們聲明一個null的常量時,我們已經失去了聲明的意義--這也可以說是C#設計的尴尬之處!

這就是說,當我們需要一個const的常量時,但它的類型又限制了它不能在編譯時期被計算出確定的值來,我們可采取將之聲明為static readonly來解決。但兩者之間還是有一點細微的差別的。看下面的兩個不同的文件:

//file1.cs
//csc /t:library file1.cs
using System;
namespace MyNamespace1
{
public class MyClass1
{
        public static readonly int myField = 10;
    }
}
//file2.cs
//csc /r:file1.dll file2.cs
using System;
namespace MyNamespace2
{
public class MyClass1
{
        public static void Main()
        {
Console.WriteLine(MyNamespace1.MyClass1.myField);
        }
    }
}

我們的兩個類分屬於兩個文件file1.cs 和file2.cs,並分開編譯。在文件file1.cs內的域myField聲明為static readonly時,如果我們由於某種需要改變了myField的值為20,我們只需重新編譯文件file1.cs為file1.dll,在執行file2.exe時我們會得到20。但如果我們將static readonly改變為const後,再改變myField的初始化值時,我們必須重新編譯所有引用到file1.dll的文件,否則我們引用的MyNamespace1.MyClass1.myField將不會如我們所願而改變。這在大的系統開發過程中尤其需要注意。實際上,如果我們能夠理解const修飾的常量是在編譯時便被計算出確定的值,並代換到引用該常量的每一個地方,而readonly時在運行時才確定的量--只是在初始化後我們不希望它的值再改變,我們便能理解C#設計者們的良苦用心,我們才能徹底把握const和readonly的行為!

域的初始化是面向對象編程中一個需要特別注意的問題。C#編譯器缺省將每一個域初始化為它的默認值。簡單的說,數值類型(枚舉類型)的默認值為0或0.0。字符類型的默認值為'\x0000'。布爾類型的默認值為false。引用類型的默認值為null。結構類型的默認值為其內的所有類型都取其相應的默認值。雖然C#編譯器為每個類型都設置了默認類型,但作為面向對象的設計原則,我們還是需要對變量進行正確的初始化。實際上這也是C#推薦的做法,沒有對域進行初始化會導致編譯器發出警告信息。C#中對域進行初始化有兩個地方--聲明的同時進行初始化和在構造器內進行初始化。如前所述,域的聲明初始化實際上被編譯器作為賦值語句放在了構造器的內部的最開始處執行。實例變量初始化會被放在實例構造器內,靜態變量初始化會被放在靜態構造器內。如果我們聲明了一個靜態的變量並同時對之進行了初始化,那麼編譯器將為我們構造出一個靜態構造器來把這個初始化語句變成賦值語句放在裡面。而作為const修飾的常量域,從嚴格意義上講不能算作初始化語句,我們可以將它看作類似於C++中的宏代換。

屬性

屬性可以說是C#語言的一個創新。當然你也可以說不是。不是的原因是它背後的實現實際上還是兩個函數--一個賦值函數(get),一個取值函數(set),這從它生成的中間語言代碼可以清晰地看到。是的原因是它的的確確在語言層面實現了面向對象編程一直以來對“屬性”這一OO風格的類的特殊接口的訴求。理解屬性的設計初衷是我們用好屬性這一工具的根本。C#不提倡將域的保護級別設為public而使用戶在類外任意操作--那樣太不OO,或者具體點說太不安全!對所有有必要在類外可見的域,C#推薦采用屬性來表達。屬性不表示存儲位置,這是屬性和域的根本性的區別。下面是一個典型的屬性設計:

using System;
class MyClass
{
    int integer;
    public int Integer
    {
        get {return integer;}
        set {integer=value;}
    }
}
class Test
{
    public static void Main()
    {
        MyClass MyObject=new MyClass();
        Console.Write(MyObject.Integer);
        MyObject.Integer++;
        Console.Write(MyObject.Integer);
    }
}

一如我們期待的那樣,程序輸出0 1。我們可以看到屬性通過對方法的包裝向程序員提供了一個友好的域成員的存取界面。這裡的value是C#的關鍵字,是我們進行屬性操作時的set的隱含參數,也就是我們在執行屬性寫操作時的右值。

屬性提供了只讀(get),只寫(set),讀寫(get和 set)三種接口操作。對域的這三種操作,我們必須在同一個屬性名下聲明,而不可以將它們分離,看下面的實現:

class MyClass
{
    private string name;
public string Name
    {
        get { return name; }
    }
    public string Name
    {
        set { name = value; }
    }
}

上面這種分離Name屬性實現的方法是錯誤的!我們應該像前面的例子一樣將他們放在一起。值得注意的是三種屬性(只讀,只寫,讀寫)被C#認為是同一個屬性名,看下面的例子:

class MyClass
{
    protected int num=0;
    public int Num
    {
        set
        {
            num=value;
        }
    }
}
class MyClassDerived: MyClass
{
    new public int Num
    {
        get
        {
            return num;
        }
    }
}
class Test
{
    public static void Main()
    {
        MyClassDerived MyObject = new MyClassDerived();
        //MyObject.Num= 1; //錯誤 !
        ((MyClass)MyObject).Num = 1;
    }
}

我們可以看到MyClassDerived中的屬性Num-get{}屏蔽了MyClass中屬性Num-set{}的定義。

當然屬性遠遠不止僅僅限於域的接口操作,屬性的本質還是方法,我們可以根據程序邏輯在屬性的提取或賦值時進行某些檢查,警告等額外操作,看下面的例子:

class MyClass
{
    private string name;
    public string Name
    {
        get { return name; }
        set
        {
            if (value==null)
                name="Microsoft";
            else
                name=value;
        }
    }
}

由於屬性的方法的本質,屬性當然也有方法的種種修飾。屬性也有5種存取修飾符,但屬性的存取修飾往往為public,否則我們也就失去了屬性作為類的公共接口的意義。除了方法的多參數帶來的方法重載等特性屬性不具備外, virtual, sealed, override, abstract等修飾符對屬性與方法同樣的行為,但由於屬性在本質上被實現為兩個方法,它的某些行為需要我們注意。看下面的例子:

abstract class A
{
    int y;
    public virtual int X
    {
        get { return 0; }
    }
    public virtual int Y
    {
        get { return y; }
        set { y = value; }
    }
    public abstract int Z { get; set; }
}
class B: A
{
    int z;
    public override int X
    {
        get { return base.X + 1; }
    }
    public override int Y
    {
        set { base.Y = value < 0? 0: value; }
    }
    public override int Z
    {
        get { return z; }
        set { z = value; }
    }
}

這個例子集中地展示了屬性在繼承上下文中的某些典型行為。這裡,類A由於抽象屬性Z的存在而必須聲明為abstract。子類B中通過base關鍵字來引用父類A的屬性。類B中可以只通過Y-set便覆蓋了類A中的虛屬性。

靜態屬性和靜態方法一樣只能存取類的靜態域變量。我們也可以像做外部方法那樣,聲明外部屬性。

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