動態

詳情 返回 返回

C# 的類型系統 - 動態 詳情

C# 是一種強類型語言。每個變量和常量都有其類型,每個計算出結果為值的表達式也是如此。每個方法聲明都會指定每個輸入參數以及返回值的名稱、類型和類型(值類型、引用類型或輸出類型)。.NET 類庫定義了內置的數值類型和複雜類型,這些類型代表了各種各樣的結構。其中包括文件系統、網絡連接、對象的集合和數組以及日期。一個典型的 C# 程序會使用類庫中的類型以及用户自定義的類型,這些類型能夠模擬與程序所處理的問題領域相關的特定概念。

一個類型所存儲的信息可包括以下內容:

  • 該變量所佔用的存儲空間。
  • 它所能表示的最大和最小值。
  • 它包含的成員(方法、字段、事件等)。
  • 它所繼承的基類型。
  • 它所實現的接口。
  • 允許執行的操作。

編譯器利用類型信息來確保您代碼中執行的所有操作都是類型安全的。例如,如果您聲明瞭一個類型為 int 的變量,編譯器允許您在加法和減法運算中使用該變量。如果嘗試在類型為 bool 的變量上執行相同的運算,編譯器會生成錯誤,如下例所示:

int a = 5;
int b = a + 2; // Ok

bool ber = true;
int c = a + ber // 錯誤。‘+’ 操作運算符不能應用於類型為 “整數” 和 “布爾值” 的操作數

注意:C 和 C++ 開發者們請注意,在 C# 中,布爾類型(bool)不能轉換為整型(int)。

編譯器將類型信息嵌入到可執行文件中作為元數據。通用語言運行時(CLR)在運行時利用這些元數據來進一步確保類型的安全性,以便在分配和回收內存時做到這一點。

在變量聲明中指定類型

在程序中聲明變量或常量時,您必須要麼指定其類型,要麼使用 var 關鍵字讓編譯器自動推斷類型。以下示例展示了一些同時使用內置數值類型和複雜用户定義類型的變量聲明:

// 僅聲明:
float WD;
string XM;
MyClass myClass;

// 聲明並初始化(四個例子):
char ZF首 = 'C';
var XZ = 3;
int [ ] Y = [ 0 , 1 , 2 , 3 , 4 , 5 ];
var CX = from XM in Y
            where XM <= XZ
            select XM;

方法的參數類型和返回值在方法聲明中已明確説明。以下的簽名展示了一個方法,它需要一個整數作為輸入參數,並返回一個字符串:

public string FF獲取姓名 ( int ID )
    {
        if ( ID < XMs . Length )
            return XMs [ ID ];
        else
            return String . Empty;
    }
private string [ ] XMs = [ "小明" , "小霞" , "小紅" ];

在聲明一個變量之後,就不能再用新的類型對其進行重新聲明,也不能給它賦值與所聲明類型不兼容的值。例如,你不能先聲明一個整型變量,然後給它賦一個布爾值(true)。然而,值可以轉換為其他類型,比如當它們被賦給新變量或作為方法參數傳遞時。編譯器會自動執行不會導致數據丟失的類型轉換。而可能會導致數據丟失的轉換則需要在源代碼中使用類型轉換操作符(cast)。

內置類型

C# 提供了一組標準的內置類型。這些類型包括整數、浮點值、布爾表達式、文本字符、十進制值以及其他數據類型。此外,還有內置的字符串和對象類型。這些類型可供您在任何 C# 程序中使用。

自定義類型

您可以通過使用結構體(struct)、類(class)、接口(interface)、枚舉(enum)和記錄(record)等構造來創建自己的自定義類型。.NET 類庫本身就是一個包含各種自定義類型的集合,您可以將其用於自己的應用程序中。默認情況下,類庫(class library)中最常用的類型在任何 C# 程序中都是可用的。其他類型只有在您明確添加對定義它們的程序集的項目引用時才會可用。在編譯器獲得該程序集的引用後,您可以在源代碼中聲明該程序集中聲明的類型的變量(和常量)。

在定義類型時,您首先要做出的一個決定就是確定使用哪種結構來表示該類型。以下列表有助於您做出這一初步決定。這些選項之間存在一定的重疊。在大多數情況下,不止一種選擇都是合理的。

  • 如果數據存儲大小較小(不超過 64 字節),則應選擇 struct 或 record struct。
  • 如果該類型是不可變的,或者您希望進行非破壞性修改,則應選擇 struct 或 record struct。
  • 如果您的類型需要具有值語義以實現相等性比較,則應選擇 record class 或 record struct。
  • 如果該類型主要用於存儲數據而非行為,則應選擇 record class 或 record struct。
  • 如果該類型屬於繼承層次結構的一部分,則應選擇 record class 或 class。
  • 如果該類型使用多態性,則應選擇 class。
  • 如果主要目的是行為,則應選擇 class。

常見的類型系統

瞭解.NET 中類型系統中的兩個基本要點非常重要:

  • 它支持繼承原則。類型可以從其他類型(稱為基類型)派生而來。派生類型(在某些限制條件下)會繼承基類型的方法、屬性和其他成員。基類型也可以進一步從其他類型派生,此時派生類型會在其繼承層次結構中繼承兩個基類型的成員。所有類型,包括諸如 System . Int32(C# 關鍵字:int)這樣的內置數值類型,最終都源自一個單一的基類型,即 System . Object(C# 關鍵字:object)。這種統一的類型層次結構被稱為通用類型系統(CTS)。
  • CTS 中的每個類型都被定義為值類型或引用類型。這些類型包括 .NET 類庫中的所有自定義類型以及您自己定義的用户自定義類型。使用 struct 關鍵字定義的類型是值類型;所有內置數值類型都是結構體。使用 class 或 record 關鍵字定義的類型是引用類型。引用類型和值類型具有不同的編譯時規則和不同的運行時行為。

以下示例展示了 CTS 中值類型與引用類型之間的關係。

注意:您可以看到,最常用的類型都歸類在 “System” 命名空間中。然而,一個類型所處的命名空間與它是值類型還是引用類型並無關聯。

class 和 struct 是 .NET 中通用類型系統中的兩種基本結構。它們本質上都是數據結構,能夠將一組相關聯的數據和行為封裝成一個邏輯單元。數據和行為構成了 class、struct 或 record 的成員。這些成員包括其方法(Method)、屬性(Property)、事件(Event)等等。

class、struct 或 record 的聲明就像是用於在運行時創建實例或對象的藍圖。如果你定義了一個名為 “Ren” 的 class、struct 或 record,那麼 “Ren” 就是該類型的名稱。如果你聲明並初始化一個名為 “r” 的具有 “Ren” 類型變量,那麼 “r” 就被認為是 “Ren” 類型的對象或實例。可以創建同一 “Ren” 類型的多個實例,並且每個實例在其屬性和字段中都可以有不同的值。

class 是一種引用類型。當創建該類型的對象時,分配給該對象的變量僅保存對該內存的引用。當將對象引用賦給一個新的變量時,新的變量將指向原始對象。通過一個變量所做的更改會在另一個變量中得到反映,因為它們都指向相同的數據。

struct 是一種值類型。當創建 struct 時,分配給該 struct 的變量會保存 struct 的實際數據。當將 struct 賦值給一個新的變量時,會進行復制。因此,新變量和原始變量會分別包含相同數據的兩個獨立副本。對其中一個副本所做的更改不會影響另一個副本。

record 類型可以是引用類型(即 record class)或者值類型(即 record struct)。record 類型包含支持值相等性的方法。

一般來説,class 用於模擬更復雜的行為。class 通常會存儲在創建 class 對象之後仍需修改的數據。struct 最適合用於小型數據結構。struct 通常存儲在創建 struct 之後無需修改的數據。record 類型是具有額外編譯器自動生成成員的數據結構。record 通常存儲在對象創建之後無需修改的數據。

值類型

值類型源自於 System . ValueType 類,而 System . ValueType 又源自於 System . Object 類。從 System . ValueType 類派生出來的類型在 CLR 中具有特殊的特性。值類型變量直接包含其值。對於 struct 類型,其內存會在變量聲明的任何上下文中內聯分配。對於值類型變量,不存在單獨的堆分配或垃圾回收開銷。您可以聲明 record struct 類型(這些是值類型),幷包含 record 的合成成員。

值類型分為兩類:struct 和 enum。

內置的數值類型是 struct,它們具有可以訪問的字段和方法:
byte b = byte . MaxValue; // byte 類型的常數字段
但您卻像對待簡單非聚合類型一樣為它們聲明並賦值:

byte zj = 0xA;
int z = 5;
char zf = 'Z';

值類型是密封的。您不能從任何值類型(例如 System . Int32)派生出新的類型。您也不能定義一個 struct 來繼承任何用户自定義的 class 或 struct,因為 struct 只能繼承 System . ValueType 類型。然而,struct 可以實現一個或多個接口。您可以將 struct 類型轉換為它所實現的任何接口類型。這種轉換會引發 boxing(裝箱)操作,將 struct 封裝在一個託管堆上的引用類型對象中。boxing(裝箱)操作會在您將值類型傳遞給接受 System . Object 或任何接口類型作為輸入參數的方法時發生。

您使用 “struct” 關鍵字來創建您自己的自定義值類型。通常,一個 struct 會被用作一組相關變量的容器,如以下示例所示:

public struct ZuoBiao
    {
        public int x , y;

        public ZuoBiao ( int p1 , int p2 )
            {
                x = p1;
                y = p2;
            }
    }

另一種值類型是 enum(枚舉)。枚舉定義了一組帶名稱的整型常量。例如,.NET 類庫中的 “System . IO . FileMode” 枚舉包含一組帶名稱的常量整數,這些整數用於指定如何打開一個文件。其定義方式如下所示:

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

System . IO . FileMode . Create 常量的值為 2。然而,該名稱對於閲讀源代碼的人來説更具意義,因此為了便於理解,最好使用枚舉而非常量的數字表示。

所有的 enum 類都繼承自 System . Enum 類,而 System . Enum 類又繼承自 System . ValueType 類。適用於 struct 的所有規則同樣也適用於 enum 類。

引用類型

被定義為 class、record、delegate、array 或 interface 的類型屬於引用類型。

當您聲明一個引用類型的變量時,該變量的值默認為 “null”。只有在您為其賦值一個該類型的實例或者使用 new 運算符創建一個實例後,其值才會變為非 “null”。下面的示例展示了類的創建和賦值過程:

LeiMy l1 = new ( );
LeiMy l2 = l1;

不能直接使用 “new” 運算符來實例化一個接口。相反,需要先創建並賦值一個實現了該接口的類的實例。請看下面的示例:

LeiMy my = new ( );

// 使用現有值來聲明並賦值
IMyInterface myInterface = my;

// 或者在單個語句中創建並賦值一個值
IMyInterface myInterface2 = new LeiMy ( );

當對象被創建時,內存會在託管堆上進行分配。該變量僅保存對象所在位置的引用。託管堆上的類型在分配和回收時都需要額外開銷。垃圾回收是 CLR 的自動內存管理功能,負責進行回收操作。然而,垃圾回收也經過了高度優化,在大多數情況下不會造成性能問題。

所有數組都是引用類型,即便其元素為值類型。數組會隱式地派生自 “System . Array” 類。您可以通過 C# 提供的簡化語法來聲明和使用它們,如以下示例所示:

// 聲明並初始化一個整數數組
int [ ] nums = [ 1 , 2 , 3 , 4 , 5 ];
// 訪問 System . Array 類的實例屬性
int len = nums . Length;

引用類型完全支持繼承。在創建類時,您可以從任何未定義為密封的其他接口或類中繼承。其他類可以繼承您的類並重寫您的虛方法。

基本值的類型

在 C# 中,基本值的類型由編譯器決定。您可以通過在數字末尾添加一個字母來指定數值的類型。例如,要指定值 4.56 應被視為浮點數,則在數字後面添加 “f” 或 “F”:4.56f。如果沒有添加字母,則編譯器會推斷出該基本值的類型。

因為字面值是有類型的,而所有類型最終都源自 “System . Object” 類,所以您可以編寫並編譯如下這樣的代碼:

string s = "答案是:" + 5 . ToString ( );
// 答案是:5
Console . WriteLine ( s );

Type lx = 12345 . GetType ( );
// System . Int32
Console . WriteLine ( lx );

泛型類型

一個類型可以通過一個或多個類型參數來聲明,這些參數用作實際類型(具體類型)的佔位符。客户端代碼在創建該類型的實例時會提供具體的類型。這類類型被稱為泛型類型。例如,.NET 類型 System . Collections . Generic . List < T > 有一個類型參數,按照慣例該參數被命名為 T。在創建該類型的實例時,您需要指定列表中包含的對象的類型,例如字符串:

List < string > LB字符串 = new ( );
LB字符串 . Add ( "字符串示例:" );
// 編譯時錯誤:添加的類型不是字符串類型:
LB字符串 . Add ( 4 );

編譯時:使用類型參數使得能夠使用同一個類來存儲任何類型的元素,而無需將每個元素轉換為對象。泛型集合類被稱為強類型集合,因為編譯器知道集合元素的具體類型,並且如果例如在上一個示例中您嘗試將整數添加到字符串列表對象中,編譯器會在編譯時引發錯誤。

隱式類型、匿名類型和可 null 值類型

您可以使用 “var” 關鍵字為局部變量(但不能為 class 成員)進行隱式類型定義。該變量在編譯時仍會獲得一個類型,但該類型是由編譯器提供的。

對於那些您並不打算存儲或在方法邊界之外傳遞的簡單相關值集合,創建具有名稱的類型可能會有些不便。在這種情況下,您可以使用匿名類型來實現。

普通的值類型不能具有 null 值。然而,您可以通過在類型後添加一個 “?” 來創建可為 null 的值類型。例如,int? 是一種 int 類型,它也可以具有 null 值。可為 null 的值類型是 System . Nullable < T > 這種通用結構類型的實例。當您在數據庫中傳遞和從數據庫中獲取數據(其中數值可能會為 null)時,可為空的值類型特別有用。

編譯時類型和運行時類型

一個變量可以具有不同的編譯時類型和運行時類型。編譯時類型是指變量在源代碼中所聲明或推斷出的類型。運行時類型則是該變量所引用的實例的類型。通常這兩種類型是相同的,如下例所示:
string XinXi = "這是一串字符";
在其他情況下,編譯時的類型會有所不同,如下面的兩個示例所示:

object XinXi2 = "這是另一串字符";
IEnumerable < char > ZF小寫 = "abcdefghijklmnopqrstuvwxyz";

在其他情況下,編譯時的類型會有所不同,如下面所示。在上述兩個示例中,運行時的類型均為字符串。第一行的編譯時類型為 “object”,而第二行的編譯時類型為 “IEnumerable < char >”。

如果某個變量的兩種類型有所不同,那麼就需要明確編譯時的類型和運行時的類型分別在何時起作用。編譯時的類型決定了編譯器所執行的所有操作。這些編譯器操作包括方法調用的解析、重載的解析以及可用的隱式和顯式轉換。而運行時的類型則決定了在運行時被解析的所有操作。這些運行時操作包括虛擬方法調用的分派、is 和 switch 表達式的求值以及其他類型測試 API。為了更好地理解您的代碼與類型之間的交互方式,要明確哪種操作適用於哪種類型。

聲明命名空間以對類型進行分類

在 C# 編程中,命名空間的使用非常廣泛,主要有兩種方式。首先,.NET 利用命名空間來組織其眾多的類,具體如下:
System . Console . WriteLine ( "Hello World!" );
System 是一個命名空間,而 Console 則是該命名空間中的一個類。可以使用 “using” 關鍵字,從而無需給出完整的名稱,例如如下所示:

using System;
Console . WriteLine ( "Hello World!" );

重要事項:
.NET 6 的 C# 模板採用頂層語句。如果您已經將應用程序升級到.NET 6,那麼您的應用程序可能與本文中的代碼不匹配。

.NET 6 SDK 還為使用以下 SDK 的項目添加了一組隱式全局使用指令:

  • Microsoft . NET . Sdk
  • Microsoft . NET . Sdk . Web
  • Microsoft . NET . Sdk . Worker

這些隱式的全局使用指令包含了項目類型中最常用的命名空間。

其次,聲明自己的命名空間有助於您在大型編程項目中控制類和方法名稱的範圍。使用 “namespace” 關鍵字來聲明一個命名空間,例如如下所示:

namespace MMKJ示例
    {
        class Lei示例
            {
                public void FF示例 ( )
                    {
                        System . Console . WriteLine ( "FF示例 在 MMKJ示例 內部" );
                    }
            }
    }

該命名空間的名稱必須是有效的 C# 標識符名稱。

您可以為該文件中定義的所有類型聲明一個命名空間,如以下示例所示:

namespace MMKJ示例;

class Lei另一個示例
    {
        public void FF示例 ( )
            {
                System . Console . WriteLine ( "FF示例 在MMKJ示例 內部" );
            }
    }

這種新語法的優點在於它更簡潔,節省了水平空間和括號。這使得您的代碼更易於閲讀。

命名空間概述

命名空間具有以下特性:

  • 他們負責組織大型代碼項目。
  • 它們通過使用 “.” 運算符來劃分。
  • “using” 指令消除了為每個類指定命名空間名稱的需求。
  • 全局命名空間是 “根” 命名空間:global::System 總是指向 .NET 的 System 命名空間。

類的介紹

引用類型

被定義為類的類型屬於引用類型。在運行時,當您聲明一個引用類型的變量時,該變量在創建實例之前會包含值 “null”(即為空值)。只有通過使用 new 操作符顯式創建該類的實例,或者將其賦值為在其他地方創建的具有兼容類型的對象時,該變量才會獲得實際值,如以下示例所示:

// 聲明一個類型為 “Lei” 的對象
Lei l1 = new ( );
// 聲明另一個相同類型的對象,並將第一個對象的值賦給它
Lei l2 = l1;

當對象被創建時,會在託管堆上為該特定對象分配足夠的內存,而變量僅保存對該對象所在位置的引用。對象所使用的內存由 CLR 的自動內存管理功能回收,這被稱為垃圾回收。

聲明類

通過使用 “class” 關鍵字後跟一個唯一的標識符來聲明類,如下例所示:

// [訪問修飾符] - [class] - [標識符]
public class Lei示例
    {
        // 字段,屬性,方法 和 事件 在此……
    }

當創建對象時,會在託管堆上為該特定對象分配足夠的內存,而變量僅保存對該對象所在位置的引用。對象所使用的內存由 CLR 的自動內存管理功能回收,這被稱為垃圾回收。class 關鍵字前可選地使用訪問修飾符。class 類型的默認訪問級別為 internal。由於此處使用了 public,因此任何人都可以創建此類的實例。類名緊跟在 class 關鍵字之後。類名必須是有效的 C# 標識符名稱。定義的其餘部分是類體,在其中定義行為和數據。類上的字段、屬性、方法和事件統稱為類成員。

創建對象

儘管有時會混用這兩個詞,但類和對象是不同的東西。類定義了一種對象的類型,但它本身並非對象。對象是基於類的具體實體,有時也被稱為類的一個實例。

可以通過使用 “new” 關鍵字後跟類名來創建對象,如下所示:
Lei l1 = new Lei ( );
當創建對象時,會在託管堆上為該特定對象分配足夠的內存,而變量僅保存對該對象位置的引用。對象所使用的內存由自動內存管理功能回收。當創建類的實例時,會將對該對象的引用返回給程序員。在前面的示例中,l1 是基於 Lei 的對象的引用。此引用指向新對象,但並不包含對象數據本身。實際上,您甚至可以在根本不創建對象的情況下創建對象引用:
Lei l2;
當對象創建時,會在託管堆中為該特定對象分配足夠的內存,而變量僅保存對該對象位置的引用。對象所佔用的內存由自動內存管理功能負責回收。當使用這樣的對象引用進行訪問時,如果引用所指向的對象已不存在,則在運行時會引發錯誤。一個引用可以指向一個對象,可以通過創建新對象或將現有對象賦值給它來實現,例如:

Lei l3 = new ( );
Lei l4 = l3;

這段代碼創建了兩個對象引用,它們都指向同一個對象。因此,通過 l3 對這個對象所做的任何更改都會反映在對 l4 的後續使用中。由於基於類的對象是通過引用來引用的,所以類被稱為引用類型。

構造函數與初始化

前面的章節介紹了聲明類類型以及創建該類型實例的語法。當創建一個類類型的實例時,您需要確保其字段和屬性被初始化為有用的值。有幾種方法可以進行值的初始化:

  • 接受默認值
  • 字段初始化器
  • 構造函數參數
  • 對象初始化器

每個.NET 類型都有一個默認值。通常,對於數值類型,該值為 0;對於所有引用類型,該值為 null。在您的應用程序中,當這種默認值在合理範圍內時,您可以依賴它。
當.NET 的默認值不恰當時,您可以使用字段初始化器來設置初始值:

public class Container
    {
        // 初始化 capacity 字段到默認值 10:
        private int _capacity = 10;
    }

您可以通過定義一個構造函數來實現這一功能,該構造函數負責設置初始值:要求來電者提供初始值。

public class Container
    {
        private int _capacity;

        public Container ( int capacity ) => _capacity = capacity;
    }

從 C# 12 版本開始,您可以在類聲明中定義一個主構造函數:

public class Container( int capacity )
    {
        private int _capacity = capacity;
    }

開始:在類名中添加參數會定義主構造函數。這些參數可在類體中使用,類體包括其成員。您可以利用這些參數來初始化字段或在任何需要的地方進行使用。
您還可以將所需的修飾符應用於屬性,並允許調用者使用對象初始化器來設置該屬性的初始值:

public class Ren
    {
        public required string 姓 { get; set; }
        public required string 名 { get; set; }
    }

開始為類名添加參數會定義 Main 構造函數。這些參數可在類體中使用,包括其成員。您可以利用這些參數來初始化字段或在任何需要的地方進行賦值。

添加了 “required” 關鍵字意味着調用者必須在新的表達式中設置這些屬性:

var r1 = new Ren ( ) // 錯誤!未設置必須的屬性
var c2 = new Ren ( 姓 = "王" , 名 = "小華" );

開始為類名添加參數會定義 Main 構造函數。這些參數可在類體中使用,類體包括其成員。您可以利用這些參數來初始化字段或在需要的地方進行其他操作。

class 繼承

class 完全支持繼承,這是面向對象編程的一個基本特性。當您創建一個 class 時,可以繼承自任何未被定義為密封的其他 class。其他 class 可以繼承自您的 class 並重寫 class 的 virture 方法。此外,您還可以實現一個或多個接口(interface)。

繼承是通過使用派生實現的,這意味着一個 class 是通過使用一個基類(base class)來聲明的,該基類為該類提供了數據和行為的繼承。基類的指定方式是在派生類名稱之後加上冒號和基類的名稱,例如:

public class Lei經理 : Lei員工
    {
        // 員工的字段、屬性、方法和事件是繼承自父類的
        // 新的經理類的字段、屬性、方法和事件則放在這裏……
    }

當一個類聲明包含一個基類時,它會繼承該基類的所有成員(除了構造函數)。

在 C# 中,一個類只能直接繼承一個基類。然而,由於基類本身可以繼承另一個類,所以一個類可能會間接繼承多個基類。此外,一個類可以直接實現一個或多個接口。

一個類可以被聲明為 abstract(抽象類)。抽象類包含具有簽名定義但無實現的抽象方法。抽象類不能被實例化。它們只能通過實現這些抽象方法的派生類來使用。相比之下,sealed(密封類)不允許其他類從其派生。

類定義可以分散在不同的源文件中。

record 類型的介紹

在 C# 中,記錄是一種 class 或 struct,它為處理數據模型提供了特殊的語法和行為。record 修飾符會指示編譯器自動生成一些成員,這些成員對於那些主要作用於存儲數據的類型來説非常有用。這些成員包括 ToString ( ) 的重載版本以及支持值相等性的成員。

何時使用記錄

在以下情形中,可考慮使用記錄來替代類或結構體:

  • 您希望定義一個依賴於值相等性的數據模型。
  • 您希望定義一種對象不可變的類型。

值相等性

對於 record 類型,值相等性意味着如果 record 類型的類型匹配且所有屬性和字段的值都相等,那麼兩個 record 類型的變量就是相等的。對於其他引用類型(如 class),默認情況下相等性指的是引用相等性,除非已實現值相等性。也就是説,類類型的兩個變量如果指向同一個對象,則就是相等的。用於確定兩個 record 實例相等性的方法和運算符使用的是值相等性。

並非所有的數據模型都能很好地適應值相等性原則。例如,Entity Framework Core 依賴於引用相等性來確保對於概念上屬於同一實體的多個實例,它只會使用一個實體類型的唯一實例。因此,record 類型不適用於在 Entity Framework Core 中作為實體類型使用。

不可變性

不可變類型是指在對象實例化後,不允許您更改其任何屬性或字段的值。在某些情況下,如需要類型具備線程安全性或需要確保哈希表中的哈希碼保持不變時,不可變性會非常有用。record 提供了創建和處理不可變類型的簡潔語法。

不可變性並不適用於所有數據場景。例如,Entity Framework Core 不支持對不可變的實體類型進行更新操作。

record 與 class 和 struct 的區別

聲明和實例化 class 或 struct 所使用的相同語法同樣適用於 record。只需將 “class” 關鍵字替換為 “record”,或者使用 “record struct” 而非 “struct”。同樣,record class 也支持表達繼承關係的相同語法。record 與 class 的區別在於以下方面:

  • 在主構造函數中可以使用位置參數來創建並實例化具有不可變屬性的類型。
  • 在 class 中表示引用相等或不相等的方法和運算符(例如 Object . Equals ( Object ) 和 ==);在 record 中表示值相等或不相等。
  • 可以使用 with 表達式來創建一個具有選定屬性新值的不可變對象的副本。
  • record 的 ToString 方法會創建一個格式化的字符串,該字符串會顯示對象的類型名稱以及其所有公共屬性的名稱和值。
  • record 可以繼承另一個 record。record 不能繼承 class,而 class 不能繼承 record。

record struct 與 struct 的不同之處在於編譯器會自動生成相等性和 ToString 方法。編譯器還會為位置 record struct 自動生成一個 Deconstruct(解構)方法。

在 record class 中,編譯器會為每個基本構造函數參數生成一個只供初始化使用的公共屬性。在 record struct 中,編譯器會生成一個可讀寫的公共屬性。對於不包含 record 修飾符的 class 和 struct 類型中的基本構造函數參數,編譯器不會為其生成屬性。

示例

以下示例定義了一個公共記錄,該記錄使用位置參數來聲明和實例化記錄。隨後它會打印出記錄的類型名稱以及其屬性值:

public record 人 ( string 姓 , string 名 )
    {
        public required string [ ] 電話號碼 { get; init; }
    }

public class Program
    {
        public static void Main ( )
            {
                人 r1 = new ( "段" , "飛飛" ) { 電話號碼 = new string [ 1 ] };
                Console . WriteLine ( r1 );
                // 人 { 姓 = 段 , 名 = 飛飛 , 電話號碼 = System . String [ ] }

                人 r2 = r1 with { 姓 = "劉" };
                Console . WriteLine ( r2 );
                // 人 { 姓 = 劉 , 名 = 飛飛 , 電話號碼 = System . String [ ] }
                Console . WriteLine ( r1 == r2 ); // False

                r2 = r1 with { 電話號碼 = new string [ 1 ] };
                Console . WriteLine ( r2 );
                // 人 { 姓 = 段 , 名 = 飛飛 , 電話號碼 = System . String [ ] }
                Console . WriteLine ( r1 == r2 ); // False

                r2 = r1 with { };
                Console . WriteLine ( r1 == r2 ); // True
            }
    }

interface - 為多種類型定義行為

接口包含一組相關功能的定義,非抽象類或結構體必須實現這些功能。接口可以定義靜態方法,這些方法必須有實現。接口可以為成員定義默認實現。接口不能聲明實例數據,例如字段、自動實現的屬性或類似屬性的事件。

通過使用接口,例如,您可以在一個類中包含來自多個來源的行為。這種能力在 C# 中很重要,因為該語言不支持類的多重繼承。此外,如果您想為結構體模擬繼承,就必須使用接口,因為它們實際上不能從另一個結構體或類繼承。

您可以通過使用 “interface” 關鍵字來定義一個接口,如下例所示。

interface IEquatable < T >
    {
        bool Equals ( T obj );
    }

接口的名稱必須是有效的 C# 標識符名稱。按照慣例,接口名稱以大寫字母 I 開頭。

任何實現了 IEquatable < T > 接口的 class 或 struct 都必須包含一個與該接口指定的簽名相匹配的 Equals 方法的定義。因此,您可以確信實現了 IEquatable < T > 接口的 T 類型的類包含一個 Equals 方法,通過該方法,此類的一個實例可以確定它是否與同一類的另一個實例相等。

IEquatable < T > 的定義並未提供 Equals 的實現。一個 class 或 struct 可以實現多個接口,但一個 class 只能繼承自一個 class。

接口可以包含實例方法、屬性、事件、索引器,或者這四種成員類型的任意組合。接口可以包含 static 構造函數、字段、常量或運算符。從 C# 11 開始,非字段的接口成員可以是 static abstract 的。接口不能包含實例字段、實例構造函數或終結器。接口成員默認為 public 的,您可以顯式指定訪問修飾符,例如 public(公共)、protected(受保護)、internal(內部)、private(私有)、protected internal(受保護的內部)或 private protected(私有的受保護)。private 成員必須具有默認實現。

要實現接口成員,實現類中的相應成員必須是 public 的、非 static 的,並且與接口成員具有相同的名稱和簽名。

注意:當一個接口聲明 static 成員時,實現該接口的類型也可以聲明具有相同簽名的 static 成員。這些成員是不同的,並且通過聲明該成員的類型來唯一標識。在類型中聲明的 static 成員不會覆蓋在接口中聲明的 static 成員。

實現接口的 class 或 struct 必須為接口中聲明的所有成員提供實現,除非接口中已提供了默認實現。但是,如果基類實現了某個接口,則從該基類派生的任何類都會繼承該實現。

以下示例展示了 IEquatable < T > 接口的一個實現。實現該接口的類 Car 必須提供 Equals 方法的實現。

public class 轎車 : IEquatable < 轎車 >
    {
        public string? 製造商 { get; set; }
        public string? 型號 { get; set; }
        public string? 出廠日期 { get; set; }

        // IEquatable < T > 接口的實現
        public bool Equals ( 轎車? 比較目標 )
            {
                return ( this . 製造商 , this . 型號 , this . 出廠日期 ) == ( 比較目標? . 製造商 , 比較目標? . 型號 , 比較目標? . 出廠日期 );
            }
    }

class 的屬性和索引器可以為接口中定義的屬性或索引器定義額外的訪問器。例如,一個接口可能聲明一個具有獲取訪問器的屬性。而實現該接口的類可以為同一個屬性同時聲明具有獲取和設置訪問器的版本。然而,如果屬性或索引器使用顯式實現方式,那麼訪問器必須保持一致。

接口可以繼承一個或多個接口。派生接口會繼承其基接口中的成員。實現派生接口的 class 必須實現派生接口中的所有成員,包括派生接口的基接口中的所有成員。該類可以隱式轉換為派生接口或其任何基接口。一個類可能會通過其繼承的基類或通過其他接口繼承的接口多次包含一個接口。然而,該類只能一次提供一個接口的實現,並且只有當該類將該接口作為類的定義的一部分進行聲明(類名 : 接口名)時才可如此。如果接口是由於繼承實現了該接口的基類而被繼承的,那麼基類將提供該接口成員的實現。然而,派生類可以重新實現任何虛接口成員,而不是使用繼承的實現。當接口聲明瞭一個方法的默認實現時,任何實現該接口的類都會繼承該實現(您需要將類實例轉換為接口類型,以便能夠訪問接口成員中的默認實現)。

基類也可以通過使用 virtual 成員來實現接口成員。在這種情況下,派生類可以通過重寫 virtual 成員來改變接口的行為。

接口概述

一個接口具有以下特性:

  • 在 C# 8.0 之前的版本中,接口類似於只包含 abstract 成員的 virtual 基類。實現該接口的 class 或 struct 必須實現其所有的成員。
  • 從 C# 8.0 開始,接口可以為部分或全部成員定義默認實現。實現該接口的 class 或 struct 不必實現具有默認實現的成員。
  • 接口不能直接實例化。其成員由實現該接口的任何 class 或 struct 來實現。
  • class 或 struct 可以實現多個接口。class 可以繼承基類,同時也可以實現一個或多個接口。

通用 class 和通用 method

泛型為 .NET 引入了類型參數的概念。泛型使得能夠設計出在使用類或方法時才指定一個或多個類型參數的類和方法。例如,通過使用泛型類型參數 T,您可以編寫一個通用類,其他客户端代碼可以使用該類而無需承擔運行時類型轉換或裝箱操作的成本或風險,如下所示:

// 聲明泛型類
public class GenericList < T >
    {
        public void Add ( T item ) { }
    }

public class ExampleClass { }

class TestGenericList
    {
        static void Main ( )
            {
                // 創建一個 int 類型的列表
                GenericList < int > l1 = new ( );
                l1 . Add ( 1 );

                // 創建一個 string 類型的列表
                GenericList < string > l2 = new ( );
                l2 . Add ( "" );

                // 創建一個 ExampleClass 類型的列表
                GenericList < ExampleClass > l3 = new ( );
                l3 . Add ( new ExampleClass ( ) );
        }
    }

泛型類和泛型方法能夠以一種非泛型類和方法無法實現的方式實現可重用性、類型安全性和高效性。在編譯過程中,泛型類型的參數會被替換為類型參數。在上述示例中,編譯器會將 T 替換為 int。泛型最常用於集合及其操作這些集合的方法中。System . Collections . Generic 命名空間包含了幾個基於泛型的集合類。非泛型集合,如 ArrayList,並不推薦使用,只是為了兼容性而保留。

您還可以創建自定義的通用類型和方法,以提供您自己的通用解決方案和設計模式,這些解決方案和模式是類型安全且高效的。以下代碼示例展示了一個簡單的通用鏈表類,僅用於演示目的(在大多數情況下,您應該使用.NET 提供的 List < T > 類,而不是自行創建)。類型參數 T 在多個位置被使用,而在通常情況下會使用具體的類型來表示鏈表中存儲的項的類型:

  • 在 “AddHead” 方法中,作為方法參數的類型。
  • 在嵌套的 “Node” 類中的 “Data” 屬性的返回類型。
  • 作為嵌套類中的 private 成員數據的類型。

T 可用於嵌套的 “Node” 類。當使用具體的類型(例如 “List通用 < int >”)實例化 “List通用 < T >” 時,每個出現的 T 都會被替換為 int。

public class List通用 < T >
    {
        // 這個嵌套類也是具有通用性的,並且包含了一個類型為 T 的數據項
        private class 結點 ( T t )
            {
                // T 作為一種屬性類型
                public T 值 { get; set; } = t;

                public 結點? 下一個
                    {
                        get; set;
                    }
            }

        // 鏈表的首項
        private 結點? 首;

        // T 作為參數類型
        public void FF添加首 ( T t )
            {
                結點 n = new ( t );
                n . 下一個 = 首;
                首 = n;
            }

        // T 在方法的返回類型中
        public IEnumerator < T > GetEnumerator ( )
            {
                結點? 當前 = 首;

                while ( 當前 is not null )
                    {
                        yield return 當前 . 值;
                        當前 = 當前 . 下一個;
                    }
            }
    }

以下代碼示例展示了客户端代碼如何使用通用的 GenericList < T > 類來創建一個整數列表。如果您更改類型參數,上述代碼將創建字符串列表或任何其他自定義類型的列表:

private static void Main(string[] args)
    {
        List通用 < int > list = new ( );

        // 添加十個 int 值
        for ( int x = 0 ; x < 10 ; x++ )
            {
                list . FF添加首 ( x );
            }

        // 輸出到控制枱
        foreach ( int i in list )
            {
                Console . WriteLine ( i );
            }

        Console . WriteLine ( "完成" );
    }

注意:泛型類型並不侷限於 class。前面的示例使用的是 class 類型,但您也可以定義泛型 interface 和 struct 類型,包括 record 類型。

泛型概述

使用泛型類型可最大限度地提高代碼的複用性、類型安全性和性能。

泛型最常見的用途是創建集合類。

.NET 類庫在 System . Collections . Generic 命名空間中包含幾個泛型集合類。在可能的情況下,應優先使用這些泛型集合類,而非 System . Collections 命名空間中的諸如 ArrayList 之類的類。
您可以創建自己的泛型接口、類、方法、事件和委託。

泛型類可以進行約束,以便能夠訪問特定數據類型的特定方法。

您可以使用反射在運行時獲取有關泛型數據類型中使用的類型的信息。

匿名(anonymous)類型

匿名類型提供了一種簡便的方法,可將一組只讀屬性封裝到一個單一對象中,而無需事先明確定義類型。類型名稱由編譯器生成,在源代碼級別不可見。每個屬性的類型由編譯器推斷得出。

您可以通過使用 “new” 運算符並結合對象初始化器來創建匿名類型。

以下示例展示了一個匿名類型,該類型通過兩個名為 “金額” 和 “信息” 的屬性進行初始化。

var v = new
    {
        金額 = 1500 ,
        信息 = "紅裙子"
    };
Console . WriteLine ( v . 金額 + " " + v . 信息 ); // 1500 紅裙子

匿名類型通常在查詢表達式的 “選擇” 子句中使用,用於從源序列中的每個對象中返回一部分屬性。

匿名類型包含一個或多個 public 的 readonly 屬性。不允許存在其他類型的類成員,例如方法或事件。用於初始化屬性的表達式不能為 null、匿名函數或指針類型。

最常見的情況是使用另一個類型的屬性來初始化一個匿名類型。在下面的示例中,假設存在一個名為 “產品” 的類。該類包含 “顏色” 和 “造價” 這兩個屬性,以及您不感興趣的其他屬性:

class 產品
    {
        public string? 顏色 { get; set; }
        public decimal 造價 { get; set; }
        public string? 名稱 { get; set; }
        public string? 種類 { get; set; }
        public string? 大小 { get; set; }
    }

匿名類型聲明以 “new” 關鍵字開頭。該聲明創建了一個新的類型,該類型僅使用了 “產品” 類中的兩個屬性。使用匿名類型會使查詢返回的數據量減少。

如果在匿名類型中未指定成員名稱,編譯器會將匿名類型的成員與用於初始化它們的屬性的名稱相同。對於使用表達式進行初始化的屬性,您需要為其提供名稱,如前面的示例所示。在以下示例中,匿名類型的屬性名稱分別為 “顏色” 和 “造價”。這些實例是從 “產品” 類型的 “產品” 集合中獲取的:

public class LeiMain
    {
        private static void Main ( string [ ] args )
            {
                產品 [ ] CPs= [ new 產品 ( Color . Red , 13.5 , 160 ) , new 產品 ( Color . Black , 17.5 , 268 ) ];
                var CX產品 =
                    from CP in CPs
                    select new
                        {
                            CP . 顏色 ,
                            CP . 造價
                        };
            foreach ( var CP in CX產品 )
                {
                    Console . WriteLine ( $"顏色 = {CP . 顏色},造價 = {CP . 造價}" );
                }
        }

        public class 產品 ( Color? 顏色 , double 大小 , decimal 造價 )
            {
                public string? 顏色 { get; set; } = 顏色 . Value . ToString ( );
                public string? 大小 { get; set; } = 大小 . ToString ( );
                public string? 種類 { get; set; }
                public decimal? 造價 { get; set; } = 造價;
                public string? 名稱 { get; set; }
            }
    }

匿名類型聲明中的投影初始化器

匿名類型支持對象初始化器,它允許您直接使用局部變量或參數,而無需明確指定成員名稱。編譯器會根據變量名稱推斷出成員名稱。以下示例展示了這種簡化的語法:

var r明確 = new
    {
        姓 = "扈" ,
        名 = "十娘"
    };
    var xing = "扈";
    var ming = "十娘";
    var r推導 = new
        {
            xing ,
            ming
        };
    Console . WriteLine ( $"明確:{r明確 . 姓},{r明確 . 名}" );
    Console . WriteLine ( $"推導:{r推導 . xing},{r推導 . ming}" );

這種簡化的語法在創建具有許多屬性的匿名類型時特別有用:

var z標題 = "軟件工程師";
var z部門 = "工程部";
var d薪水 = 75000;

// 使用項目初始化器
var yg = new { z標題 , z部門 , d薪水 };

// 等同於顯式語法:
// var yg = new { 標題 = z標題 , 部門 = z部門 , 薪水 = d薪水 };

Console . WriteLine ( $"標題:{yg . z標題},部門:{yg . z部門},薪水:{yg . d薪水}" );

在以下情況下,不會推斷出成員名稱:

  • 候選名稱是匿名類型中的成員名稱,例如 ToString 或 GetHashCode。
  • 該候選名稱是同一匿名類型中另一個屬性成員的重複項,無論是顯式還是隱式。
  • 該候選名稱不是一個有效的標識符(例如,它包含空格或特殊字符)。

在這些情況下,您必須明確指定成員名稱。

提示:您可以使用.NET 樣式的規則 IDE0037 來強制規定是優先使用推導的成員名稱還是顯式的成員名稱。還可以通過另一種類型的對象(如 class、struct 或甚至是另一個匿名類型)來定義一個字段。其方法是使用保存此對象的變量,就像下面的示例中那樣,其中使用已實例化的用户定義類型創建了兩個匿名類型。在兩種情況下,匿名類型 “shipment” 和 “shipmentWithBonus” 中的 “product” 字段都將為 “Product” 類型,包含每個字段的默認值。而 “bonus” 字段將是由編譯器創建的匿名類型。

var cp = new 產品 ( 顏色: Color . Yellow , 26.5 , 1700 );
var jl = new { 信息 = "你贏了!" };
var shl = new { 地址 = "到處都是" , cp };
var shljl = new { 地址 = "某處" , cp , jl };

通常情況下,當您使用匿名類型來初始化一個變量時,您會通過使用 “var” 來聲明該變量為一個隱式類型的局部變量。在變量聲明中不能指定類型名稱,因為只有編譯器能夠訪問匿名類型的底層名稱。

可以通過將一個隱式類型的局部變量與一個隱式類型的數組相結合,來創建一個匿名類型的元素數組,如下面的示例所示。
var anonArray = new [ ] { new { 名稱 = "蘋果" , 直徑 = 4 } , new { 名稱 = "香蕉" , 直徑 = 2 } };
匿名類型是從 object 類直接派生的 class 類型,並且除了 object 類型之外不能轉換為任何其他類型。編譯器為每個匿名類型提供一個名稱,不過您的應用程序無法訪問該名稱。從公共語言運行庫(CLR)的角度來看,匿名類型與其他任何引用類型沒有區別。

如果程序集中兩個或多個匿名對象初始化器指定了順序相同、名稱相同且類型相同的屬性序列,編譯器將把這些對象視為相同類型的實例。它們共享相同的編譯器生成的類型信息。

匿名類型支持以 “with 表達式” 的形式進行非破壞性修改。這使您能夠創建匿名類型的實例,其中一個或多個屬性具有新值。

var PingGuo = new { 項目 = "蘋果" , 價格 = 1.35 };
var XiaoShou = PingGuo with { 價格 = 0.79 };
Console . WriteLine ( PingGuo );
Console . WriteLine ( XiaoShou );

您不能將字段、屬性、事件或方法的返回類型聲明為具有匿名類型。同樣,您也不能將方法、屬性、構造函數或索引器的形參聲明為具有匿名類型。若要將匿名類型或包含匿名類型的集合作為方法的參數傳遞,可以將參數聲明為 object 類型。但是,對於匿名類型使用 object 類型會違背強類型的目的。如果必須存儲查詢結果或將其傳遞到方法邊界之外,請考慮使用普通的命名結構體或類,而不是匿名類型。

由於匿名類型的 Equals 和 GetHashCode 方法是根據其屬性的 Equals 和 GetHashCode 方法來定義的,因此只有當兩個匿名類型的實例的所有屬性都相等時,這兩個實例才相等。

注意:匿名類型的可訪問性級別為內部級別,因此在不同程序集中定義的兩個匿名類型不是同一種類型。所以,即使所有屬性都相等,在不同程序集中定義的匿名類型實例也不能彼此相等。
匿名類型確實會重寫 ToString 方法,將每個屬性的名稱和 ToString 輸出用花括號括起來並連接起來。

var v = new { 標題 = "Hello" , 年齡 = 24 };

Console . WriteLine ( v . ToString ( ) ); // "{ 標題 = Hello , 年齡 = 24 }"

Add a new 評論

Some HTML is okay.