C# 基礎複習Note(型別系統)
型別系統 概觀
❗ 是強型別語言, 每個變數和常數都有型別 ❗
在 C# bool 中無法轉換成 int
- 儲存在類型中的資訊可以包含下列
- 型別的變數需要的儲存空間
- 它可以代表的最大值和最小值
- 它所包含的成員 (方法、欄位、事件等等)
- 它繼承自的基底型別
interface
(實作)- 允許的作業類型
- 編譯器會將型別資訊視為中繼資料內嵌至可執行檔
- 通用語言執行平台 (CLR) 會在執行階段使用該中繼資料,以在它配置和回收記憶體時,進一步保證型別安全
變數宣告中指定類型
❗ 程式中宣告變數或常數時 必須指定其類型 ❗
也能使用
var
關鍵字來讓編譯器推斷類型
|
|
內建類型
- C# 提供一組標準內建類型
- 整數
- 浮點值
- 布林運算式
- 文字字元
- 十進位值
- string
- object
自訂類型
- 可以使用 結構類型(struct)、 類別(Class) 、
interface
(interface)、列舉 (enum) 和 記錄(record) 建構來建立您自己的自訂類型 - 當明確將專案參考新增至定義這些專案的元件時,其他專案才可用
編譯器在有該組件的參考之後,您可以針對在原始程式碼的那個組件中宣告的型別宣告變數 (或常數)
- .NET 類別庫本身是自訂類型的集合,可以在應用程式中使用
根據預設,類別庫中最常使用的型別可用於任何 C# 程式
一般型別系統(CTS)
-
支援繼承原則
- 型別可以衍生自稱為「基底型別」的其他型別, 而衍生的型別會繼承 (有部份限制) 基底型別的方法、屬性和其他成員
- 基底型別同樣可以衍生自一些其他型別,所衍生的型別會繼承其繼承階層架構中兩個基底型別的成員
- 所有類型 最終衍生自單一基底類型,即 System.Object (C# 關鍵字:object)
- 這種統一型別階層架構稱為一般型別系統 (CTS)
-
一般型別系統 (CTS)中的每個型別都會定義為「實值型別」或「參考型別」
- 包括 .NET 類別庫中的所有自訂類型
- 使用者定義型別
- 使用 結構類型(struct) 關鍵字定義的類型為實值型別,所有內建的數數值型別都是 結構類型(struct)
- 使用 類別(class) 或 記錄(record) 關鍵字定義的類型是參考型別
- 參考型別和實值型別有不同的編譯時期規則和不同的執行階段行為
- C# 9.0 新增 記錄型別
- 資料和行為是類別、結構或記錄 的成員
- 類別、結構或記錄宣告就像是用來在執行時間建立實例或物件的藍圖
|
|
類別是參考型別
- 建立型別的物件時,指派物件的變數只會保留該記憶體的參考
- 當物件參考指派至新的變數時,新的變數會參考到原始物件
- 透過某個變數所做的變更會反映在其他變數中,因為它們都參考相同的資料
結構是實值型別
- 建立結構時,結構指派的變數會保留結構的實際資料
- 當結構指派給新的變數時,就會複製它
- 新的變數和原始變數會各自包含一份相同的資料,對一個複本所做的變更不會影響另一個複本
記錄類型可以是參考型別(record class
)或實值型別(record struct
)
- 類別 是用來建立更複雜的行為模型
- 類別通常會儲存在建立類別物件之後要修改的資料
- 結構 最適合小型資料結構
- 結構通常會儲存在建立結構之後不打算修改的資料
- 記錄類型 是具有其他編譯器合成成員的資料結構
- 記錄通常會儲存在建立物件之後不打算修改的資料
值類型
實值型別衍生自 System.ValueType,該型別又衍生自 System.Object
-
實數值型別變數會直接包含其值
- 結構記憶體會內嵌配置於變數所宣告的任何內容中,實數值型別變數沒有個別的堆積配置或垃圾收集額外負荷
-
實值型別有兩種類別
實值型別為 密封, 無法從任何實值型別衍生型別
|
|
使用 結構類型 來建立自訂實值型別:
|
|
實值型別的另一個類別是 enum
:
|
|
參考型別
其定義為 類別(class), 記錄(record), 委派(delegate), 陣列(array)或 interface
(interface)的類型是參考型別
- 參考型別完全支援繼承
- 當您建立類別時,可以繼承自未定義為 密封的任何其他
interface
或類別 - 其他類別可以繼承自您的類別,並覆寫您的虛擬方法
- 當您建立類別時,可以繼承自未定義為 密封的任何其他
類別的建立和指派 Example:
|
|
interface
(interface) 無法使用new
運算子直接具現化,請建立並指派實作 interface
之類別的實例 Example:
|
|
-
所有陣列都是參考型別,即使其元素都是實值型別
- 陣列會隱含衍生自System.Array 類別,可以宣告並使用 搭配 C# 提供的簡化語法
1 2 3 4 5
// 宣告並初始化一個數字陣列 int[] nums = { 1, 2, 3, 4, 5 }; // 訪問 System.Array 的實例屬性 int len = nums.Length;
泛型類型
類型可以使用一或多個 類型參數 來宣告,做為實際型別的預留位置
建立 類型的實例時,可以指定清單將包含的物件類型,例如 string
:
|
|
- 使用型別參數(<T>)讓您能夠重複使用相同的類別來保存任何元素型別,而不需要將每個元素都轉換成
object
- 泛型集合類別稱為 強型別集合 ,因為編譯器知道集合元素的特定類型
隱含型別、匿名型別和可為 Null 的實值型別
- 隱含型別 : 可以使用
var
隱含輸入區域變數(但不能輸入類別成員),其變數還是會在編譯時期收到型別,但其是由編譯器所提供的型別 - 匿名型別 : 針對不想要在外部方法 儲存或傳遞的簡單相關值集合,建立具名類型可能很不方便,為此,可以建立「匿名型別」
- 可為 Null 的實值型別 : 一般實值型別不能有
null
的值, 在類別後附加?
後,允許建立可為 Null 的實值型別,例如,int?
是一種int
類型,也可以有 值null
- 可為 Null 的實值型別是泛型結構類型的
System.Nullable<T>
實例。 當您將資料傳入資料庫時,可為 Null 的實值型別特別有用,其中數值可能是null
- 可為 Null 的實值型別是泛型結構類型的
編譯時間類型和執行時間類型
-
變數可以有不同的編譯時間和執行時間類型
- 編譯時間類型是原始程式碼中變數的宣告或推斷類型
- 執行時間類型是該變數所參考之實例的類型
-
這兩種類型通常相同,Example:
1
string message = "This is a string of characters";
-
在其他情況下,編譯時間類型不同,Example:
1 2 3 4 5
// 編譯時間類型位於 object, 執行時間類型為 string object anotherMessage = "This is another string of characters"; // 編譯時間類型位於 IEnumerable<char>, 執行時間類型為 string IEnumerable<char> someCharacters ="abcdefghijklmnopqrstuvwxyz";
變數的兩種類型不同,請務必瞭解編譯時間類型和執行時間類型套用的時間,而編譯時間類型會決定編譯器所採取的所有動作
命名空間(宣告命名空間以組織類型)
- C# 程式設計大量使用命名空間的原因有兩個
-
.NET 會使用命名空間來組織其許多類別
1 2
// System 是命名空間,而 Console 是該命名空間中的類別 System.Console.WriteLine("Hello World!");
1 2 3 4 5
// using關鍵字可用來讓完整名稱不需要 using System; Console.WriteLine("Hello World!");
-
宣告您自己的命名空間,將有助於在較大型的程式設計專案中控制類別和方法名稱的範圍
1 2 3 4 5 6 7 8 9 10 11 12
// 使用 namespace 關鍵字宣告命名空間 namespace SampleNamespace { class SampleClass { public void SampleMethod() { System.Console.WriteLine( "SampleMethod inside SampleNamespace"); } } }
命名空間的名稱必須是有效的 C# 識別碼名稱
1 2 3 4 5 6 7 8 9 10 11
// 從 C# 10 開始,您可以針對該檔案中定義的所有類型宣告命名空間 namespace SampleNamespace; class AnotherSampleClass { public void AnotherSampleMethod() { System.Console.WriteLine( "SampleMethod inside SampleNamespace"); } }
-
命名空間概觀
- 命名空間具有下列屬性:
- 命名空間可組織大型程式碼專案
- 它們會使用
.
運算子來分隔 using
指示詞讓其不需要指定每個類別的命名空間名稱global
命名空間是 “root” 命名空間:global::System
一律會參考 .NET System 命名空間
類別
-
參考型別 : 定義為類別(Class)的類型是參考型別
-
執行時間,當宣告參考型別的變數時,該變數會包含值 null ,直到使用 new 運算子明確建立類別的實例,或指派可能已在其他地方建立之相容型別的物件
1 2 3 4 5
//Declaring an object of type MyClass. MyClass mc = new MyClass(); //Declaring another object of the same type, assigning it the value of the first object. MyClass mc2 = mc;
-
-
宣告類別 : 類別是使用 class 關鍵字來宣告,後面接著唯一識別碼
1 2 3 4 5 6 7
// class 關鍵字的前面會加上存取層級(public) // [access modifier] - [class] - [identifier] public class Customer { // 類別上的欄位、屬性、方法和事件統稱為「類別成員」 // Fields, properties, methods and events go here... }
-
建立物件 : 物件是根據類別的具體實體,而且有時稱為類別的執行個體
類別會定義一種類型的物件,但不是物件本身
1 2 3 4 5
// 使用 new 關鍵字來建立物件 // object1 是根據 Customer 之物件的參考 Customer object1 = new Customer(); // 可以建立物件參考,而根本不需要建立物件 Customer object2;
建立物件參考,如未參考上一個物件參考,嘗試透過這類參考來存取物件將會在執行時間失敗
1 2 3
//可以藉由建立新的物件,或為其指派現有的物件,來參考物件 Customer object3 = new Customer(); Customer object4 = object3;
-
類別繼承 : 類別完全支援「繼承」,這是物件導向程式設計的基礎特性
-
建立類別時,可以繼承自任何其他未定義為 sealed 的類別,而其他類別可以繼承自您的類別,並覆寫類別虛擬方法,且可以執行一或多個
interface
-
使用「衍生」可完成繼承,這表示使用從中繼承資料和行為的「基底類別」來宣告類別。 附加冒號以及接著衍生類別名稱後面的基底類別名稱,以指定基底類別
-
類別宣告基底類別時,會繼承基底類別的所有成員,但建構函式除外
1 2 3 4 5
public class Manager : Employee { // Employee fields, properties, methods and events are inherited // New Manager fields, properties, methods and events go here... }
-
可用
abstract
宣告類別- 抽象類別包含具有簽章定義但沒有實作的抽象方法, 無法具現化抽象類別
- 它們僅用於實作抽象方法的衍生類別
- 與
sealed
類別相反,sealed
不允許從它衍生其他類別
-
記錄
C# 中的 記錄 是一種 類別 或 結構 ,可提供使用資料模型的特殊語法和行為
使用記錄的時機
- 您想要定義相依于"值相等“的資料模型
- 您想要定義物件為”不可變“的類型
實值相等
- 對於記錄而言,值相等表示如果類型相符且所有屬性和域值相符,則記錄類型的兩個變數會相等
- 對於其他參考型別(例如類別),相等表示 參考相等
並非所有資料模型都能搭配值相等來運作, 例如 :
Entity Framework Core
取決於參考是否相等,以確保它只針對概念為一個實體的實體類型使用一個實例 , 基於這個理由,記錄類型不適合用來做為Entity Framework Core
中的實體類型
不變性
- 不可變的型別是一種可防止在物件具現化之後,變更該物件的任何屬性或域值
- 需要型別必須是安全線程,或者您是根據雜湊表中剩餘的雜湊碼而定時,永久性可能很有用(
JWTToken
、EnycrptPassword
) - 記錄提供簡潔的語法來建立和使用不可變的類型
- 需要型別必須是安全線程,或者您是根據雜湊表中剩餘的雜湊碼而定時,永久性可能很有用(
永久性並不適用于所有資料案例, 例如 :
Entity Framework Core
不支援使用不可變的實體類型進行更新
記錄與類別和結構有何不同
- 宣告和具現化類別或結構的相同語法可用於記錄
- 只需以關鍵字取代
class
,或使用record struct
取代struct
- 只需以關鍵字取代
record
同樣地,記錄類別也支援用來表示繼承關聯性的相同語法- 記錄與類別的差異如下:
- 您可以使用 位置參數 ,利用不可變的屬性來建立和具現化型別
- 在類別中指出參考相等或不相等的相同方法和運算子 (例如 Object.Equals(Object) 和
==
) ,表示記錄中的 Object.Equals(Object) 不相等可以用
Object.Equals(Object)
檢測是否相等 - 您可以使用 運算式來建立不可變物件的複本,並在選取的屬性中包含新的值
- 記錄的
ToString
方法會建立格式化的字串,以顯示物件的類型名稱以及其所有公用屬性的名稱和值 - 記錄可以 繼承自另一個記錄, 記錄無法繼承自類別,而且類別無法繼承自記錄
- 記錄結構與結構的不同之處在於
- 編譯器合成了相等的方法和 ToString
- 編譯器為位置記錄結構合成 Deconstruct 方法
- 記錄與類別的差異如下:
Example
|
|
|
|
|
|
介面(定義多個類型的行為)
-
interface
包含非抽象 類別(class
) 或 結構(struct
) 必須實作之相關功能群組的定義 -
interface
可以定義 靜態(static
) 必須具有實作的方法從 C# 8.0 開始,
interface
可能會定義成員的預設實作-
可以藉由使用
interface
,在類別中包含多個來源的行為(這項功能在 C# 中是很重要的,因為語言不支援類別的多重繼承) -
如果要模擬結構繼承,則必須使用
interface
,因為它們實際上無法繼承自另一個結構或類別 -
使用
interface
關鍵字來定義interface
:1 2 3 4 5
interface IEquatable<T> { bool Equals(T obj); } // 任何實作 IEquatable<T> `interface`的類別或結構,必須包含 Equals 方法的定義,該方法符合`interface`指定的簽章
-
interface
的名稱必須是有效的 C# 識別碼名稱(依慣例,interface
名稱以大寫字母 I 開頭)- IEquatable<T>的定義沒有提供
Equals
的實作- 類別 或 結構 可以實現多個
interface
,但 類別 只能繼承自 單一類別
-
interface
可以包含實作方法、屬性、事件、索引子,或這四個成員類型的任何組合 -
interface
可能包含靜態建構函式、欄位、常數或運算子C# 11 開始,不是欄位的
interface
成員可能是static abstract
-
interface
不能包含實例欄位、實例建構函式或完成項(interface
是無法被實例化的) -
interface
成員預設為公用,而且可以明確指定協助工具修飾詞,EX:public
、protected
、internal
、private
、protected internal
Orprivate protected
-
成員
private
必須具有預設實作
- 若要實作
interface
成員,實作類別的對應成員必須是公用、非靜態,且具有與interface
成員相同的名稱和簽章- ❗ 當
interface
宣告靜態成員時,實作該interface
的類型也可能宣告具有相同簽章的靜態成員, 這些是宣告成員的型別有所區別且是唯一識別的, 在型別中宣告的靜態成員 不會覆寫interface
中所宣告的靜態成員 ❗
class
/struct
繼承了interface
必須實作該interface
的所有成員,而不需要interface
提供預設實作- 如果基底類別實作
interface
,則衍生自基底類別的任何class
/struct
都會繼承該實作 interface
也能繼承interface
(一或多個)class
/struct
繼承了interface
,而此interface
如有繼承其他interface
,則該class
/struct
必須實作出所有繼承鏈中所有interface
的成員- 該
class
/struct
可能會隱含轉換成衍生interface
或其任何基底interface
class
/struct
可能透過基底類別包含interface
多次,繼承或透過其他interface
繼承的interface
- 該
- 只有在類別將
interface
宣告為類別 (class ClassName : InterfaceName
) 定義的一部分時,類別只能提供interface
實作一次
- 如果基底類別實作
Example
|
|
如果類別實作兩個具有相同簽章成員的介面,則在類別上實作該成員會造成這兩個介面都使用該成員進行實作
|
|
|
|
- 若要根據使用中的介面來呼叫不同的執行,可以明確地執行介面成員,明確的介面實作為僅透過指定介面呼叫的類別成員:
|
|
- 類別成員
IControl.Paint
只能透過IControl
介面取得,ISurface.Paint
只能透過ISurface
取得(這兩種方法都是分開的,而且不會直接在類別上使用)
|
|
若要同時執行這兩個介面,類別必須使用明確的實作為屬性 P 或方法 P (或兩者),以避免編譯器錯誤
|
|
從 c # 8.0開始,可以為介面中所宣告的成員定義實作為
- 如果類別從介面繼承方法執行,則只能透過介面類別型的參考來存取該方法,繼承的成員不會顯示為公用介面的一部分
|
|
|
|
泛型
泛型會將型別參數的概念引進 .NET,讓您能夠設計類別和方法來延遲一或多個型別的規格,直到用戶端程式代碼宣告並具現化類別或方法為止
藉由使用泛型型別參數 T
,您可以撰寫可供其他用戶端程式代碼使用的單一類別,而不會產生執行時間轉換或裝箱作業的成本或風險
|
|
- 泛型類別和方法結合了重複使用性、型別安全和效率
- 泛型最常搭配在其上操作的集合和方法使用
- System.Collections.Generic命名空間包含數個以泛型為基礎的集合類別
- 非泛型集合(例如 ArrayList )不建議使用
建立自訂的泛型型別和方法,簡單的泛型類別
當
GenericArray<T>
以具象類型具現化時 (例如具現化為GenericArray<int>
),所出現的每個T
都會以int
取代
|
|
-
使用泛型 GenericArray 類別並輸出結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
class Tester { static void Main(string[] args) { //declaring an int array MyGenericArray<int> intArray = new MyGenericArray<int>(5); //setting values for (int c = 0; c < 5; c++) { intArray.setItem(c, c*5); } //retrieving the values for (int c = 0; c < 5; c++) { Console.Write(intArray.getItem(c) + " "); } Console.WriteLine(); //declaring a character array MyGenericArray<char> charArray = new MyGenericArray<char>(5); //setting values for (int c = 0; c < 5; c++) { charArray.setItem(c, (char)(c+97)); } //retrieving the values for (int c = 0; c< 5; c++) { Console.Write(charArray.getItem(c) + " "); } Console.WriteLine(); Console.ReadKey(); } } //OutPut: //0 5 10 15 20 //a b c d e
-
泛型方法 Example 2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
class Program { static void Swap<T>(ref T lhs, ref T rhs) { T temp; temp = lhs; lhs = rhs; rhs = temp; } static void Main(string[] args) { int a, b; char c, d; a = 10; b = 20; c = 'I'; d = 'V'; //display values before swap: Console.WriteLine("Int values before calling swap:"); Console.WriteLine("a = {0}, b = {1}", a, b); Console.WriteLine("Char values before calling swap:"); Console.WriteLine("c = {0}, d = {1}", c, d); //call swap Swap<int>(ref a, ref b); Swap<char>(ref c, ref d); //display values after swap: Console.WriteLine("Int values after calling swap:"); Console.WriteLine("a = {0}, b = {1}", a, b); Console.WriteLine("Char values after calling swap:"); Console.WriteLine("c = {0}, d = {1}", c, d); Console.ReadKey(); } } /// OutPut: /// Int values before calling swap: /// a = 10, b = 20 /// Char values before calling swap: /// c = I, d = V /// Int values after calling swap: /// a = 20, b = 10 /// Char values after calling swap: /// c = V, d = I
泛型總覽
- 使用泛型型別以最佳化程式碼重複使用、型別安全和效能
- 泛型的最常見用法是建立集合類別
- .NET 類別庫包含命名空間中 System.Collections.Generic 有數個泛型集合類別,應該盡可能使用泛型集合,而不是命名空間中 System.Collections 的類別 ArrayList
- 可以建立自己的泛型介面、類別、方法、事件和委派
- 泛型類別可限制為允許存取特定資料類型上的方法
- 泛型資料類型中所使用的類型相關資訊,可在執行階段透過反映取得
匿名類型
-
匿名類型提供一個便利的方法,將一組唯讀屬性封裝成一個物件,而不需要事先明確定義類型
- 類型名稱會由編譯器產生,並且無法在原始程式碼層級使用
- 每個屬性的類型會由編譯器推斷
1 2 3 4 5 6
// 以兩個名為 Amount 和 Message 的屬性初始化的匿名類型 var v = new { Amount = 108, Message = "Hello" }; // Rest the mouse pointer over v.Amount and v.Message in the following // statement to verify that their inferred types are int and string. Console.WriteLine(v.Amount + v.Message);
匿名型別通常用於查詢運算式的
select
子句中 ,以從來源序列中的每個物件傳回屬性的子集
-
匿名類型包含一個或多個公用唯讀屬性
- 其他類型的類別成員 (例如方法或事件) 則無效
- 用於初始化屬性的運算式不可以是
null
、匿名函式或指標類型
1 2 3 4 5 6 7 8 9 10
var productQuery = from prod in products select new { prod.Color, prod.Price }; foreach (var v in productQuery) { Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price); }
-
可以使用
var
將變數宣告為隱含型別區域變數, 由於只有編譯器可以存取匿名類型的基本名稱,因此無法在變數宣告中指定類型名稱1 2
// 合併隱含類型區域變數和隱含類型陣列,以建立匿名類型項目的陣列 var anonArray = new[] { new { name = "apple", diam = 4 }, new { name = "grape", diam = 1 }};
-
匿名型別是
class
衍生自object
的型別,而且不能轉換成除了object
以外的任何類型 -
如果組件中有兩個或多個匿名物件初始設定式,指定了順序相同並具有相同名稱和類型的屬性序列,編譯器會將這些物件視為相同類型的執行個體
- 這些物件會共用編譯器產生的相同類型資訊
-
匿名型別以 運算式的形式支援非破壞性變化,這可建立匿名型別的新實例,其中一或多個屬性具有新的值
1 2 3 4
var apple = new { Item = "apples", Price = 1.35 }; var onSale = apple with { Price = 0.79 }; Console.WriteLine(apple); Console.WriteLine(onSale);
-
無法將欄位、屬性、事件或方法的傳回類型,宣告為具有匿名類型
-
無法將方法、屬性、建構函式或索引子的型式參數宣告為具有匿名類型
-
若要傳遞匿名型別或包含匿名型別的集合,做為方法的引數,可以將參數宣告為類型
object
- ❗ 針對匿名型別使用 object 會破壞強型別的目的 ❗
-
如果必須在方法界限外儲存或傳遞查詢結果,請考慮使用一般具名結構或類別來取代匿名類型
-
匿名類型上的 Equals 和 GetHashCode 方法會以屬性的
Equals
和GetHashCode
方法來定義,相同匿名類型的兩個執行個體僅在其所有屬性都相等時,這兩個執行個體才相等