首頁 > 軟體

C#類和結構詳解

2022-04-11 13:01:10

類和結構實際上都是建立物件(範例)的模版,每個物件都包含資料,並提供了處理和存取資料的方法。

類定義了類的每個物件可以包含什麼資料和功能。

class PhoneCus

    {

        public const string DaySend = "Mon";

        public int CusId;

    }

結構與類的區別是它們在記憶體中的儲存方式,存取方式和它們的一些特性(稍後詳細介紹它們的區別)。

較小的資料型別使用結構可提高效能,在語法上,比較類似,主要區別是使用關鍵字struct代替class來宣告結構。

struct PhoneCusStruct
    {
        public const string DaySend = "Mon";
        public int CusId=;
    }

對於類和結構,都是用new來宣告範例:這個關鍵字建立物件並對其進行初始化。

PhoneCus myCus = new PhoneCus();
PhoneCusStruct  myCus2 = new PhoneCusStruct();

上面的例子,類和結構的欄位值都預設0.

一.類

類中的資料和函數稱為類的成員(資料成員和函數成員)。

1.資料成員

資料成員是包含類的資料————欄位,常數和事件的成員。資料成員可以是靜態資料。類成員總是範例成員,除非用static顯示宣告。

2.函數成員

函數成員提供了操作類中資料的某些功能,包括方法,屬性,建構函式,終端子,運運算元以及索引。

(1)方法

*C#區分函數和方法。C#中函數包含上述提到的。

*給方法傳遞引數

引數可以通過參照或值傳遞給方法。在變數通過參照傳遞給方法時,被呼叫的方法得到的就是這個變數,準確的説就是指向記憶體中變數的指標。所以在方法內對變數進行的任何改變在方法退出後仍然有效。

而如果變數通過值傳遞給方法,被呼叫的方法得到的是變數的一個相同副本,也就是說,在方法退出後,對變數的修改會丟失。

對於複雜的資料型別,按參照傳遞的效率更高,因為在按值傳遞時,必須複製大量的資料。

注意字串的行為方式有所不同,因為字串是不可變的,所以字串無法採用一般參照型別的行為方式。在方法呼叫中,對字串所做的改變都不會影響原始字串。

*ref引數

像上面所説,值型別通過值傳遞變數是預設的。但也可以迫使值引數通過參照傳遞給方法。為此要使用ref關鍵字。這樣該方法對變數所做的任何改變都會影響原始值。

static void SomeFunction(int[] ints,ref int i)
        {
            ints[0] = 100;
            i = 100;
        }

在呼叫該方法的時候,必須新增ref關鍵字。

SomeFunction(ints, ref i);
*out引數

C#要求變數在被參照前必須用一個初始值進行初始化。但使用out關鍵字來初始化可以簡化C# 編譯器所堅持的輸入引數的初始化。

在方法的輸入引數前加上out字首時,傳遞給該方法的變數可以不初始化。而且該變數通過參照傳遞,所以在從被呼叫的方法中返回時,對應方法對該變數進行的任何改變都會保留下來。

在呼叫該方法時,仍需要使用out關鍵字:

static void SomeFunction(int[] ints,out int i)
         {
                ints[0] = 100;
                 i = 100;
         }

        SomeFunction(ints, out i);
*命名引數

引數一般需要按定義的順序傳遞給方法。命名引數允許按任意順序傳遞。

string FullName(string firstName,string lastName)
    {
        renturn firstName+" " +lastName; 
    }

呼叫方法:

FullName("John","Doe");
      FullName(lastName:"Doe",firstName:"John");
*可選引數

引數也可以是可選的。必須為可選引數提供預設值。可選引數還必須是方法定義的最後一個引數。

void TestMethod(int notOption,int option = 10)
        {
            Console.WriteLine( notOption + option);
        }
*方法的過載

C#支援方法的過載————方法的幾個版本有不同的簽名(方法名相同,但引數的個數和/或型別不同)。

class MathTest
          {
             public int Value;
             public int GetSquare()
             {
                    return Value*Value;
             }

             public  int GetSquare(int x)
             {
                return x*x;
             }
          }

過載方法在引數方面的一些限制:

兩個方法不能僅在返回型別上有區別;

兩個方法不能僅根據引數是宣告為ref還是out來區分。

在任何語言中,對於方法過載,如果呼叫了錯誤的過載方法,就有可能出現執行錯誤。(後面討論如何避免這些錯誤)。

(2)屬性(property)

屬性是一個方法或一對方法,在使用者端看來,它是一個欄位。

public string  SomeProperty
        {
            get
           {
                return "value";
            }
            set
            {
                //設定屬性值
            }
        }

get存取器不帶任何引數,且必須返回屬性宣告的型別。也不應為set存取器指定任何顯示引數,編譯器會

假定它帶一個引數,器型別也許屬性相同,並表示為value.

private int age
        public int  Age
        {
            get
            {
                return age;
            }
            set
            {
                age = valeu;
            }
        }

注意所用的命名約定,採用C#的區分大小寫模式,使用相同的名稱,但公有屬性採用大寫形式命名,如果存在一個等價的私有欄位,則採用小寫形式命名。

一些開發人員喜歡使用把下劃線作為字首的欄位名,如_age,這會為識別欄位提供極大的便利。

*唯讀和只寫屬性

在屬性定義中省略set存取器,就會建立唯讀屬性。這樣使用者端程式碼只可以讀取該屬性的值,但不能設定值。

private int age
        public int  Age
        {
            get
            {
                return age;
            }
        }

同樣在屬性定義中省略get存取器,就會建立只寫屬性。

*屬性的存取修飾符

C#允許給屬性的gei和set存取器設定不同的存取修飾符,所以屬性可以有公有的get存取器和受保護的set存取器。

在gey和set存取器中,必須有一個具有屬性的存取級別(公有)。

*自動實現的屬性

如果屬性的set和get存取器中沒有任何邏輯,就可以使用自動實現的屬性。這種屬性會自動實現後背成員變數。

public int  Age
        {
            get;
            set;
        }

不需要宣告private int age;,編譯器會自動建立它。

使用自動實現的屬性,就不能在屬性設定中驗證屬性的有效性。但必須有兩個存取器,不能把屬性設定為唯讀或只寫。

  public int  Age
  {
    get;//報錯
  }

  但是,每個存取器的存取級別可以不同,
  public int  Age
  {
    get;
    private set;
  }

(3)建構函式

宣告基本建構函式就是宣告一個與包含的類同名的方法,但該方法沒有返回值。

  public class MyClass
  {
    public MyClass()
    {

    }
    //
  }

一般情況下,如果沒有提供任何建構函式,編譯器會在後臺建立一個預設的建構函式。這是一個基本的建構函式,它只能把所有的成員欄位初始化為標準的預設值。這通常就足夠了,否則需要編寫自己的建構函式。

建構函式的過載與其它方法的規則相同。可以為建構函式提供任意多的的過載,只要它們的簽名有明顯區別。

public class MyClass
        {
            public MyClass()
            {

            }

            public MyClass(int i )
            {
                / /
            }
            //
        }

如果提供了帶引數的建構函式,編譯器就不會自動提供預設的建構函式。只有在沒有定義任何建構函式的時候,編譯器才會自動提供預設的建構函式。

public class MyNum
        {
            private int number;
            public MyNum(int number)
            {
                this.number  =number;
            }
        }

一般使用this關鍵字區分成員欄位和同名的引數。

如果試圖使用無引數的建構函式範例化物件就會報錯:

MyNum num = new MyNum();//報錯

可以把建構函式定義為private或protected,這樣不相關的類就不能存取它們:

public class MyNum
        {
            private int number;
            private MyNum(int number)
            {
                this.number  =number;
            }
        }

上述例子沒有為MyNum定義為任何公有或受保護的建構函式。這就使MyNum不能使用new運運算元在外部程式碼中範例化,但可以在MyNum類中編寫一個公有靜態屬性或方法,以範例化該類。

這在下面兩種情況下受有用的:

類僅用作某些靜態成員或屬性的容器,因此永遠不會範例化它。

希望類僅通過某個靜態成員函數來範例化。

*靜態建構函式

C#可以給類編寫無引數的靜態建構函式。這種建構函式只執行一次,而前面的建構函式是範例建構函式,只要建立類的物件,就會

執行它。

  class MyClass
  {
    static MyClass()
    {

    }
  }

編寫靜態建構函式的一個原因是,類有一些靜態欄位或屬性,需要在第一次使用類之前,從外部源中初始化這些靜態欄位和屬性。

.NET執行庫不能確保什麼時候執行靜態建構函式,所以不能把要求在某個特定時刻執行的程式碼放在靜態建構函式中。也不能預計不同類的靜態建構函式按照什麼順序執行。但是可以確保靜態建構函式最多執行一次,就在程式碼參照類之前呼叫它。

在C#中,通常在第一次呼叫類的任何成員之前執行靜態建構函式。

注意,靜態建構函式沒有存取修飾符,其它C#程式碼從來不呼叫它,但在載入類時,總是由.NET執行庫呼叫它,所以像public,private這樣的存取修飾符就沒有任何意義。出於同樣原因,靜態建構函式不能帶任何引數,一個類也只能有一個靜態建構函式。很顯然,靜態構造只能存取累的靜態成員,不能存取類的範例成員。

無引數的範例建構函式與靜態建構函式可以在同一個類中同時定義。雖然參數列相同,但這並不矛盾,因為在載入類的時候執行靜態建構函式,在建立範例時執行範例建構函式,所以何時執行哪個建構函式不會有衝突。

如果任何靜態欄位有預設值,就在呼叫靜態建構函式之前指定它們。

下面演示靜態建構函式的用法:

class MainEntryPoint
{
     static void Main()
     {

      Console.WriteLine("UserPreference:BackColor is " + UserPreference.BackColor.ToString());
     }
}

class UserPreference
{
     public static readonly Color BackColor;
     static UserPreference()
     {
      BackColor = Color.Red;
     }

    private UserPreference()
    {

    }
}

該靜態變數在靜態建構函式中進行初始化。

*從建構函式中呼叫其它建構函式

有時,在一個類中有幾個建構函式,這些建構函式包含一些共同的程式碼。

  class Car
  {
    private string des;
    private int nWheels;
    public Car(string des,int nWheels)
    {
      this.des = des;
      this.nWheels = nWheels;
    }

    public Car(string des)
    {
      this.des = des;
        this.nWheels = 4;
      }
  }

這兩個建構函式初始化了相同的欄位,顯然最好把所有的程式碼放在一個地方。C#有一個特殊的語法,稱為建構函式初始化器,可以實現這個目的。

  class Car
  {
    private string des;
    private int nWheels;
    public Car(string des,int nWheels)
    {
      this.des = des;
        this.nWheels = nWheels;
      }
    public Car(string des):this(des,4)
    {

      }
  }

這裡,this關鍵字僅呼叫引數最匹配的那個建構函式。建構函式初始化器在建構函式的函數體之前執行。

C#建構函式初始化器可以包含對同一個類的另一個建構函式的呼叫,也可以包含對直接基礎類別的建構函式的呼叫,使用同樣的語法,但應用base關鍵字代替this.初始化器中不能有多個呼叫。

3.唯讀欄位

常數是一個包含不能修改的值的變數。但常數不必滿足所有的要求。有時需要一些一些變數,其值不應改變,但在執行之前其值是未知的。C#為這種情形提供了另一種型別的變數:唯讀欄位(readonly)。

readonly關鍵字比const靈活得多,允許把一個欄位設定為常數,但可以執行一些計算,以確定它得初始值。

其規則是可以在建構函式中給唯讀欄位賦值,但不能在其它地方賦值。唯讀欄位還可以是一個範例欄位,類的每個範例可以有不同得值。

與const不同,如果要把唯讀欄位設定為靜態,就必須顯示得宣告它。

二.匿名型別

var關鍵字用於表示隱式型別化得變數。var和new關鍵字一起使用時,可以建立匿名型別。

匿名型別只是一個繼承自Object且沒有名稱的類。

var caption = new {FirstName = "John",LastName="Doe"};

這會生成一個包含FirstName,LastName屬性的物件。

建立另一個物件:

var doctor = new {FirstName = "James",LastName="Mc"};

caption和doctor的型別就相同,可以設定caption = doctor

如果設定的值來自於另一個物件,就可以簡化初始化器。

var doctor = new {caption.FirstName,caption.LastName};

這些物件的型別名未知。編譯器為型別“偽造”了一個名稱,但只有編譯器才能使用它。

三.結構(struct)

如果僅需要一個小的資料結構,此時類提供的功能多餘我們需要的功能,由於效能原因,最好使用結構。

結構是值型別,它們儲存在棧中或儲存為內聯(inline)(如果它們是儲存在堆中的另一個物件的一部分),其生存期的限制與簡單的資料型別一樣。

  • *結構不支援繼承。
  • *對於結構,建構函式的方式與類有一些區別。編譯器總是提供一個無引數的預設建構函式,它是不允許替換的。
  • *使用結構可以指定欄位如何在記憶體中佈局(後面詳細介紹)

結構實際上是把資料項組合在一起,有時大多數位段都宣告為public。嚴格來說,這與編寫.net程式碼的規則相反(欄位應總是私有的(除const欄位外),並由公有屬性封裝)。但是,對於簡單的結構,公有欄位是可以接受的程式設計方式。

四.類和結構的區別

1.結構是值型別

雖然結構是值型別,但在語法上可以把它當作類來處理。

struct PhoneCusStruct
    {
        public const string DaySend = "Mon";
        public int CusId=0;
    }

    PhoneCusStruct phoneCusStruct = new PhoneCusStruct();
    phoneCusStruct.CusId=3;

因為結構是值型別,所以new運運算元與類和其它參照型別的工作方式不同。new運運算元並不分配堆中的記憶體,而只是呼叫相應的建構函式,根據傳送給它的引數,初始化所有的欄位。

對於結構編寫下面的程式碼是合法的:

  PhoneCusStruct phoneCusStruct;
  phoneCusStruct.CusId=3;

結構遵循其它資料型別都遵循的規則:在使用前所有的元素都必須進行初始化。在結構上呼叫new運運算元,或者給所有的欄位分別賦值,結構就完全初始化了。

如果結構定義為類的成員欄位,在初始化包含的物件時,該結構會自動初始化為0.

結構是會影響效能的值型別,但根據使用結構的方式,這種影響可能是正面的,也可能是負面的。正面的影響是為結構分配記憶體時,速度很快,因為它們將內聯或儲存在棧中。在結構超出了作用域被刪除時,速度也很快,不需要等待垃圾回收。負面影響是,只要把結構作為引數來傳遞或者把一個結構賦予另一個結構,結構的內容就會被複制,而對於類只複製參照。這樣就會有效能損失,根據結構的大小,效能損失也不同。

注意,結構主要用於小的資料結構。當把結構作為引數傳遞給方法時,應把它作為ref引數傳遞,以避免效能損失(這樣只傳遞了結構在記憶體中的地址)。

2.結構和繼承

結構不能從一個結構中繼承。唯一的例外是對應的結構(和其它型別一樣)最終派生於類System.Object。因此結構也可以存取Object的方法。

在結構中也可以重寫Object中的方法——如ToString()方法。

結構的繼承鏈是:每個結構派生於System.ValueType類,System.ValueType類有派生於System.Object。ValueType並沒有給Object新增任何成員,但提供了一些更適合結構的實現方法。

注意,不能為結構提供其它基礎類別。

3.結構的建構函式

為結構定義建構函式的方式與類的方式相同,但不允許定義無引數的建構函式。因為在一些罕見的情況下,.NET執行庫不能呼叫使用者提供的自定義無引數建構函式,因此Microsoft乾脆採用禁止在C#的結構內使用無引數的建構函式。

預設建構函式會隱式的把欄位初始化,即使提供了其它帶引數的建構函式,也會先呼叫它。提供欄位的初始值也不能繞過預設建構函式。下面程式碼會編譯錯誤:

  struct PhoneCusStruct
  {
    public int CusId =0;
  }

如果PhoneCusStruct宣告為一個類,就不會報錯了。

另外,可以像類那樣為結構提供Close()或Dispose()方法。

五.弱參照

在應用程式程式碼內範例化一個類或結構時,只要有程式碼參照這個物件,就會形成強參照。這意味著垃圾回收器不會清理這個物件使用的記憶體,一般而言這是好事,因為可能需要參照這個物件,但是如果這個物件很大,而且不經常存取。這個時候可以建立物件的弱參照。

弱參照允許建立和使用物件,但在垃圾回收器執行時,就會回收物件並釋放記憶體。由於存在潛在的Bug和效能問題,一般不會這麼做,但在特定情況下使用是合理的。

弱參照使用WeakReference類建立。因為物件可能在任意時刻被回收,所以參照該物件前必須確認它的存在。  

class MainEntryPoint
{
    static void Main()
    {
        // Instantiate a weak reference to MathTest object
        WeakReference mathReference = new WeakReference(new MathTest()); 
        MathTest math;
        if(mathReference.IsAlive)
        {
            math = mathReference.Target as MathTest;
            math.Value = 30;
            Console.WriteLine(
            "Value field of math variable contains " + math.Value);
            Console.WriteLine("Square of 30 is " + math.GetSquare());
        }
        else
        {
            Console.WriteLine("Reference is not available.");
        }
        GC.Collect();
        
        if(mathReference.IsAlive)
        {
            math = mathReference.Target as MathTest;
        }
        else
        {
            Console.WriteLine("Reference is not available.");
        }
    }
}

// Define a class named MathTest on which we will call a method
class MathTest
{
    public int Value;

    public int GetSquare()
    {
        return Value*Value;
    }

    public static int GetSquareOf(int x)
    {
        return x*x;
    }

    public static double GetPi()
    {
        return 3.14159;
    }
}

六.部分類

partial關鍵字允許把類,結構,介面放在多個檔案中。

partial關鍵字的用法:把partial放在class,struct,interface前面即可。

如果宣告類時使用了下面的關鍵字,這些關鍵字就必須應用於同一個類的所有部分:

public,private,protected,internal,abstract,sealed,new,一般約束

在把部分類編譯後,類的成員和繼承等會合並。

七.靜態類

如果類只包含靜態的方法和屬性,該類就是靜態的。靜態類在功能上與使用私有靜態建構函式建立的類相同。都不能建立靜態類的範例。

使用static關鍵字,編譯器可以檢查使用者是否給該類新增了範例成員。如果是,就會生成一個編譯錯誤。這可以確保不建立靜態類的範例。

  static class PhoneCusStruct
  {
    public static void GetPhene()
    {

    }
  }

呼叫:PhoneCusStruct.GetPhene();

八.Object類

前面提到,所有的.NET類都派生自System.Object類.實際上,如果在定義類的時候沒有指定基礎類別,編譯器就會自動假定這個類派生自Object類。其實際意義在於,除了自己定義的方法和屬性等外,還可以存取Object定義的許多公有的和受保護的成員方法。這些方法可用於自己定義的其它類中。

System.Object的方法:

  • 1.GetHashCode():如果物件放在名為對映(雜湊表或字典)的資料結構中,就可以使用這個方法。處理這些結構的類使用該方法可以確定把物件放在結構的什麼地方。如果希望把類用作字典的一個鍵,就需要重寫這個方法。(後面介紹字典時會詳細介紹)
  • 2.Equals()和ReferenceEquals()方法:後面會詳細介紹。
  • 3.Finalize()方法:在參照物件作為垃圾被回收以清理資源時呼叫它。Object中實現的Finalize()方法實際上什麼也沒有做,因而被垃圾回收器忽略。如果物件擁有對未託管資源的參照,則在該物件被刪除時,就需要刪除這些參照,此時一般要重寫Finalize()方法。垃圾收集器不能直接刪除這些未託管資源的參照,因為它只負責託管的資源,於是它只能依賴使用者提供的Finalize()方法。垃圾收集器不能直接刪除這些未託管資源的參照,因為它只負責託管的資源,於是它只能依賴使用者提供的Finalize方法。
  • 4.GetType()方法:這個方法返回從System.Type派生的類的一個範例。這個物件可以提供物件成員所屬類的很多資訊。System.Type還提供了.NET的反射技術的入口。
  • 5.MemberwiseClose()方法:這個方法複製物件,並返回對副本的一個參照(對於值型別,就是一個裝箱的參照)。得到的副本是一個淺表複製,即它複製了類中的所有值型別。如果類包含內嵌的參照,就只複製參照,而不復制參照的物件。這個方法是受保護的,所以不能用於複製外部的物件(可以複製父類別的物件)。該方法不是虛方法,所以不能重寫。
  • 6.ToString()方法:是獲取物件的字串表示的一種快捷方式。當只需要快速獲取物件的內容,以進行偵錯時,就可以使用這個方法。在資料的格式化方面,它幾乎沒有提供選擇,比如:在原則上日期可以表示為許多不同格式,但DateTime.ToString()沒有在這方面提供任何選擇。這個方法是虛方法,可以重寫這個方法以返回這些型別的正確字串表示。

九.擴充套件方法

如果有類的原始碼,繼承就可以給物件新增方法。但如果沒有原始碼,則可以使用擴充套件方法,它允許改變一個類,但不需要該類的原始碼。

擴充套件方法是靜態方法,它是類的一部分,但實際上沒有放在類的原始碼中。假定PhoneCusStruct類需要一個Add()方法,但不能修改原始碼,就可以建立一個靜態類,把Add()方法新增為一個靜態方法:

  public static class PhoneExtension
  {
    public static void Add(this PhoneCusStruct phoneCusStruct,string phone)
    {
    //
    }
  }

注意擴充套件方法的第一個引數是要擴充套件的型別,它放在this關鍵字的後面。這告訴編譯器,這個方法是PhoneCusStruct型別的一部分。在這個例子中,PhoneCusStruct是要擴充套件的型別。在擴充套件方法中,可以存取所擴充套件型別的所有公有方法和屬性。

呼叫:

PhoneCusStruct p =new PhoneCusStruct();
p.Add();//即使方法是靜態方法,也需要使用實體方法的語法。

如果擴充套件方法與類中的某個方法同名,就不會呼叫擴充套件方法。類中已有的任何實體方法優先。

到此這篇關於C#類和結構的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com