C#創建類型


類(Class)

最常見的一種引用類型

class YourClassName
{
}

class前面可以加上一些關鍵字,比如public、private、abstract、static、readonly

class后面是這個類的名稱,類型名稱后面也可以跟一些東西,比如泛型類、繼承的父類等

 

字段(Fields)

是Class或Struct的成員,它是一個變量

class Octopus
{
    string name;
    public int Age = 10;
}

readonly修飾符

  • readonly修飾符防止字段在構造之后被改變
  • readonly字段只能在聲明的時候被賦值,或在構造函數里被賦值

字段的初始化

  • 字段可以可選的初始化
  • 未初始化的字段有一個默認值
  • 字段的初始化在構造函數之前運行

同時聲明多個字段

static readonly int legs = 8, eyes = 2;

 

方法

  • 一個方法通常有一些語句組成,執行某個動作
  • 參數
  • 返回類型
  • void
  • ref/out

方法的簽名

類型類方法的簽名必須唯一

簽名:方法名、參數類型(含順序,但與參數名稱和返回類型無關)

 

Expression-bodied methods (C# 6)

int Foo (int x)
{
return x * 2;
}
int Foo (int x) => x * 2;
void Foo (int x) => Console.WriteLine (x);

第一個是一個常規的寫法,第二個就是第一個的Expression-bodied方法的形式。當然沒有返回類型的也可以這樣寫,這種寫法只適用於一些簡單的方法,單表達式的方法。

方法的重載(Overloading methods)

類型里的方法可以進行重載(允許多個同名的方法同時存在),只要這些方法的簽名不同就行

void Foo (int x) {...}
void Foo (double x) {...}
void Foo (int x, float y) {...}
void Foo (float x, int y) {...}

在看一個特殊點的方法的例子

void Foo (int x) {...}
float Foo (int x) {...} // 編譯時報錯,簽名相同,因為簽名不包含方法的返回類型
void Goo (int[] x) {...}
void Goo (params int[] x) {...} // 編譯時錯誤,簽名相同,因為簽名不包含參數類型的修飾符(params去掉就是一樣的)

按值傳遞 和 按引用傳遞

參數是按值傳遞的還是按引用傳遞的,也是方法簽名的一部分

void Foo (int x) {...}
void Foo (ref int x) {...} // 是可以的

因為參數的傳遞方式是不同的,一個是值類型,一個是引用類型,所以簽名不一致,是可以的,下面這樣就不行了:

void Foo (int x) {...}
void Foo (ref int x) {...} // 是可以的
void Foo (out int x) {...} // 編譯時錯誤

后兩個方法的參數類型和方法名都是一樣的,而且都是按引用傳遞,所以后兩個的簽名是一樣的

本地方法(C# 7)

一個方法在另一個方法的里面,就叫本地方法(Local methods)

void WriteCubes()
{
    Console.WriteLine (Cube (3));
    Console.WriteLine (Cube (4));
    Console.WriteLine (Cube (5));
    int Cube (int value) => value * value * value;
}
  • 本地方法僅對封閉方法可見,Cube僅在WriteCubes方法內可見
  • 本地方法可以出現在其他函數類型中,例如屬性訪問器、構造函數等
  • 本地方法也可以是迭代器或異步方法
  • static修飾符對於本地方法是無效的

 

構造函數

  • 運行class或struct的初始化代碼
  • 和方法差不多,方法名和類型一致,返回類型也和類型一致,但不寫了

 看一個構造的例子:

public class Panda
{
    string name; // 定義字段
    public Panda (string n) // 定義構造函數
    {
        name = n; // 初始化代碼
    }
}

……
Panda p = new Panda ("Petey"); // 調用構造函數

構造函數允許以下修飾符

  • 訪問修飾符: public internal private protected
  • 非托管代碼修飾符: unsafe extern

從C#7開始,允許單語句的構造函數寫成expression-bodied成員的形式,上面的Panda類的構造函數可以寫成如下形式:

public Panda (string n) => name = n;

 

構造函數的重載

  • class和struct可以重載構造函數
  • 調用重載構造函數時使用this

看下例子:

public class Wine
{
    public decimal Price;
    public int Year;

    public Wine (decimal price) 
    {
        Price = price;
    }

    public Wine (decimal price, int year) : this (price)
    {
        Year = year;
    }
}
  • 當同一個類型下的構造函數A調用構造函數B的時候,B先執行

看下下面的例子:

public class Wine
{
    public decimal Price;
    public int Year;

    public Wine (decimal price) 
    {
        Price = price;
        Console.WriteLine("1");
    }

    public Wine (decimal price, int year) : this (price)
    {
        Year = year;
        Console.WriteLine("2");
    }
}

調用構造函數var wine = new Wine(25m, 1988);

結果是先輸出1,再輸出2

  • 可以把表達式傳遞給另一個構造函數,但表達式本身不能使用this引用,因為這時候對象還沒有被初始化,所以任何方法的調用都會失敗,但是可以使用static方法。

 

無參構造函數

  • 對於class,如果你沒有定義任何構造函數,那么C#編譯器會自動生成一個無參的public構造函數
  • 但是如果你定義了構造函數,那么這個無參的構造函數就不會被編譯器生成了

 

構造函數和字段的初始化順序

  • 字段的初始化發生在構造函數執行之前
  • 字段按照聲明的先后順序進行初始化

 

非public的構造函數

構造函數可以不是public的,看個例子:

public class Class1
{
    Class1() {} // 私有的構造函數
    
    public static Class1 Create (...)
    {
        // 在這里執行自定義的邏輯來返回Class1的實例
        ...
    }
}

那么獲取類的實例則無法在通過new關鍵字獲取了,而是通過類內部的創建實例的靜態方法獲取:var instance = Class1.Create();

單例模式則可以這么寫

 

Deconstructors (C# 7)

C#7引入了deconstructor模式,作用基本和構造函數相反,它會把字段反賦給一堆變量

有一些要求,比如方法名必須是Deconstruct,有一個或多個out參數

class Rectangle
{
    public readonly float Width, Height;
    public Rectangle (float width, float height)
    {
        Width = width;
        Height = height;
    }
    public void Deconstruct (out float width, out float height)
    {
        width = Width;
        height = Height;
    }
}

調用析構函數,我們可以下面的方式

var rect = new Rectangle (3, 4);
(float width, float height) = rect; // Deconstruction
Console.WriteLine (width + " " + height); // 3 4

第二行就是調用析構函數,當然,它也可以這么寫,似乎更好理解一點

float width, height;
rect.Deconstruct (out width, out height);

或者以下的方法都是可以的:

rect.Deconstruct (out var width, out var height);
//或者這樣
(var width, var height) = rect;
//或這樣
var (width, height) = rect;
//或這樣
float width, height;
(width, height) = rect;

析構函數,可以重載,也可以是擴展方法,擴展方法這里就不展開說了

 

對象初始化器(Object Initializers)

對象任何可訪問的字段/屬性在構造之后,可通過對象初始化器直接為其進行設定值

下面是一個Bunny類,包含一個無參的構造函數和一個有參的構造函數

public class Bunny
{
    public string Name;
    public bool LikesCarrots;
    public bool LikesHumans;
    public Bunny () {}
    public Bunny (string n) { Name = n; }
}

使用對象初始化器實例化Bunny對象

// 無參的構造函數可以省略掉小括號
Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false };
Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false };

上面的這個對象初始化器的代碼,就相當於下面的這段代碼:

Bunny temp1 = new Bunny(); // temp1 是編譯器生成的名稱
temp1.Name = "Bo";
temp1.LikesCarrots = true;
temp1.LikesHumans = false;

Bunny b1 = temp1;

Bunny temp2 = new Bunny ("Bo");
temp2.LikesCarrots = true;
temp2.LikesHumans = false;

Bunny b2 = temp2;

臨時變量是為了確保如果在初始化過程中發生了異常,那么不會以一個初始化了一半的對象而結束

 

對象初始化器與可選參數

如果不使用初始化器,上面例子中的構造函數也可以使用可選參數:

public Bunny (string name, bool likesCarrots = false, bool likesHumans = false)
{
    Name = name;
    LikesCarrots = likesCarrots;
    LikesHumans = likesHumans;
}

那么在調用的時候可以這么調用,name是必須的,后面兩個參數可傳可不傳:

Bunny b1 = new Bunny (name: "Bo", likesCarrots: true);

可選參數方式有個優點,可以做到讓Bunny類的字段/屬性只讀,也有個缺點,每個可選參數的值都被嵌入到了調用棧(calling site),C#會把構造函數的調用翻譯成下面的形式:

Bunny b1 = new Bunny ("Bo", true, false);

其實我們使用了兩個參數,最后一個值我們沒有用,但是它把這個值嵌入進去(默認值)

這個可選參數的缺點,有時候可能會引起一些問題,比如,我們在另一個程序集里實例化這個類,然后為這個Bunny再添加了一個參數,變成4個參數了,這個時候就可能有問題,因為如果另一個程序集沒有重新編譯,那么調用的就還是原來的三個參數的構造函數,然后一運行運行時就會報錯了;還有一種情況是,如果改變了其中一個可選參數的值,那么其他程序集的調用者調用的還是會使用原來舊的參數值,直到那些程序集被重新編譯。

 

this 引用

this引用指的是實例的本身

public class Panda
{
    public Panda Mate;
    public void Marry (Panda partner)
    {
        Mate = partner;
        partner.Mate = this;
    }
}

this引用可以讓你把字段與本地變量或參數區分開

public class Test
{
    string name;
    public Test (string name)
    {
        this.name = name;
    }
}

只有class/struct的非靜態成員才可以使用this

 

屬性(Properties)

從外部來看,屬性和字段很像,但從內部看,屬性含有邏輯,就像方法一樣

Stock msft = new Stock();
msft.CurrentPrice = 30;
msft.CurrentPrice -= 3;
Console.WriteLine (msft.CurrentPrice);

我們從外面看不出來CurrentPrice是屬性還是字段

屬性的聲明

屬性的聲明和字段的聲明很像,但是多了一個get、set塊

public class Stock
{
    decimal currentPrice; // The private "backing" field

    public decimal CurrentPrice // The public property
    {
        get { return currentPrice; }
        set { currentPrice = value; }
    }
}

屬性的get set

  • get/set代表屬性的訪問器
  • get訪問器會在屬性被讀取的時候運行,必須返回一個該屬性類型的值
  • set訪問器會在屬性被賦值的時候運行,有一個隱式的該類型的參數value,通常你會把value賦值給一個私有字段

屬性與字段的區別

盡管屬性的訪問方式與字段的訪問方式相同,但不同之處在於,屬性賦予了實現者對獲取和賦值的完全控制權。這種控制允許實現者選擇任意所需的內部表示,不向屬性的使用者公開其內部實現細節。

 

只讀和計算的屬性

如果屬性只有get訪問器,那么它就是只讀的

如果屬性只有set訪問器,那么它就是只寫的(很少這樣用)

屬性通常擁有一個專用的“幕后”字段(backing field),這個幕后的字段用來存儲數據

decimal currentPrice, sharesOwned;
public decimal Worth
{
    get { return currentPrice * sharesOwned; }
}

 

Expression-bodied 屬性

從C#6開始,你可以使用Expression-bodied形式來表示只讀屬性

public decimal Worth => currentPrice * sharesOwned;

C#7,允許set訪問器也可以使用這種形式

public decimal Worth
{
    get => currentPrice * sharesOwned;
    set => sharesOwned = value / currentPrice;
}

 

自動屬性

屬性最常見的一種實踐是:setter和getter只是對private field進行簡單的直接的讀寫。自動屬性聲明就告訴編譯器來提供這種實現

public class Stock
{
    ...
    public decimal CurrentPrice { get; set; }
}

編譯器會自動生成一個幕后的私有字段,其名稱不可引用

set訪問器也可以是private或protected

 

屬性初始化器

從C#6開始,你可以為自動屬性添加屬性初始化器

public decimal CurrentPrice { get; set; } = 123;

只讀的自動屬性也可以使用(只讀自動屬性也可以在構造函數里被賦值)

public int Maximum { get; } = 999;

 

get 和 set 的訪問性

get和set訪問器可以擁有不同的訪問級別,典型的用法:public get,internal/private set

public class Foo
{
    private decimal x;
    public decimal X
    {
        get { return x; }
        private set { x = Math.Round (value, 2); }
    }
}

注意,通常屬性的訪問級別更寬松一些,訪問器的訪問級別更嚴一些(上面例子中的屬性X的public和set的private)

 

CLR的屬性實現

C#的屬性訪問器內部會編譯成get_XXX 和 set_XXX

public decimal get_CurrentPrice {...}
public void set_CurrentPrice (decimal value) {...}

簡單的非virtual屬性訪問器會被JIT編譯器進行內聯(inline)操作,這會消除訪問屬性與訪問字段之間的性能差異。內聯是一種優化技術,它會把方法調用換成直接使用方法體。

 

索引器

索引器提供了一種可以訪問封裝了列表值或字典值的class/struct的元素的一種自然的語法

string s = "hello";
Console.WriteLine (s[0]); // 'h'
Console.WriteLine (s[3]); // 'l'

語法很像使用數組時用的語法,但是這里的索引參數是可以是任何類型的

索引器和屬性擁有同樣的修飾符

可以按照下列方式使用null條件操作符:

string s = null;
Console.WriteLine (s?[0]);

實現索引器

需要定義一個this屬性,並通過中括號指定參數

class Sentence
{
    string[] words = "The quick brown fox".Split();
    public string this [int wordNum] // 索引器
    {
        get { return words [wordNum]; }
        set { words [wordNum] = value; }
    }
}

使用索引器

Sentence s = new Sentence();
Console.WriteLine (s[3]); // fox
s[3] = "kangaroo";
Console.WriteLine (s[3]); // kangaroo

多個索引器

一個類型可以聲明多個索引器,它們的參數類型可以不同

一個索引器可以有多個參數

public string this [int arg1, string arg2]
{
    get { ... } set { ... }
}

只讀索引器

如果不寫set訪問器,那么這個索引器就是只讀的

在C#6以后,也可以使用Expression-bodied語法

public string this [int wordNum] => words [wordNum];

CLR的索引器實現

索引器在內部會編譯成get_Item和set_Item方法

public string get_Item (int wordNum) {...}
public void set_Item (int wordNum, string value) {...}

 

常量

  • 一個值不可以改變的靜態字段,在編譯時值就已經定下來了
  • 任何使用常量的地方,編譯器都會把這個常量替換為它的值
  • 常量的類型可以是內置的數值類型、bool、char、string或enum
  • 使用const關鍵字聲明,聲明的同時必須使用具體的值來對其初始化

一個常量的例子:

public class Test
{
    public const string Message = "Hello World";
}

常量與靜態只讀字段

常量比靜態只讀字段更嚴格:

體現在兩個方面,一個是可使用的類型,第二個是字段初始化的語義上

常量是在編譯時進行值得估算的

public static double Circumference (double radius)
{
    return 2 * System.Math.PI * radius;
}

編譯成下面的形式:

public static double Circumference (double radius)
{
    return 6.2831853071795862 * radius;
}

有一點需要注意,當值有可能改變,並且需要暴露給其它程序集的時候,靜態只讀字段是相對較好的選擇

public const decimal ProgramVersion = 2.3;

如果Y 程序集引用了X程序集並且使用了這個常量,那么在編譯的時候,2.3這個值就會固化在Y程序集里,這意味着,如果后來X重新編譯了,這個常量變成了2.4,如果Y不重新編譯的話,Y仍將使用2.3這個值,直到Y被重新編譯,它的值才會變成2.4.靜態只讀字段就會避免這個問題的發生。

常量不僅僅可以作為類型的字段,它還可以作為本地常量,在方法里聲明常量,例如:

static void Main()
{
    const double twoPI = 2 * System.Math.PI;
    ...
}

 

靜態構造函數

  • 靜態構造函數,每個類型執行一次
  • 非靜態構造函數,每個實例執行一次
  • 一個類型只能定義一個靜態構造函數,並且必須無參、方法名與類型一致
class Test
{
    static Test()
    {
        Console.WriteLine ("Type Initialized");
    }
}
  • 在類型使用之前的一瞬間,編譯器會自動調用類型的靜態構造函數:

一個是實例化一個類型之前,一個是訪問類型的一個靜態成員之前

  • 只允許使用unsafe 和 extern 修飾符

如果靜態構造函數跑出來未處理的異常,那么這個類型在該程序的剩余生命周期內將無法使用了

 

靜態構造函數和字段初始化順序

  • 靜態字段的初始化器在靜態構造函數被調用之前的一瞬間運行
  • 如果類型沒有靜態構造函數,那么靜態字段初始化器在類型被使用之前的一瞬間執行,或者更早,在運行時突發奇想的時候就執行了
  • 靜態字段的初始化順序與它們的聲明順序一致
class Foo
{
    public static int X = Y; // 0
    public static int Y = 3; // 3
}

因為是按照聲明的順序初始化的,所以X的時候Y還沒有,所以是0。

class Program
{
    static void Main() { Console.WriteLine (Foo.X); } // 3
}
class Foo
{
    public static Foo Instance = new Foo();
    public static int X = 3;
    Foo() { Console.WriteLine (X); } // 0
}

在類Program里,輸出的是3,因為在使用Foo類之前這個類型就已經初始化完成,所以是3,而在Foo類里面,則按照什么的先后順序分別初始化靜態構造函數,然后是靜態字段,所以靜態構造函數輸出的是0,X還沒有初始化。

 

靜態類

類也可以是靜態的,但是有個要求,就是其成員也必須全是靜態的,不可以有子類。

例如:System.Console 、System.Math 等

 

終結器(Finalizers)

Finalizer是class專有的一種方法,在GC回收未引用對象的內存之前運行,其實就是對object的Finalize()方法重寫的一種語法

這個很少用,例子:

class Class1
{
    ~Class1()
    {
        ...
    }
}

編譯生成的是這樣的

protected override void Finalize()
{
    ...
    base.Finalize();
}

C#7可以這樣寫:

~Class1() => Console.WriteLine ("Finalizing");

 


免責聲明!

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



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