個人手機網(wǎng)站開發(fā)站長工具日本
本文摘錄了C#語法的主要內容,接近20萬字。
所有雞湯的味道都等于馬尿!
如果你相信任何所謂的雞湯文章,智商堪憂。
計算機語言沒有”好不好“之說,騙子才會告訴你哪個語言好,學好任何一本基礎語言(C,C++,C#,Java不含python),其他語言都是一天就可以搞定的。
學習任何東西,第一要知道哪些內容要先學,哪些內容后學。懂的,很容易就像俺一樣的學霸。不懂的,就是學渣。
本文91.48%的內容不適合初學者。想學好編程,最好不要讀這樣的文章。成為一個真正的程序員,應該不看書,多寫能解決問題程序,多寫別人喜歡用的程序,要會搜索,會git。
本文屬于閑了蛋疼抄來的。
01 C# 語言介紹
C# 語言是可以運行于 Windows/Linux與兼容系統(tǒng),適用于?.NET?平臺(免費的跨平臺開源開發(fā)環(huán)境)的最流行與全面的語言。 C# 程序可以在許多不同的設備上運行,從物聯(lián)網(wǎng) (IoT) 設備到云以及介于兩者之間的任何設備。 可為手機、臺式機、筆記本電腦和服務器編寫應用。
C# 是一種跨平臺的通用語言,可以讓開發(fā)人員在編寫高性能代碼時提高工作效率。 C# 是數(shù)百萬開發(fā)人員中最受歡迎的 .NET 語言。 C# 在生態(tài)系統(tǒng)和所有 .NET?工作負載中具有廣泛的支持。 基于面向對象的原則,它融合了其他范例中的許多功能,尤其是函數(shù)編程。 低級功能支持高效方案,無需編寫不安全的代碼。 大多數(shù) .NET 運行時和庫都是用 C# 編寫的,C# 的進步通常會使所有 .NET 開發(fā)人員受益。
01.01 Hello world(看不看都行)
“Hello, World”程序歷來都用于介紹編程語言。 下面展示了此程序的 C# 代碼:
// This line prints "Hello, World"
Console.WriteLine("Hello, World");
以?//
?開頭的行是單行注釋。 C# 單行注釋以?//
?開頭,持續(xù)到當前行的末尾。 C# 還支持多行注釋。 多行注釋以?/*
?開頭,以?*/
?結尾。?System
?命名空間中的?Console
?類的?WriteLine
?方法生成程序的輸出。 此類由標準類庫提供,默認情況下,每個 C# 程序中會自動引用這些庫。
以上示例顯示了“Hello,World”程序的一個窗體,其中使用了頂級語句。 早期版本的 C# 要求在方法中定義程序的入口點。 此格式仍然有效,你將在許多現(xiàn)有 C# 示例中看到它。 你也應該熟悉此窗體,如以下示例所示:
using System;class Hello
{static void Main(){// This line prints "Hello, World" Console.WriteLine("Hello, World");}
}
此版本顯示了在程序中使用的構建基塊。 “Hello, World”程序始于引用?System
?命名空間的?using
?指令。 命名空間提供了一種用于組織 C# 程序和庫的分層方法。 命名空間包含類型和其他命名空間。例如,System
?命名空間包含許多類型(如程序中引用的?Console
?類)和其他許多命名空間(如?IO
?和?Collections
)。 借助引用給定命名空間的?using
?指令,可以非限定的方式使用作為相應命名空間成員的類型。 由于使用?using
?指令,因此程序可以使用?Console.WriteLine
?作為?System.Console.WriteLine
?的簡寫。 在前面的示例中,該命名空間是隱式包含的。
“Hello, World”程序聲明的?Hello
?類只有一個成員,即?Main
?方法。?Main
?方法使用?static
?修飾符進行聲明。 實例方法可以使用關鍵字?this
?引用特定的封閉對象實例,而靜態(tài)方法則可以在不引用特定對象的情況下運行。 按照慣例,當沒有頂級語句時,名為?Main
?的靜態(tài)方法將充當 C# 程序的入口點。
這兩個入口點窗體生成等效的代碼。 使用頂級語句時,編譯器會合成程序入口點的包含類和方法。
提示
本文中的示例幫助你初步了解 C# 代碼。 某些示例可能會顯示你不熟悉的 C# 元素。 當準備好學習 C# 時,請從我們的初學者教程開始,或通過每個部分中的鏈接學習深度知識。 如果你擁有?Java、JavaScript、TypeScript?或?Python?方面的經(jīng)驗,請閱讀我們的提示,其中提供了快速學習 C# 所需的信息。
01.02 熟悉的 C# 功能(可以不讀)
C# 對于初學者而言很容易上手,但同時也為經(jīng)驗豐富的專業(yè)應用程序開發(fā)人員提供了高級功能。 你很快就能提高工作效率。 你可以根據(jù)應用程序的需要學習更專業(yè)的技術。
C# 應用受益于 .NET 運行時的自動內存管理。 C# 應用還可以使用 .NET SDK 提供的豐富運行時庫。 有些組件獨立于平臺,例如文件系統(tǒng)庫、數(shù)據(jù)集合與數(shù)學庫。 還有一些組件特定于單個工作負載,例如 ASP.NET Core Web 庫或 .NET MAUI UI 庫。?NuGet?的豐富開源生態(tài)系統(tǒng)增強了作為運行時一部分的庫。 這些庫提供更多可用的組件。
C# 屬于 C 語言家族。 如果你使用過 C、C++、JavaScript 或 Java,那么也會熟悉?C# 語法。 與 C 語言家族中的所有語言一樣,分號 (;
) 定義語句的結束。 C# 標識符區(qū)分大小寫。 C# 同樣使用大括號({
?和?}
)、控制語句(例如?if
、else
?和?switch
)以及循環(huán)結構(例如?for
?和?while
)。 C# 還具有適用于任何集合類型的?foreach
?語句。
C# 是一種強類型語言。 聲明的每個變量都有一個在編譯時已知的類型。 編譯器或編輯工具會告訴你是否錯誤地使用了該類型。 可以在運行程序之前修復這些錯誤。 以下基礎數(shù)據(jù)類型內置于語言和運行時中:值類型(例如?int
、double
、char
)、引用類型(例如?string
)、數(shù)組和其他集合。 編寫程序時,你會創(chuàng)建自己的類型。 這些類型可以是值的?struct
?類型,也可以是定義面向對象的行為的?class
?類型。 可以將?record
?修飾符添加到?struct
?或?class
?類型,以便編譯器合成用于執(zhí)行相等性比較的代碼。 還可以創(chuàng)建?interface
?定義,用于定義實現(xiàn)該接口的類型必須提供的協(xié)定或一組成員。 還可以定義泛型類型和方法。?泛型使用類型參數(shù)為使用的實際類型提供占位符。
編寫代碼時,可以將函數(shù)(也稱為方法)定義為?struct
?和?class
?類型的成員。 這些方法定義類型的行為。 可以使用不同數(shù)量或類型的參數(shù)來重載方法。 方法可以選擇性地返回一個值。 除了方法之外,C# 類型還可以帶有屬性,即由稱作訪問器的函數(shù)支持的數(shù)據(jù)元素。 C# 類型可以定義事件,從而允許類型向訂閱者通知重要操作。 C# 支持面向對象的技術,例如?class
?類型的繼承和多形性。
C# 應用使用異常來報告和處理錯誤。 如果你使用過 C++ 或 Java,則也會熟悉這種做法。 當無法執(zhí)行預期的操作時,代碼會引發(fā)異常。 其他代碼(無論位于調用堆棧上面的多少個級別)可以選擇性地使用?try
?-?catch
?塊進行恢復。
01.03 獨特的 C# 功能(強烈建議掠過,你看不懂的)
你可能不太熟悉 C# 的某些元素。?語言集成查詢 (LINQ)?提供一種基于模式的通用語法來查詢或轉換任何數(shù)據(jù)集合。 LINQ 統(tǒng)一了查詢內存中集合、結構化數(shù)據(jù)(例如 XML 或 JSON)、數(shù)據(jù)庫存儲,甚至基于云的數(shù)據(jù) API 的語法。 你只需學習一套語法即可搜索和操作數(shù)據(jù),無論其存儲在何處。 以下查詢查找平均學分大于 3.5 的所有學生:
var honorRoll = from student in Studentswhere student.GPA > 3.5select student;
上面的查詢適用于?Students
?表示的許多存儲類型。 它可以是對象的集合、數(shù)據(jù)庫表、云存儲 Blob 或 XML 結構。 相同的查詢語法適用于所有存儲類型。
使用基于任務的異步編程模型,可以編寫看起來像是同步運行的代碼,即使它是異步運行的。 它利用?async
?和?await
?關鍵字來描述異步方法,以及表達式何時進行異步計算。 以下示例等待異步 Web 請求。 異步操作完成后,該方法返回響應的長度:
public static async Task<int> GetPageLengthAsync(string endpoint)
{var client = new HttpClient();var uri = new Uri(endpoint);byte[] content = await client.GetByteArrayAsync(uri);return content.Length;
}
C# 還支持使用?await foreach
?語句來迭代由異步操作支持的集合,例如 GraphQL 分頁 API。 以下示例以塊的形式讀取數(shù)據(jù),并返回一個迭代器,該迭代器提供對每個可用元素的訪問:
public static async IAsyncEnumerable<int> ReadSequence()
{int index = 0;while (index < 100){int[] nextChunk = await GetNextChunk(index);if (nextChunk.Length == 0){yield break;}foreach (var item in nextChunk){yield return item;}index++;}
}
調用方可以使用?await foreach
?語句迭代該集合:
await foreach (var number in ReadSequence())
{Console.WriteLine(number);
}
C# 提供模式匹配。 這些表達式使你能夠檢查數(shù)據(jù)并根據(jù)其特征做出決策。 模式匹配為基于數(shù)據(jù)的控制流提供了極好的語法。 以下代碼演示如何使用模式匹配語法來表達布爾 and、or 和 xor 運算的方法:
public static bool Or(bool left, bool right) =>(left, right) switch{(true, true) => true,(true, false) => true,(false, true) => true,(false, false) => false,};public static bool And(bool left, bool right) =>(left, right) switch{(true, true) => true,(true, false) => false,(false, true) => false,(false, false) => false,};
public static bool Xor(bool left, bool right) =>(left, right) switch{(true, true) => false,(true, false) => true,(false, true) => true,(false, false) => false,};
可以通過對任何值統(tǒng)一使用?_
?來簡化模式匹配表達式。 以下示例演示如何簡化 and 方法:
public static bool ReducedAnd(bool left, bool right) =>(left, right) switch{(true, true) => true,(_, _) => false,};
最后,作為 .NET 生態(tài)系統(tǒng)的一部分,你可以將?Visual Studio?或?Visual Studio Code?與?C# DevKit?配合使用。 這些工具可以全方位地理解 C# 語言,包括你編寫的代碼。 它們還提供調試功能。
01.04?C# 標識符命名規(guī)則和約定
標識符是分配給類型(類、接口、結構、委托或枚舉)、成員、變量或命名空間的名稱。
01.04 命名規(guī)則(熟讀一萬遍,堆碼如有神!)
有效標識符必須遵循以下規(guī)則。 C# 編譯器針對不遵循以下規(guī)則的任何標識符生成錯誤:
- 標識符必須以字母或下劃線 (
_
) 開頭。 - 標識符可以包含 Unicode 字母字符、十進制數(shù)字字符、Unicode 連接字符、Unicode 組合字符或 Unicode 格式字符。 有關 Unicode 類別的詳細信息,請參閱?Unicode 類別數(shù)據(jù)庫。
可以在標識符上使用?@
?前綴來聲明與 C# 關鍵字匹配的標識符。?@
?不是標識符名稱的一部分。 例如,@if
?聲明名為?if
?的標識符。 這些逐字標識符主要用于與使用其他語言聲明的標識符的互操作性。
有關有效標識符的完整定義,請參閱?C# 語言規(guī)范中的標識符一文。
?重要
C# 語言規(guī)范僅允許字母(Lu、Ll、Lt、Lm、Lo 或 Nl)、數(shù)字 (Nd)、連接 (Pc)、組合 (Mn 或 Mc) 和格式 (Cf) 類別。 除此之外的任何內容都會自動使用?_
?替換。 這可能會影響某些 Unicode 字符。
01.04.01 命名約定
除了規(guī)則之外,標識符名稱的約定也在整個 .NET API 中使用。 這些約定為名稱提供一致性,但編譯器不會強制執(zhí)行它們。 可以在項目中使用不同的約定。
按照約定,C# 程序對類型名稱、命名空間和所有公共成員使用?PascalCase
。 此外,dotnet/docs
?團隊使用從?.NET Runtime 團隊的編碼風格中吸收的以下約定:
-
接口名稱以大寫字母?
I
?開頭。 -
屬性類型以單詞?
Attribute
?結尾。 -
枚舉類型對非標記使用單數(shù)名詞,對標記使用復數(shù)名詞。
-
標識符不應包含兩個連續(xù)的下劃線 (
_
) 字符。 這些名稱保留給編譯器生成的標識符。 -
對變量、方法和類使用有意義的描述性名稱。
-
清晰勝于簡潔。。
-
將 PascalCase 用于類名和方法名稱。
-
對方法參數(shù)和局部變量使用駝峰式大小寫。
-
將 PascalCase 用于常量名,包括字段和局部常量。
-
專用實例字段以下劃線 (
_
) 開頭,其余文本為駝峰式大小寫。 -
靜態(tài)字段以?
s_
?開頭。 此約定不是默認的 Visual Studio 行為,也不是框架設計準則的一部分,但在 editorconfig 中可配置。 -
避免在名稱中使用縮寫或首字母縮略詞,但廣為人知和廣泛接受的縮寫除外。
-
使用遵循反向域名表示法的有意義的描述性命名空間。
-
選擇表示程序集主要用途的程序集名稱。
-
避免使用單字母名稱,但簡單循環(huán)計數(shù)器除外。 此外,描述 C# 構造的語法示例通常使用與?C# 語言規(guī)范中使用的約定相匹配的以下單字母名稱。 語法示例是規(guī)則的例外。
- 將?
S
?用于結構,將?C
?用于類。 - 將?
M
?用于方法。 - 將?
v
?用于變量,將?p
?用于參數(shù)。 - 將?
r
?用于?ref
?參數(shù)。
- 將?
?提示
可以使用代碼樣式命名規(guī)則強制實施涉及大寫、前綴、后綴和單詞分隔符的命名約定。
在下面的示例中,與標記為?public
?的元素相關的指導也適用于使用?protected
?和?protected internal
?元素的情況 - 所有這些元素旨在對外部調用方可見。
01.04.02 Pascal 大小寫
在命名?class
、interface
、struct
?或?delegate
?類型時,使用 Pascal 大小寫(“PascalCasing”)。
public class DataService
{
}
C#
public record PhysicalAddress(string Street,string City,string StateOrProvince,string ZipCode);
C#
public struct ValueCoordinate
{
}
C#
public delegate void DelegateType(string message);
命名?interface
?時,使用 pascal 大小寫并在名稱前面加上前綴?I
。 此前綴可以清楚地向使用者表明這是?interface
。
C#
public interface IWorkerQueue
{
}
在命名字段、屬性和事件等類型的?public
?成員時,使用 pascal 大小寫。 此外,對所有方法和本地函數(shù)使用 pascal 大小寫。
C#
public class ExampleEvents
{// A public field, these should be used sparinglypublic bool IsValid;// An init-only propertypublic IWorkerQueue WorkerQueue { get; init; }// An eventpublic event Action EventProcessing;// Methodpublic void StartEventProcessing(){// Local functionstatic int CountQueueItems() => WorkerQueue.Count;// ...}
}
編寫位置記錄時,對參數(shù)使用 pascal 大小寫,因為它們是記錄的公共屬性。
C#
public record PhysicalAddress(string Street,string City,string StateOrProvince,string ZipCode);
有關位置記錄的詳細信息,請參閱屬性定義的位置語法。
01.04.03 駝峰式大小寫
在命名?private
?或?internal
?字段時,使用駝峰式大小寫(“camelCasing”),并對它們添加?_
?作為前綴。 命名局部變量(包括委托類型的實例)時,請使用駝峰式大小寫。
C#
public class DataService
{private IWorkerQueue _workerQueue;
}
?提示
在支持語句完成的 IDE 中編輯遵循這些命名約定的 C# 代碼時,鍵入?_
?將顯示所有對象范圍的成員。
使用為?private
?或?internal
?的static
?字段時 請使用?s_
?前綴,對于線程靜態(tài),請使用?t_
。
C#
public class DataService
{private static IWorkerQueue s_workerQueue;[ThreadStatic]private static TimeSpan t_timeSpan;
}
編寫方法參數(shù)時,請使用駝峰式大小寫。
C#
public T SomeMethod<T>(int someNumber, bool isValid)
{
}
有關 C# 命名約定的詳細信息,請參閱?.NET Runtime 團隊的編碼樣式。
01.04.04 類型參數(shù)命名指南
以下準則適用于泛型類型參數(shù)上的類型參數(shù)。 類型參數(shù)是泛型類型或泛型方法中參數(shù)的占位符。 可以在 C# 編程指南中詳細了解?泛型類型參數(shù)。
-
請使用描述性名稱命名泛型類型參數(shù),除非單個字母名稱完全具有自我說明性且描述性名稱不會增加任何作用。
./snippets/coding-conventions復制
public interface ISessionChannel<TSession> { /*...*/ } public delegate TOutput Converter<TInput, TOutput>(TInput from); public class List<T> { /*...*/ }
-
對具有單個字母類型參數(shù)的類型,考慮使用?
T
?作為類型參數(shù)名稱。./snippets/coding-conventions復制
public int IComparer<T>() { return 0; } public delegate bool Predicate<T>(T item); public struct Nullable<T> where T : struct { /*...*/ }
-
在類型參數(shù)描述性名稱前添加前綴 "T"。
./snippets/coding-conventions復制
public interface ISessionChannel<TSession> {TSession Session { get; } }
-
請考慮在參數(shù)名稱中指示出類型參數(shù)的約束。 例如,約束為?
ISession
?的參數(shù)可命名為?TSession
。
可以使用代碼分析規(guī)則?CA1715?確保恰當?shù)孛愋蛥?shù)。
01.04.05 額外的命名約定
-
在不包括?using 指令的示例中,使用命名空間限定。 如果你知道命名空間默認導入項目中,則不必完全限定來自該命名空間的名稱。 如果對于單行來說過長,則可以在點 (.) 后中斷限定名稱,如下面的示例所示。
C#
var currentPerformanceCounterCategory = new System.Diagnostics.PerformanceCounterCategory();
-
你不必更改使用 Visual Studio 設計器工具創(chuàng)建的對象的名稱以使它們適合其他準則。
01.05 常見 C# 代碼約定
編碼約定對于在開發(fā)團隊中維護代碼可讀性、一致性和協(xié)作至關重要。 遵循行業(yè)實踐和既定準則的代碼更易于理解、維護和擴展。 大多數(shù)項目通過代碼約定強制要求樣式一致。?dotnet/docs?和?dotnet/samples?項目并不例外。 在本系列文章中,你將了解我們的編碼約定和用于強制實施這些約定的工具。 你可以按原樣采用我們的約定,或修改它們以滿足團隊的需求。
我們對約定的選擇基于以下目標:
- 正確性:我們的示例將會復制并粘貼到你的應用程序中。 我們希望如此,因此我們需要代碼具有復原能力且正確無誤,即使在多次編輯之后也是如此。
- 教學:示例的目的是教授 .NET 和 C# 的全部內容。 因此,我們不會對任何語言功能或 API 施加限制。 相反,這些示例會告知某個功能在何時會是良好的選擇。
- 一致性:讀者期望我們的內容提供一致的體驗。 所有示例應遵循相同的樣式。
- 采用:我們積極更新示例以使用新的語言功能。 這種做法提高了對新功能的認識,并且提高了所有 C# 開發(fā)人員對這些功能的熟悉程度。
?重要
Microsoft 會使用這些準則來開發(fā)示例和文檔。 它們摘自?.NET 運行時、C# 編碼樣式和?C# 編譯器 (roslyn)?準則。 我們選擇這些準則是因為它們已經(jīng)經(jīng)過了多年開放源代碼開發(fā)的測試。 他們幫助社區(qū)成員參與運行時和編譯器項目。 它們是常見 C# 約定的示例,而不是權威列表(有關此內容,請參閱框架設計指南)。
教學和采用目標是文檔編碼約定不同于運行時和編譯器約定的原因。 運行時和編譯器對熱路徑具有嚴格的性能指標。 許多其他應用程序則并非如此。 我們的教學目標要求我們不會禁止任何構造。 相反,示例顯示了何時應使用構造。 與大多數(shù)生產(chǎn)應用程序相比,我們在更新示例方面更加積極。 我們的采用目標要求我們顯示你目前應該編寫的代碼,即使去年編寫的代碼無需更改。
本文將對我們的準則進行說明。 這些準則已隨時間推移發(fā)生變化,因此,你會發(fā)現(xiàn)并不遵循準則的示例。 我們歡迎推動這些示例合規(guī)的 PR,或促使我們關注應更新的示例的問題。 我們的準則是開放源代碼的,因此我們歡迎 PR 和問題。 但如果你的提交將更改這些建議,請先提出一個問題以供討論。 歡迎使用我們的準則,或根據(jù)你的需求對其進行調整。
01.05.01 工具和分析器(非初學者的菜!)
工具可幫助團隊強制實施約定。 可以啟用代碼分析來強制實施你偏好的規(guī)則。 還可以創(chuàng)建?editorconfig,以便 Visual Studio 可自動強制實施樣式準則。 作為起點,可以復制?dotnet/docs 存儲庫的文件以使用我們的樣式。
借助這些工具,團隊可以更輕松地采用首選的準則。 Visual Studio 將在范圍中的所有?.editorconfig
?文件中應用規(guī)則,以設置代碼的格式。 可以使用多個配置來強制實施企業(yè)范圍的約定、團隊約定,甚至精細的項目約定。
啟用的規(guī)則被違反時,代碼分析會生成警告和診斷。 可以配置想要應用于項目的規(guī)則。 然后,每個 CI 生成會在違反任何規(guī)則時通知開發(fā)人員。
01.05.02 語言準則
以下部分介紹了 .NET 文檔團隊在準備代碼示例和示例時遵循的做法。 一般情況下,請遵循以下做法:
- 盡可能利用新式語言功能和 C# 版本。
- 避免陳舊或過時的語言構造。
- 僅捕獲可以正確處理的異常;避免捕獲泛型異常。
- 使用特定的異常類型提供有意義的錯誤消息。
- 使用 LINQ 查詢和方法進行集合操作,以提高代碼可讀性。
- 將異步編程與異步和等待用于 I/O 綁定操作。
- 請謹慎處理死鎖,并在適當時使用?Task.ConfigureAwait。
- 對數(shù)據(jù)類型而不是運行時類型使用語言關鍵字。 例如,使用?
string
?而不是?System.String,或使用?int
?而不是?System.Int32。 - 使用?
int
?而不是無符號類型。?int
?的使用在整個 C# 中很常見,并且當你使用?int
?時,更易于與其他庫交互。 特定于無符號數(shù)據(jù)類型的文檔例外。。 - 僅當讀者可以從表達式推斷類型時使用?
var
。 讀者可在文檔平臺上查看我們的示例。 它們沒有懸?;蝻@示變量類型的工具提示。 - 以簡潔明晰的方式編寫代碼。
- 避免過于復雜和費解的代碼邏輯。
遵循更具體的準則。
01.05.03 字符串數(shù)據(jù)
-
使用字符串內插來連接短字符串,如下面的代碼所示。
C#
string displayName = $"{nameList[n].LastName}, {nameList[n].FirstName}";
-
若要在循環(huán)中追加字符串,尤其是在使用大量文本時,請使用?System.Text.StringBuilder?對象。
C#
var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala"; var manyPhrases = new StringBuilder(); for (var i = 0; i < 10000; i++) {manyPhrases.Append(phrase); } //Console.WriteLine("tra" + manyPhrases);
01.05.04 數(shù)組
- 當在聲明行上初始化數(shù)組時,請使用簡潔的語法。 在以下示例中,不能使用?
var
?替代?string[]
。
C#
string[] vowels1 = { "a", "e", "i", "o", "u" };
- 如果使用顯式實例化,則可以使用?
var
。
C#
var vowels2 = new string[] { "a", "e", "i", "o", "u" };
01.05.05 委托(非初學者的菜!)
- 使用?Func<>?和?Action<>,而不是定義委托類型。 在類中,定義委托方法。
C#
Action<string> actionExample1 = x => Console.WriteLine($"x is: {x}");Action<string, string> actionExample2 = (x, y) =>Console.WriteLine($"x is: {x}, y is {y}");Func<string, int> funcExample1 = x => Convert.ToInt32(x);Func<int, int, int> funcExample2 = (x, y) => x + y;
- 使用?
Func<>
?或?Action<>
?委托定義的簽名來調用方法。
C#
actionExample1("string for x");actionExample2("string for x", "string for y");Console.WriteLine($"The value is {funcExample1("1")}");Console.WriteLine($"The sum is {funcExample2(1, 2)}");
-
如果創(chuàng)建委托類型的實例,請使用簡潔的語法。 在類中,定義委托類型和具有匹配簽名的方法。
C#
public delegate void Del(string message);public static void DelMethod(string str) {Console.WriteLine("DelMethod argument: {0}", str); }
-
創(chuàng)建委托類型的實例,然后調用該實例。 以下聲明顯示了緊縮的語法。
C#
Del exampleDel2 = DelMethod; exampleDel2("Hey");
-
以下聲明使用了完整的語法。
C#
Del exampleDel1 = new Del(DelMethod); exampleDel1("Hey");
01.05.06?try-catch
?和?using
?語句正在異常處理中
-
對大多數(shù)異常處理使用?try-catch?語句。
C#
static double ComputeDistance(double x1, double y1, double x2, double y2) {try{return Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));}catch (System.ArithmeticException ex){Console.WriteLine($"Arithmetic overflow or underflow: {ex}");throw;} }
-
通過使用 C#?using 語句簡化你的代碼。 如果具有?try-finally?語句(該語句中?
finally
?塊的唯一代碼是對?Dispose?方法的調用),請使用?using
?語句代替。在以下示例中,
try-finally
?語句僅在?finally
?塊中調用?Dispose
。C#
Font bodyStyle = new Font("Arial", 10.0f); try {byte charset = bodyStyle.GdiCharSet; } finally {if (bodyStyle != null){((IDisposable)bodyStyle).Dispose();} }
可以使用?
using
?語句執(zhí)行相同的操作。C#
using (Font arial = new Font("Arial", 10.0f)) {byte charset2 = arial.GdiCharSet; }
使用不需要大括號的新?using?語法:
C#
using Font normalStyle = new Font("Arial", 10.0f); byte charset3 = normalStyle.GdiCharSet;
01.05.07?&&
?和?||
?運算符
-
在執(zhí)行比較時,使用?&&?而不是?&,使用?||?而不是?|,如以下示例所示。
C#
Console.Write("Enter a dividend: "); int dividend = Convert.ToInt32(Console.ReadLine());Console.Write("Enter a divisor: "); int divisor = Convert.ToInt32(Console.ReadLine());if ((divisor != 0) && (dividend / divisor) is var result) {Console.WriteLine("Quotient: {0}", result); } else {Console.WriteLine("Attempted division by 0 ends up here."); }
如果除數(shù)為 0,則?if
?語句中的第二個子句將導致運行時錯誤。 但是,當?shù)谝粋€表達式為 false 時,&& 運算符將發(fā)生短路。 也就是說,它并不評估第二個表達式。 如果?divisor
?為 0,則 & 運算符將同時計算這兩個表達式,從而導致運行時錯誤。
01.05.08?new
?運算符
-
使用對象實例化的簡潔形式之一,如以下聲明中所示。
C#
var firstExample = new ExampleClass();
C#
ExampleClass instance2 = new();
前面的聲明等效于下面的聲明。
C#
ExampleClass secondExample = new ExampleClass();
-
使用對象初始值設定項簡化對象創(chuàng)建,如以下示例中所示。
C#
var thirdExample = new ExampleClass { Name = "Desktop", ID = 37414,Location = "Redmond", Age = 2.3 };
下面的示例設置了與前面的示例相同的屬性,但未使用初始值設定項。
C#
var fourthExample = new ExampleClass(); fourthExample.Name = "Desktop"; fourthExample.ID = 37414; fourthExample.Location = "Redmond"; fourthExample.Age = 2.3;
01.05.09 事件處理
- 使用 lambda 表達式定義稍后無需移除的事件處理程序:
C#
public Form2()
{this.Click += (s, e) =>{MessageBox.Show(((MouseEventArgs)e).Location.ToString());};
}
Lambda 表達式縮短了以下傳統(tǒng)定義。
C#
public Form1()
{this.Click += new EventHandler(Form1_Click);
}void Form1_Click(object? sender, EventArgs e)
{MessageBox.Show(((MouseEventArgs)e).Location.ToString());
}
01.05.10 靜態(tài)成員
使用類名調用?static?成員:ClassName.StaticMember。 這種做法通過明確靜態(tài)訪問使代碼更易于閱讀。 請勿使用派生類的名稱來限定基類中定義的靜態(tài)成員。 編譯該代碼時,代碼可讀性具有誤導性,如果向派生類添加具有相同名稱的靜態(tài)成員,代碼可能會被破壞。
01.05.11 LINQ 查詢
-
對查詢變量使用有意義的名稱。 下面的示例為位于西雅圖的客戶使用?
seattleCustomers
。C#
var seattleCustomers = from customer in customerswhere customer.City == "Seattle"select customer.Name;
-
使用別名確保匿名類型的屬性名稱都使用 Pascal 大小寫格式正確大寫。
C#
var localDistributors =from customer in customersjoin distributor in distributors on customer.City equals distributor.Cityselect new { Customer = customer, Distributor = distributor };
-
如果結果中的屬性名稱模棱兩可,請對屬性重命名。 例如,如果你的查詢返回客戶名稱和分銷商 ID,而不是在結果中將它們保留為?
Name
?和?ID
,請對它們進行重命名以明確?Name
?是客戶的名稱,ID
?是分銷商的 ID。C#
var localDistributors2 =from customer in customersjoin distributor in distributors on customer.City equals distributor.Cityselect new { CustomerName = customer.Name, DistributorID = distributor.ID };
-
在查詢變量和范圍變量的聲明中使用隱式類型化。 有關 LINQ 查詢中隱式類型的本指導會替代適用于隱式類型本地變量的一般規(guī)則。 LINQ 查詢通常使用創(chuàng)建匿名類型的投影。 其他查詢表達式使用嵌套泛型類型創(chuàng)建結果。 隱式類型變量通常更具可讀性。
C#
var seattleCustomers = from customer in customerswhere customer.City == "Seattle"select customer.Name;
-
對齊?from?子句下的查詢子句,如上面的示例所示。
-
在其他查詢子句前面使用?where?子句,確保后面的查詢子句作用于經(jīng)過縮減和篩選的一組數(shù)據(jù)。
C#
var seattleCustomers2 = from customer in customerswhere customer.City == "Seattle"orderby customer.Nameselect customer;
-
使用多行?
from
?子句代替?join?子句來訪問內部集合。 例如,Student
?對象的集合可能包含測驗分數(shù)的集合。 當執(zhí)行以下查詢時,它返回高于 90 的分數(shù),并返回得到該分數(shù)的學生的姓氏。C#
var scoreQuery = from student in studentsfrom score in student.Scores!where score > 90select new { Last = student.LastName, score };
01.05.12 隱式類型本地變量
-
當變量的類型在賦值右側比較明顯時,對局部變量使用隱式類型。
C#
var message = "This is clearly a string."; var currentTemperature = 27;
-
當類型在賦值右側不明顯時,請勿使用?var。 請勿假設類型明顯來自方法名稱。 如果變量類型是?
new
?運算符、對文本值的顯式強制轉換或賦值,則將其視為明確的變量類型。C#
int numberOfIterations = Convert.ToInt32(Console.ReadLine()); int currentMaximum = ExampleClass.ResultSoFar();
-
不要使用變量名稱指定變量的類型。 它可能不正確。 請改用類型來指定類型,并使用變量名稱來指示變量的語義信息。 以下示例應對類型使用?
string
,并使用類似?iterations
?的內容指示從控制臺讀取的信息的含義。C#
var inputInt = Console.ReadLine(); Console.WriteLine(inputInt);
-
避免使用?
var
?來代替?dynamic。 如果想要進行運行時類型推理,請使用?dynamic
。 有關詳細信息,請參閱使用類型 dynamic(C# 編程指南)。 -
在?for?循環(huán)中對循環(huán)變量使用隱式類型。
下面的示例在?
for
?語句中使用隱式類型化。C#
var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala"; var manyPhrases = new StringBuilder(); for (var i = 0; i < 10000; i++) {manyPhrases.Append(phrase); } //Console.WriteLine("tra" + manyPhrases);
-
不要使用隱式類型化來確定?foreach?循環(huán)中循環(huán)變量的類型。 在大多數(shù)情況下,集合中的元素類型并不明顯。 不應僅依靠集合的名稱來推斷其元素的類型。
下面的示例在?
foreach
?語句中使用顯式類型化。C#
foreach (char ch in laugh) {if (ch == 'h'){Console.Write("H");}else{Console.Write(ch);} } Console.WriteLine();
-
對 LINQ 查詢中的結果序列使用隱式類型。 關于?LINQ?的部分說明了許多 LINQ 查詢會導致必須使用隱式類型的匿名類型。 其他查詢則會產(chǎn)生嵌套泛型類型,其中?
var
?的可讀性更高。?備注
注意不要意外更改可迭代集合的元素類型。 例如,在?
foreach
?語句中從?System.Linq.IQueryable?切換到?System.Collections.IEnumerable?很容易,這會更改查詢的執(zhí)行。
我們的一些示例解釋了表達式的自然類型。 這些示例必須使用?var
,以便編譯器選取自然類型。 即使這些示例不太明顯,但示例必須使用?var
。 文本應解釋該行為。
01.05.13 將 using 指令放在命名空間聲明之外
當?using
?指令位于命名空間聲明之外時,該導入的命名空間是其完全限定的名稱。 完全限定的名稱更加清晰。 如果?using
?指令位于命名空間內部,則它可以是相對于該命名空間的,也可以是它的完全限定名稱。
using Azure;namespace CoolStuff.AwesomeFeature
{public class Awesome{public void Stuff(){WaitUntil wait = WaitUntil.Completed;// ...}}
}
假設存在對?WaitUntil?類的引用(直接或間接)。
現(xiàn)在,讓我們稍作改動:
namespace CoolStuff.AwesomeFeature
{using Azure;public class Awesome{public void Stuff(){WaitUntil wait = WaitUntil.Completed;// ...}}
}
今天的編譯成功了。 明天的也沒問題。 但在下周的某個時候,前面(未改動)的代碼失敗,并出現(xiàn)兩個錯誤:
- error CS0246: The type or namespace name 'WaitUntil' could not be found (are you missing a using directive or an assembly reference?)
- error CS0103: The name 'WaitUntil' does not exist in the current context
其中一個依賴項已在命名空間中引入了此類,然后以?.Azure
?結尾:
namespace CoolStuff.Azure
{public class SecretsManagement{public string FetchFromKeyVault(string vaultId, string secretId) { return null; }}
}
放置在命名空間中的?using
?指令與上下文相關,使名稱解析復雜化。 在此示例中,它是它找到的第一個命名空間。
CoolStuff.AwesomeFeature.Azure
CoolStuff.Azure
Azure
添加匹配?CoolStuff.Azure
?或?CoolStuff.AwesomeFeature.Azure
?的新命名空間將在全局?Azure
?命名空間前匹配。 可以通過向?using
?聲明添加?global::
?修飾符來解決此問題。 但是,改為將?using
?聲明放在命名空間之外更容易。
namespace CoolStuff.AwesomeFeature
{using global::Azure;public class Awesome{public void Stuff(){WaitUntil wait = WaitUntil.Completed;// ...}}
}
01.06 編碼樣式指南
一般情況下,對代碼示例使用以下格式:
- 使用四個空格縮進。 不要使用選項卡。
- 一致地對齊代碼以提高可讀性。
- 將行限制為 65 個字符,以增強文檔上的代碼可讀性,尤其是在移動屏幕上。
- 將長語句分解為多行以提高清晰度。
- 對大括號使用“Allman”樣式:左和右大括號另起一行。 大括號與當前縮進級別對齊。
- 如有必要,應在二進制運算符之前換行。
01.06.01 注釋樣式
-
使用單行注釋(
//
)以進行簡要說明。 -
避免使用多行注釋(
/* */
)來進行較長的解釋。
代碼示例中的注釋未本地化。 這意味著不會翻譯代碼中嵌入的說明。 較長的解釋性文本應放在配套文章中,以便對其進行本地化。 -
若要描述方法、類、字段和所有公共成員,請使用?XML 注釋。
-
將注釋放在單獨的行上,而非代碼行的末尾。
-
以大寫字母開始注釋文本。
-
以句點結束注釋文本。
-
在注釋分隔符 (
//
) 與注釋文本之間插入一個空格,如下面的示例所示。C#復制
// The following declaration creates a query. It does not run // the query.
01.06.02 布局約定
好的布局利用格式設置來強調代碼的結構并使代碼更便于閱讀。 Microsoft 示例和樣本符合以下約定:
-
使用默認的代碼編輯器設置(智能縮進、4 字符縮進、制表符保存為空格)。 有關詳細信息,請參閱選項、文本編輯器、C#、格式設置。
-
每行只寫一條語句。
-
每行只寫一個聲明。
-
如果連續(xù)行未自動縮進,請將它們縮進一個制表符位(四個空格)。
-
在方法定義與屬性定義之間添加至少一個空白行。
-
使用括號突出表達式中的子句,如下面的代碼所示。
if ((startX > endX) && (startX > previousX)) {// Take appropriate action. }
例外情況出現(xiàn)在示例解釋運算符或表達式優(yōu)先級時。
02?C# 程序的通用結構
C# 程序由一個或多個文件組成。 每個文件均包含零個或多個命名空間。 一個命名空間包含類、結構、接口、枚舉、委托等類型或其他命名空間。 以下示例是包含所有這些元素的 C# 程序主干。
// A skeleton of a C# program
using System;// Your program starts here:
Console.WriteLine("Hello world!");namespace YourNamespace
{class YourClass{}struct YourStruct{}interface IYourInterface{}delegate int YourDelegate();enum YourEnum{}namespace YourNestedNamespace{struct YourStruct{}}
}
前面的示例使用頂級語句作為程序的入口點。 C# 9 中添加了此功能。 在 C# 9 之前,入口點是名為?Main
?的靜態(tài)方法,如以下示例所示:
// A skeleton of a C# program
using System;
namespace YourNamespace
{class YourClass{}struct YourStruct{}interface IYourInterface{}delegate int YourDelegate();enum YourEnum{}namespace YourNestedNamespace{struct YourStruct{}}class Program{static void Main(string[] args){//Your program starts here...Console.WriteLine("Hello world!");}}
}
02.01 Main() 和命令行參數(shù)(掃一眼即可!)
Main
?方法是 C# 應用程序的入口點。?Main
?方法是應用程序啟動后調用的第一個方法。
C# 程序中只能有一個入口點。 如果多個類包含?Main
?方法,必須使用 StartupObject 編譯器選項來編譯程序,以指定將哪個?Main
?方法用作入口點。 有關詳細信息,請參閱?StartupObject(C# 編譯器選項)。
class TestClass
{static void Main(string[] args){// Display the number of command line arguments.Console.WriteLine(args.Length);}
}
還可以在一個文件中使用頂級語句作為應用程序的入口點。 與?Main
?方法一樣,頂級語句還可以返回值和訪問命令行參數(shù)。 有關詳細信息,請參閱頂級語句。
using System.Text;StringBuilder builder = new();
builder.AppendLine("The following arguments are passed:");// Display the command line arguments using the args variable.
foreach (var arg in args)
{builder.AppendLine($"Argument={arg}");
}Console.WriteLine(builder.ToString());// Return a success code.
return 0;
02.02 Main概述(掃兩眼即可!)
Main
?方法是可執(zhí)行程序的入口點,也是程序控制開始和結束的位置。Main
?必須在類或結構中進行聲明。 封閉?class
?可以是?static
。Main
?必須為?static。Main
?可以具有任何訪問修飾符(file
?除外)。Main
?的返回類型可以是?void
、int
、Task
?或?Task<int>
。- 當且僅當?
Main
?返回?Task
?或?Task<int>
?時,Main
?的聲明可包括?async?修飾符。 這明確排除了?async void Main
?方法。 - 使用或不使用包含命令行自變量的?
string[]
?參數(shù)聲明?Main
?方法都行。 使用 Visual Studio 創(chuàng)建 Windows 應用程序時,可以手動添加此形參,也可以使用?GetCommandLineArgs()?方法來獲取命令行實參。 參數(shù)被讀取為從零開始編制索引的命令行自變量。 與 C 和 C++ 不同,程序的名稱不被視為?args
?數(shù)組中的第一個命令行實參,但它是?GetCommandLineArgs()?方法中的第一個元素。
以下列表顯示了最常見的?Main
?聲明:
static void Main() { }
static int Main() { }
static void Main(string[] args) { }
static int Main(string[] args) { }
static async Task Main() { }
static async Task<int> Main() { }
static async Task Main(string[] args) { }
static async Task<int> Main(string[] args) { }
前面的示例未指定訪問修飾符,因此默認為隱式?private
。 這是典型的,但可以指定任何顯式訪問修飾符。
提示
添加?async
、Task
?和?Task<int>
?返回類型可簡化控制臺應用程序需要啟動時的程序代碼,以及?Main
?中的?await
?異步操作。
02.03 Main() 返回值(沒什么用!)
可以通過以下方式之一定義方法,以從?Main
?方法返回?int
:
Main ?聲明 | Main ?方法代碼 |
---|---|
static int Main() | 不使用?args ?或?await |
static int Main(string[] args) | 使用?args ,不使用?await |
static async Task<int> Main() | 不使用?args ,使用?await |
static async Task<int> Main(string[] args) | 使用?args ?和?await |
如果不使用?Main
?的返回值,則返回?void
?或?Task
?可使代碼變得略微簡單。
展開表
Main ?聲明 | Main ?方法代碼 |
---|---|
static void Main() | 不使用?args ?或?await |
static void Main(string[] args) | 使用?args ,不使用?await |
static async Task Main() | 不使用?args ,使用?await |
static async Task Main(string[] args) | 使用?args ?和?await |
但是,返回?int
?或?Task<int>
?可使程序將狀態(tài)信息傳遞給調用可執(zhí)行文件的其他程序或腳本。
下面的示例演示了如何訪問進程的退出代碼。
此示例使用?.NET Core?命令行工具。 如果不熟悉 .NET Core 命令行工具,可通過本入門文章進行了解。
通過運行?dotnet new console
?創(chuàng)建新的應用程序。 修改 Program.cs 中的?Main
?方法,如下所示:
// Save this program as MainReturnValTest.cs.
class MainReturnValTest
{static int Main(){//...return 0;}
}
在 Windows 中執(zhí)行程序時,從?Main
?函數(shù)返回的任何值都存儲在環(huán)境變量中。 可使用批處理文件中的?ERRORLEVEL
?或 PowerShell 中的?$LastExitCode
?來檢索此環(huán)境變量。
可使用?dotnet CLI?dotnet build
?命令構建應用程序。
接下來,創(chuàng)建一個 PowerShell 腳本來運行應用程序并顯示結果。 將以下代碼粘貼到文本文件中,并在包含該項目的文件夾中將其另存為?test.ps1
。 可通過在 PowerShell 提示符下鍵入?test.ps1
?來運行 PowerShell 腳本。
因為代碼返回零,所以批處理文件將報告成功。 但是,如果將 MainReturnValTest.cs 更改為返回非零值,然后重新編譯程序,則 PowerShell 腳本的后續(xù)執(zhí)行將報告為失敗。
dotnet run
if ($LastExitCode -eq 0) {Write-Host "Execution succeeded"
} else
{Write-Host "Execution Failed"
}
Write-Host "Return value = " $LastExitCode
輸出
Execution succeeded
Return value = 0
02.04 Async Main 返回值(不看!別看!)
聲明?Main
?的?async
?返回值時,編譯器會生成樣本代碼,用于調用?Main
?中的異步方法。 如果未指定?async
?關鍵字,則需要自行編寫該代碼,如以下示例所示。 示例中的代碼可確保程序一直運行,直到異步操作完成:
class AsyncMainReturnValTest
{public static int Main(){return AsyncConsoleWork().GetAwaiter().GetResult();}private static async Task<int> AsyncConsoleWork(){// Main body herereturn 0;}
}
該樣本代碼可替換為:
class Program
{static async Task<int> Main(string[] args){return await AsyncConsoleWork();}private static async Task<int> AsyncConsoleWork(){// main body here return 0;}
}
將?Main
?聲明為?async
?的優(yōu)點是,編譯器始終生成正確的代碼。
當應用程序入口點返回?Task
?或?Task<int>
?時,編譯器生成一個新的入口點,該入口點調用應用程序代碼中聲明的入口點方法。 假設此入口點名為?$GeneratedMain
,編譯器將為這些入口點生成以下代碼:
static Task Main()
?導致編譯器發(fā)出?private static void $GeneratedMain() => Main().GetAwaiter().GetResult();
?的等效項static Task Main(string[])
?導致編譯器發(fā)出?private static void $GeneratedMain(string[] args) => Main(args).GetAwaiter().GetResult();
?的等效項static Task<int> Main()
?導致編譯器發(fā)出?private static int $GeneratedMain() => Main().GetAwaiter().GetResult();
?的等效項static Task<int> Main(string[])
?導致編譯器發(fā)出?private static int $GeneratedMain(string[] args) => Main(args).GetAwaiter().GetResult();
?的等效項
?備注
如果示例在?Main
?方法上使用?async
?修飾符,則編譯器將生成相同的代碼。
02.05 命令行自變量(快速粗看,走驢看糞!)
可以通過以下方式之一定義方法來將自變量發(fā)送到?Main
?方法:
Main ?聲明 | Main ?方法代碼 |
---|---|
static void Main(string[] args) | 無返回值,不使用?await |
static int Main(string[] args) | 返回值,不使用?await |
static async Task Main(string[] args) | 無返回值,使用?await |
static async Task<int> Main(string[] args) | 返回值,使用?await |
如果不使用參數(shù),可以從方法聲明中省略?args
,使代碼更為簡單:
Main ?聲明 | Main ?方法代碼 |
---|---|
static void Main() | 無返回值,不使用?await |
static int Main() | 返回值,不使用?await |
static async Task Main() | 無返回值,使用?await |
static async Task<int> Main() | 返回值,使用?await |
備注
還可使用?Environment.CommandLine?或?Environment.GetCommandLineArgs?從控制臺或 Windows 窗體應用程序的任意位置訪問命令行參數(shù)。 若要在 Windows 窗體應用程序的?Main
?方法中啟用命令行參數(shù),必須手動修改?Main
?的聲明。 Windows 窗體設計器生成的代碼創(chuàng)建沒有輸入?yún)?shù)的?Main
。
Main
?方法的參數(shù)是一個表示命令行參數(shù)的?String?數(shù)組。 通常,通過測試?Length
?屬性來確定參數(shù)是否存在,例如:
if (args.Length == 0)
{System.Console.WriteLine("Please enter a numeric argument.");return 1;
}
提示
args
?數(shù)組不能為 null。 因此,無需進行 null 檢查即可放心地訪問?Length
?屬性。
還可以使用?Convert?類或?Parse
?方法將字符串參數(shù)轉換為數(shù)字類型。 例如,以下語句使用?Parse?方法將?string
?轉換為?long
?數(shù)字:
long num = Int64.Parse(args[0]);
也可以使用 C# 類型?long
,其別名為?Int64
:
long num = long.Parse(args[0]);
還可以使用?Convert
?類方法?ToInt64
?來執(zhí)行同樣的操作:
long num = Convert.ToInt64(s);
有關詳細信息,請參閱?Parse?和?Convert。
提示:本段,以下不看。
分析命令行參數(shù)可能比較復雜。 請考慮使用?System.CommandLine?庫(目前為 beta 版)來簡化該過程。
以下示例演示如何在控制臺應用程序中使用命令行參數(shù)。 應用程序在運行時獲取一個參數(shù),將該參數(shù)轉換為整數(shù),并計算數(shù)字的階乘。 如果未提供任何參數(shù),則應用程序會發(fā)出一條消息,說明程序的正確用法。
若要在命令提示符下編譯并運行該應用程序,請按照下列步驟操作:
-
將以下代碼粘貼到任何文本編輯器,然后將該文件保存為名為“Factorial.cs”的文本文件。
public class Functions {public static long Factorial(int n){// Test for invalid input.if ((n < 0) || (n > 20)){return -1;}// Calculate the factorial iteratively rather than recursively.long tempResult = 1;for (int i = 1; i <= n; i++){tempResult *= i;}return tempResult;} }class MainClass {static int Main(string[] args){// Test if input arguments were supplied.if (args.Length == 0){Console.WriteLine("Please enter a numeric argument.");Console.WriteLine("Usage: Factorial <num>");return 1;}// Try to convert the input arguments to numbers. This will throw// an exception if the argument is not a number.// num = int.Parse(args[0]);int num;bool test = int.TryParse(args[0], out num);if (!test){Console.WriteLine("Please enter a numeric argument.");Console.WriteLine("Usage: Factorial <num>");return 1;}// Calculate factorial.long result = Functions.Factorial(num);// Print result.if (result == -1)Console.WriteLine("Input must be >= 0 and <= 20.");elseConsole.WriteLine($"The Factorial of {num} is {result}.");return 0;} } // If 3 is entered on command line, the // output reads: The factorial of 3 is 6.
-
從“開始”屏幕或“開始”菜單中,打開 Visual Studio“開發(fā)人員命令提示”窗口,然后導航到包含你創(chuàng)建的文件的文件夾。
-
輸入以下命令以編譯應用程序。
dotnet build
如果應用程序不存在編譯錯誤,則會創(chuàng)建一個名為“Factorial.exe”的可執(zhí)行文件。
-
輸入以下命令以計算 3 的階乘:
dotnet run -- 3
-
該命令將生成以下輸出:
The factorial of 3 is 6.
備注
在 Visual Studio 中運行應用程序時,可在“項目設計器”->“調試”頁中指定命令行參數(shù)。
03 頂級語句 - 不使用?Main
?方法的程序(本節(jié)下面的幾乎是狗屁!)
無需在控制臺應用程序項目中顯式包含?Main
?方法。 相反,可以使用頂級語句功能最大程度地減少必須編寫的代碼。
使用頂級語句可直接在文件的根目錄中編寫可執(zhí)行代碼,而無需在類或方法中包裝代碼。 這意味著無需使用?Program
?類和?Main
?方法即可創(chuàng)建程序。 在這種情況下,編譯器將使用入口點方法為應用程序生成?Program
?類。 生成方法的名稱不是?Main
,而是你的代碼無法直接引用的實現(xiàn)詳細信息。
下面是一個 Program.cs 文件看,它是 C# 10 中的一個完整 C# 程序:
Console.WriteLine("Hello World!");
借助頂級語句,可以為小實用程序(如 Azure Functions 和 GitHub Actions)編寫簡單的程序。 它們還使初次接觸 C# 的程序員能夠更輕松地開始學習和編寫代碼。
以下各節(jié)介紹了可對頂級語句執(zhí)行和不能執(zhí)行的操作的規(guī)則。
03.01 僅能有一個頂級文件
一個應用程序只能有一個入口點。 一個項目只能有一個包含頂級語句的文件。 在項目中的多個文件中放置頂級語句會導致以下編譯器錯誤:
CS8802:只有一個編譯單元可具有頂級語句。
一個項目可具有任意數(shù)量的其他源代碼文件,這些文件不包含頂級語句。
03.02 沒有其他入口點
可以顯式編寫?Main
?方法,但它不能作為入口點。 編譯器將發(fā)出以下警告:
CS7022:程序的入口點是全局代碼;忽略“Main()”入口點。
在具有頂級語句的項目中,不能使用?-main?編譯器選項來選擇入口點,即使該項目具有一個或多個?Main
?方法。
03.03 using
?指令
如果包含?using
?指令,這些指令必須先出現(xiàn)在文件中,如以下示例中所示:
using System.Text;StringBuilder builder = new();
builder.AppendLine("The following arguments are passed:");// Display the command line arguments using the args variable.
foreach (var arg in args)
{builder.AppendLine($"Argument={arg}");
}Console.WriteLine(builder.ToString());// Return a success code.
return 0;
03.04 全局命名空間
頂級語句隱式位于全局命名空間中。
03.05 命名空間和類型定義
具有頂級語句的文件還可以包含命名空間和類型定義,但它們必須位于頂級語句之后。 例如:
MyClass.TestMethod();
MyNamespace.MyClass.MyMethod();public class MyClass
{public static void TestMethod(){Console.WriteLine("Hello World!");}
}namespace MyNamespace
{class MyClass{public static void MyMethod(){Console.WriteLine("Hello World from MyNamespace.MyClass.MyMethod!");}}
}
03.06 args
頂級語句可以引用?args
?變量來訪問輸入的任何命令行參數(shù)。?args
?變量永遠不會為 null,但如果未提供任何命令行參數(shù),則其?Length
?將為零。 例如:
if (args.Length > 0)
{foreach (var arg in args){Console.WriteLine($"Argument={arg}");}
}
else
{Console.WriteLine("No arguments");
}
03.07 await
可以通過使用?await
?來調用異步方法。 例如:
Console.Write("Hello ");
await Task.Delay(5000);
Console.WriteLine("World!");
03.08 進程的退出代碼
若要在應用程序結束時返回?int
?值,請像在?Main
?方法中返回?int
?那樣使用?return
?語句。 例如:
string? s = Console.ReadLine();int returnValue = int.Parse(s ?? "-1");
return returnValue;
03.09 隱式入口點方法
編譯器會生成一個方法,作為具有頂級語句的項目的程序入口點。 方法的簽名取決于頂級語句是包含?await
?關鍵字還是?return
?語句。 下表顯示了方法簽名的外觀,為了方便起見,在表中使用了方法名稱?Main
。
頂級代碼包含 | 隱式?Main ?簽名 |
---|---|
await ?和?return | static async Task<int> Main(string[] args) |
await | static async Task Main(string[] args) |
return | static int Main(string[] args) |
否?await ?或?return | static void Main(string[] args) |
04 數(shù)據(jù)與類型(干貨開始,讀到似懂非懂即可)
C# 是一種強類型語言。 每個變量和常量都有一個類型,每個求值的表達式也是如此。 每個方法聲明都為每個輸入?yún)?shù)和返回值指定名稱、類型和種類(值、引用或輸出)。 .NET 類庫定義了內置數(shù)值類型和表示各種構造的復雜類型。 其中包括文件系統(tǒng)、網(wǎng)絡連接、對象的集合和數(shù)組以及日期。 典型的 C# 程序使用類庫中的類型,以及對程序問題域的專屬概念進行建模的用戶定義類型。
類型中可存儲的信息包括以下項:
- 類型變量所需的存儲空間。
- 可以表示的最大值和最小值。
- 包含的成員(方法、字段、事件等)。
- 繼承自的基類型。
- 它實現(xiàn)的接口。
- 允許執(zhí)行的運算種類。
編譯器使用類型信息來確保在代碼中執(zhí)行的所有操作都是類型安全的。 例如,如果聲明?int?類型的變量,那么編譯器允許在加法和減法運算中使用此變量。 如果嘗試對?bool?類型的變量執(zhí)行這些相同操作,則編譯器將生成錯誤,如以下示例所示:
int a = 5;
int b = a + 2; //OKbool test = true;// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;
備注
C 和 C++ 開發(fā)人員請注意,在 C# 中,bool
?不能轉換為?int
。
編譯器將類型信息作為元數(shù)據(jù)嵌入可執(zhí)行文件中。 公共語言運行時 (CLR) 在運行時使用元數(shù)據(jù),以在分配和回收內存時進一步保證類型安全性。
04.01 類型概述
04.01.01 在變量聲明中指定類型
當在程序中聲明變量或常量時,必須指定其類型或使用?var?關鍵字讓編譯器推斷類型。 以下示例顯示了一些使用內置數(shù)值類型和復雜用戶定義類型的變量聲明:
// Declaration only:
float temperature;
string name;
MyClass myClass;// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in sourcewhere item <= limitselect item;
方法聲明指定方法參數(shù)的類型和返回值。 以下簽名顯示了需要?int
?作為輸入?yún)?shù)并返回字符串的方法:
public string GetName(int ID)
{if (ID < names.Length)return names[ID];elsereturn String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };
聲明變量后,不能使用新類型重新聲明該變量,并且不能分配與其聲明的類型不兼容的值。 例如,不能聲明?int
?后再向它分配?true
?的布爾值。 不過,可以將值轉換成其他類型。例如,在將值分配給新變量或作為方法自變量傳遞時。 編譯器會自動執(zhí)行不會導致數(shù)據(jù)丟失的類型轉換。 如果類型轉換可能會導致數(shù)據(jù)丟失,必須在源代碼中進行顯式轉換。
有關詳細信息,請參閱顯式轉換和類型轉換。
04.01.02 內置類型
C# 提供了一組標準的內置類型。 這些類型表示整數(shù)、浮點值、布爾表達式、文本字符、十進制值和其他數(shù)據(jù)類型。 還有內置的?string
?和?object
?類型。 這些類型可供在任何 C# 程序中使用。 有關內置類型的完整列表,請參閱內置類型。
04.01.03 自定義類型
可以使用?struct、class、interface、enum?和?record?構造來創(chuàng)建自己的自定義類型。 .NET 類庫本身是一組自定義類型,以供你在自己的應用程序中使用。 默認情況下,類庫中最常用的類型在任何 C# 程序中均可用。 其他類型只有在顯式添加對定義這些類型的程序集的項目引用時才可用。 編譯器引用程序集之后,你可以聲明在源代碼的此程序集中聲明的類型的變量(和常量)。 有關詳細信息,請參閱?.NET 類庫。
04.01.04 通用類型系統(tǒng)
對于 .NET 中的類型系統(tǒng),請務必了解以下兩個基本要點:
- 它支持繼承原則。 類型可以派生自其他類型(稱為基類型)。 派生類型繼承(有一些限制)基類型的方法、屬性和其他成員。 基類型可以繼而從某種其他類型派生,在這種情況下,派生類型繼承其繼承層次結構中的兩種基類型的成員。 所有類型(包括?System.Int32?(C# keyword:?
int
) 等內置數(shù)值類型)最終都派生自單個基類型,即?System.Object?(C# keyword:?object)。 這樣的統(tǒng)一類型層次結構稱為通用類型系統(tǒng)?(CTS)。 若要詳細了解 C# 中的繼承,請參閱繼承。 - CTS 中的每種類型被定義為值類型或引用類型。 這些類型包括 .NET 類庫中的所有自定義類型以及你自己的用戶定義類型。 使用?
struct
?關鍵字定義的類型是值類型;所有內置數(shù)值類型都是?structs
。 使用?class
?或?record
?關鍵字定義的類型是引用類型。 引用類型和值類型遵循不同的編譯時規(guī)則和運行時行為。
下圖展示了 CTS 中值類型和引用類型之間的關系。
?備注
你可能會發(fā)現(xiàn),最常用的類型全都被整理到了?System?命名空間中。 不過,包含類型的命名空間與類型是值類型還是引用類型沒有關系。
類和結構是 .NET 通用類型系統(tǒng)的兩種基本構造。 C# 9 添加記錄,記錄是一種類。 每種本質上都是一種數(shù)據(jù)結構,其中封裝了同屬一個邏輯單元的一組數(shù)據(jù)和行為。 數(shù)據(jù)和行為是類、結構或記錄的成員。 這些行為包括方法、屬性和事件等,本文稍后將具體列舉。
類、結構或記錄聲明類似于一張藍圖,用于在運行時創(chuàng)建實例或對象。 如果定義名為?Person
?的類、結構或記錄,則?Person
?是類型的名稱。 如果聲明和初始化?Person
?類型的變量?p
,那么?p
?就是所謂的?Person
?對象或實例。 可以創(chuàng)建同一?Person
?類型的多個實例,每個實例都可以有不同的屬性和字段值。
類是引用類型。 創(chuàng)建類型的對象后,向其分配對象的變量僅保留對相應內存的引用。 將對象引用分配給新變量后,新變量會引用原始對象。 通過一個變量所做的更改將反映在另一個變量中,因為它們引用相同的數(shù)據(jù)。
結構是值類型。 創(chuàng)建結構時,向其分配結構的變量保留結構的實際數(shù)據(jù)。 將結構分配給新變量時,會復制結構。 因此,新變量和原始變量包含相同數(shù)據(jù)的副本(共兩個)。 對一個副本所做的更改不會影響另一個副本。
記錄類型可以是引用類型 (record class
) 或值類型 (record struct
)。
一般來說,類用于對更復雜的行為建模。 類通常存儲計劃在創(chuàng)建類對象后進行修改的數(shù)據(jù)。 結構最適用于小型數(shù)據(jù)結構。 結構通常存儲不打算在創(chuàng)建結構后修改的數(shù)據(jù)。 記錄類型是具有附加編譯器合成成員的數(shù)據(jù)結構。 記錄通常存儲不打算在創(chuàng)建對象后修改的數(shù)據(jù)。
04.01.05 值類型
值類型派生自System.ValueType(派生自?System.Object)。 派生自?System.ValueType?的類型在 CLR 中具有特殊行為。 值類型變量直接包含其值。 結構的內存在聲明變量的任何上下文中進行內聯(lián)分配。 對于值類型變量,沒有單獨的堆分配或垃圾回收開銷。 可以聲明屬于值類型的?record struct
?類型,并包括記錄的合成成員。
值類型分為兩類:struct
和enum
。
內置的數(shù)值類型是結構,它們具有可訪問的字段和方法:
// constant field on type byte.
byte b = byte.MaxValue;
但可將這些類型視為簡單的非聚合類型,為其聲明并賦值:
byte num = 0xA;
int i = 5;
char c = 'Z';
值類型已密封。 不能從任何值類型(例如?System.Int32)派生類型。 不能將結構定義為從任何用戶定義的類或結構繼承,因為結構只能從?System.ValueType?繼承。 但是,一個結構可以實現(xiàn)一個或多個接口。 可將結構類型強制轉換為其實現(xiàn)的任何接口類型。 這將導致“裝箱”操作,以將結構包裝在托管堆上的引用類型對象內。 當你將值類型傳遞給使用?System.Object?或任何接口類型作為輸入?yún)?shù)的方法時,就會發(fā)生裝箱操作。 有關詳細信息,請參閱裝箱和取消裝箱。
使用?struct?關鍵字可以創(chuàng)建你自己的自定義值類型。 結構通常用作一小組相關變量的容器,如以下示例所示:
public struct Coords
{public int x, y;public Coords(int p1, int p2){x = p1;y = p2;}
}
有關結構的詳細信息,請參閱結構類型。 有關值類型的詳細信息,請參閱值類型。
另一種值類型是enum
。 枚舉定義的是一組已命名的整型常量。 例如,.NET 類庫中的?System.IO.FileMode?枚舉包含一組已命名的常量整數(shù),用于指定打開文件應采用的方式。 下面的示例展示了具體定義:
public enum FileMode
{CreateNew = 1,Create = 2,Open = 3,OpenOrCreate = 4,Truncate = 5,Append = 6,
}
System.IO.FileMode.Create?常量的值為 2。 不過,名稱對于閱讀源代碼的人來說更有意義,因此,最好使用枚舉,而不是常量數(shù)字文本。 有關詳細信息,請參閱?System.IO.FileMode。
所有枚舉從?System.Enum(繼承自?System.ValueType)繼承。 適用于結構的所有規(guī)則也適用于枚舉。 有關枚舉的詳細信息,請參閱枚舉類型。
04.01.06 引用類型
定義為?class
、record
、delegate、數(shù)組或?interface?的類型是?reference type。
在聲明變量?reference type?時,它將包含值?null,直到你將其分配給該類型的實例,或者使用?new?運算符創(chuàng)建一個。 下面的示例演示了如何創(chuàng)建和分配類:
MyClass myClass = new MyClass();
MyClass myClass2 = myClass;
無法使用?new?運算符直接實例化?interface。 而是創(chuàng)建并分配實現(xiàn)接口的類實例。 請考慮以下示例:
MyClass myClass = new MyClass();// Declare and assign using an existing value.
IMyInterface myInterface = myClass;// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();
創(chuàng)建對象時,會在托管堆上分配內存。 變量只保留對對象位置的引用。 對于托管堆上的類型,在分配內存和回收內存時都會產(chǎn)生開銷。 “垃圾回收”是 CLR 的自動內存管理功能,用于執(zhí)行回收。 但是,垃圾回收已是高度優(yōu)化,并且在大多數(shù)情況下,不會產(chǎn)生性能問題。 有關垃圾回收的詳細信息,請參閱自動內存管理。
所有數(shù)組都是引用類型,即使元素是值類型,也不例外。 數(shù)組隱式派生自?System.Array?類。 可以使用 C# 提供的簡化語法聲明和使用數(shù)組,如以下示例所示:
// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };// Access an instance property of System.Array.
int len = nums.Length;
引用類型完全支持繼承。 創(chuàng)建類時,可以從其他任何未定義為密封的接口或類繼承。 其他類可以從你的類繼承并替代虛擬方法。 若要詳細了解如何創(chuàng)建你自己的類,請參閱類、結構和記錄。 有關繼承和虛方法的詳細信息,請參閱繼承。
04.01.07 文本值的類型
在 C# 中,文本值從編譯器接收類型。 可以通過在數(shù)字末尾追加一個字母來指定數(shù)字文本應采用的類型。 例如,若要指定應按?float
?來處理值?4.56
,則在該數(shù)字后追加一個“f”或“F”,即?4.56f
。 如果沒有追加字母,那么編譯器就會推斷文本值的類型。 若要詳細了解可以使用字母后綴指定哪些類型,請參閱整型數(shù)值類型和浮點數(shù)值類型。
由于文本已類型化,且所有類型最終都是從?System.Object?派生,因此可以編寫和編譯如下所示的代碼:
string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);
04.01.08 泛型類型(放過它!)
可使用一個或多個類型參數(shù)聲明的類型,用作實際類型(具體類型)的占位符 。 客戶端代碼在創(chuàng)建類型實例時提供具體類型。 這種類型稱為泛型類型。 例如,.NET 類型?System.Collections.Generic.List<T>?具有一個類型參數(shù),它按照慣例被命名為?T
。 當創(chuàng)建類型的實例時,指定列表將包含的對象的類型,例如?string
:
List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);
通過使用類型參數(shù),可重新使用相同類以保存任意類型的元素,且無需將每個元素轉換為對象。 泛型集合類稱為強類型集合,因為編譯器知道集合元素的具體類型,并能在編譯時引發(fā)錯誤,例如當嘗試向上面示例中的?stringList
?對象添加整數(shù)時。 有關詳細信息,請參閱泛型。
04.01.09 隱式類型、匿名類型和可以為 null 的值類型
你可以使用?var?關鍵字隱式鍵入一個局部變量(但不是類成員)。 變量仍可在編譯時獲取類型,但類型是由編譯器提供。 有關詳細信息,請參閱隱式類型局部變量。
不方便為不打算存儲或傳遞外部方法邊界的簡單相關值集合創(chuàng)建命名類型。 因此,可以創(chuàng)建匿名類型。 有關詳細信息,請參閱匿名類型。
普通值類型不能具有?null?值。 不過,可以在類型后面追加??
,創(chuàng)建可為空的值類型。 例如,int?
?是還可以包含值?null?的?int
?類型。 可以為 null 的值類型是泛型結構類型?System.Nullable<T>?的實例。 在將數(shù)據(jù)傳入和傳出數(shù)據(jù)庫(數(shù)值可能為?null
)時,可為空的值類型特別有用。 有關詳細信息,請參閱可以為 null 的值類型。
04.01.10 編譯時類型和運行時類型
變量可以具有不同的編譯時和運行時類型。 編譯時類型是源代碼中變量的聲明或推斷類型。 運行時類型是該變量所引用的實例的類型。 這兩種類型通常是相同的,如以下示例中所示:
string message = "This is a string of characters";
在其他情況下,編譯時類型是不同的,如以下兩個示例所示:
object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";
在上述兩個示例中,運行時類型為?string
。 編譯時類型在第一行中為?object
,在第二行中為?IEnumerable<char>
。
如果變量的這兩種類型不同,請務必了解編譯時類型和運行時類型的應用情況。 編譯時類型確定編譯器執(zhí)行的所有操作。 這些編譯器操作包括方法調用解析、重載決策以及可用的隱式和顯式強制轉換。 運行時類型確定在運行時解析的所有操作。 這些運行時操作包括調度虛擬方法調用、計算?is
?和?switch
?表達式以及其他類型的測試 API。 為了更好地了解代碼如何與類型進行交互,請識別哪個操作應用于哪種類型。
04.02?命名空間 namespace (看!)
04.02.01 基本概念
在 C# 編程中,命名空間在兩個方面被大量使用。 首先,.NET 使用命名空間來組織它的許多類,如下所示:
System.Console.WriteLine("Hello World!");
System?是一個命名空間,Console?是該命名空間中的一個類。 可使用?using
?關鍵字,這樣就不必使用完整的名稱,如下例所示:
using System;
Console.WriteLine("Hello World!");
有關詳細信息,請參閱?using 指令。
重要
適用于 .NET 6 的 C# 模板使用頂級語句。 如果你已升級到 .NET 6,則應用程序可能與本文中的代碼不匹配。 有關詳細信息,請參閱有關新 C# 模板生成頂級語句的文章
.NET 6 SDK 還為使用以下 SDK 的項目添加了一組隱式?global using
?指令:
- Microsoft.NET.Sdk
- Microsoft.NET.Sdk.Web
- Microsoft.NET.Sdk.Worker
這些隱式?global using
?指令包含項目類型最常見的命名空間。
有關詳細信息,請參閱隱式 using 指令一文
其次,在較大的編程項目中,聲明自己的命名空間可以幫助控制類和方法名稱的范圍。 使用?namespace?關鍵字可聲明命名空間,如下例所示:
namespace SampleNamespace
{class SampleClass{public void SampleMethod(){System.Console.WriteLine("SampleMethod inside SampleNamespace");}}
}
命名空間的名稱必須是有效的 C#?標識符名稱。
從 C# 10 開始,可以為該文件中定義的所有類型聲明一個命名空間,如以下示例所示:
namespace SampleNamespace;class AnotherSampleClass
{public void AnotherSampleMethod(){System.Console.WriteLine("SampleMethod inside SampleNamespace");}
}
這種新語法的優(yōu)點是更簡單,這節(jié)省了水平空間且不必使用大括號。 這使得你的代碼易于閱讀。
04.02.02 命名空間概述
命名空間具有以下屬性:
- 它們組織大型代碼項目。
- 通過使用?
.
?運算符分隔它們。 using
?指令可免去為每個類指定命名空間的名稱。global
?命名空間是“根”命名空間:global::System
?始終引用 .NET?System?命名空間。
04.03?類 class (細看!)
04.03.02?引用類型(編號沒錯!原文呵呵!)
定義為?class?的類型是引用類型。 在運行時,如果聲明引用類型的變量,此變量就會一直包含值?null,直到使用?new?運算符顯式創(chuàng)建類實例,或直到為此變量分配已在其他位置創(chuàng)建的兼容類型,如下面的示例所示:
//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;
創(chuàng)建對象時,在該托管堆上為該特定對象分足夠的內存,并且該變量僅保存對所述對象位置的引用。 對象使用的內存由 CLR 的自動內存管理功能(稱為垃圾回收)回收。 有關垃圾回收的詳細信息,請參閱自動內存管理和垃圾回收。
04.03.01 聲明類
使用后跟唯一標識符的?class
?關鍵字可以聲明類,如下例所示:
//[access modifier] - [class] - [identifier]
public class Customer
{// Fields, properties, methods and events go here...
}
可選訪問修飾符位于?class
?關鍵字前面。?class
?類型的默認訪問權限為?internal
。 此例中使用的是?public,因此任何人都可創(chuàng)建此類的實例。 類的名稱遵循?class
?關鍵字。 類名稱必須是有效的 C#?標識符名稱。 定義的其余部分是類的主體,其中定義了行為和數(shù)據(jù)。 類上的字段、屬性、方法和事件統(tǒng)稱為類成員。
04.03.03 創(chuàng)建對象
雖然它們有時可以互換使用,但類和對象是不同的概念。 類定義對象類型,但不是對象本身。 對象是基于類的具體實體,有時稱為類的實例。
可通過使用?new
?關鍵字,后跟類的名稱來創(chuàng)建對象,如下所示:
Customer object1 = new Customer();
創(chuàng)建類的實例后,會將一個該對象的引用傳遞回程序員。 在上一示例中,object1
?是對基于?Customer
?的對象的引用。 該引用指向新對象,但不包含對象數(shù)據(jù)本身。 事實上,可以創(chuàng)建對象引用,而完全無需創(chuàng)建對象本身:
Customer object2;
不建議創(chuàng)建不引用對象的對象引用,因為嘗試通過這類引用訪問對象會在運行時失敗。 但是,但實際上引用可以引用某個對象,方法是創(chuàng)建新對象,或者將其分配給現(xiàn)有對象,例如:
C#復制
Customer object3 = new Customer();
Customer object4 = object3;
此代碼創(chuàng)建指向同一對象的兩個對象引用。 因此,通過?object3
?對對象做出的任何更改都會在后續(xù)使用?object4
?時反映出來。 由于基于類的對象是通過引用來實現(xiàn)其引用的,因此類被稱為引用類型。
04.03.04 構造函數(shù)和初始化
前面的部分介紹了聲明類類型并創(chuàng)建該類型的實例的語法。 創(chuàng)建類型的實例時,需要確保其字段和屬性已初始化為有用的值。 可通過多種方式初始化值:
- 接受默認值
- 字段初始化表達式
- 構造函數(shù)參數(shù)
- 對象初始值設定項
每個 .NET 類型都有一個默認值。 通常,對于數(shù)字類型,該值為 0,對于所有引用類型,該值為?null
。 如果默認值在應用中是合理的,則可以依賴于該默認值。
當 .NET 默認值不是正確的值時,可以使用字段初始化表達式設置初始值:
public class Container
{// Initialize capacity field to a default value of 10:private int _capacity = 10;
}
可以通過定義負責設置初始值的構造函數(shù)來要求調用方提供初始值:
public class Container
{private int _capacity;public Container(int capacity) => _capacity = capacity;
}
從 C# 12 開始,可以將主構造函數(shù)定義為類聲明的一部分:
public class Container(int capacity)
{private int _capacity = capacity;
}
向類名添加參數(shù)可定義主構造函數(shù)。 這些參數(shù)在包含其成員的類正文中可用。 可以將其用于初始化字段或需要它們的任何其他位置。
還可以對某個屬性使用?required
?修飾符,并允許調用方使用對象初始值設定項來設置該屬性的初始值:
public class Person
{public required string LastName { get; set; }public required string FirstName { get; set; }
}
添加?required
?關鍵字要求調用方必須將這些屬性設置為?new
?表達式的一部分:
var p1 = new Person(); // Error! Required properties not set
var p2 = new Person() { FirstName = "Grace", LastName = "Hopper" };
04.03.05 類繼承
類完全支持繼承,這是面向對象的編程的基本特點。 創(chuàng)建類時,可以從其他任何未定義為?sealed?的類繼承。 其他類可以從你的類繼承并替代類虛擬方法。 此外,你可以實現(xiàn)一個或多個接口。
繼承是通過使用派生來完成的,這意味著類是通過使用其數(shù)據(jù)和行為所派生自的基類來聲明的。 基類通過在派生的類名稱后面追加冒號和基類名稱來指定,如:
public class Manager : Employee
{// Employee fields, properties, methods and events are inherited// New Manager fields, properties, methods and events go here...
}
類聲明包括基類時,它會繼承基類除構造函數(shù)外的所有成員。 有關詳細信息,請參閱繼承。
C# 中的類只能直接從基類繼承。 但是,因為基類本身可能繼承自其他類,因此類可能間接繼承多個基類。 此外,類可以支持實現(xiàn)一個或多個接口。 有關詳細信息,請參閱接口。
類可以聲明為?abstract。 抽象類包含抽象方法,抽象方法包含簽名定義但不包含實現(xiàn)。 抽象類不能實例化。 只能通過可實現(xiàn)抽象方法的派生類來使用該類。 與此相反,密封類不允許其他類繼承。 有關詳細信息,請參閱抽象類、密封類和類成員。
類定義可以在不同的源文件之間分割。 有關詳細信息,請參閱分部類和方法。
04.04?記錄 Record
C# 中的記錄是一個類或結構,它為使用數(shù)據(jù)模型提供特定的語法和行為。?record
?修飾符指示編譯器合成對主要角色存儲數(shù)據(jù)的類型有用的成員。 這些成員包括支持值相等的?ToString()?和成員的重載。
04.04.01 何時使用記錄
在下列情況下,請考慮使用記錄而不是類或結構:
- 你想要定義依賴值相等性的數(shù)據(jù)模型。
- 你想要定義對象不可變的類型。
04.04.02 值相等性
對記錄來說,值相等性是指如果記錄類型的兩個變量類型相匹配,且所有屬性和字段值都相同,那么記錄類型的兩個變量是相等的。 對于其他引用類型(例如類),相等性默認指引用相等性,除非執(zhí)行了值相等性。 也就是說,如果類的兩個變量引用同一個對象,則這兩個變量是相等的。 確定兩個記錄實例的相等性的方法和運算符使用值相等性。
并非所有數(shù)據(jù)模型都適合使用值相等性。 例如,Entity Framework Core?依賴引用相等性,來確保它對概念上是一個實體的實體類型只使用一個實例。 因此,記錄類型不適合用作 Entity Framework Core 中的實體類型。
04.04.03 不可變性
不可變類型會阻止你在對象實例化后更改該對象的任何屬性或字段值。 如果你需要一個類型是線程安全的,或者需要哈希代碼在哈希表中能保持不變,那么不可變性很有用。 記錄為創(chuàng)建和使用不可變類型提供了簡潔的語法。
不可變性并不適用于所有數(shù)據(jù)方案。 例如,Entity Framework Core?不支持通過不可變實體類型進行更新。
04.04.04 記錄與類和結構的區(qū)別
聲明和實例化類或結構時使用的語法與操作記錄時的相同。 只是將?class
?關鍵字替換為?record
,或者使用?record struct
?而不是?struct
。 同樣地,記錄類支持相同的表示繼承關系的語法。 記錄與類的區(qū)別如下所示:
- 可在主構造函數(shù)中使用位置參數(shù)來創(chuàng)建和實例化具有不可變屬性的類型。
- 在類中指示引用相等性或不相等的方法和運算符(例如?Object.Equals(Object)?和?
==
)在記錄中指示值相等性或不相等。 - 可使用?with?表達式對不可變對象創(chuàng)建在所選屬性中具有新值的副本。
- 記錄的?
ToString
?方法會創(chuàng)建一個格式字符串,它顯示對象的類型名稱及其所有公共屬性的名稱和值。 - 記錄可從另一個記錄繼承。 但記錄不可從類繼承,類也不可從記錄繼承。
記錄結構與結構的不同之處是,編譯器合成了方法來確定相等性和?ToString
。 編譯器為位置記錄結構合成?Deconstruct
?方法。
編譯器為?record class
?中的每個主構造函數(shù)參數(shù)合成一個公共僅初始化屬性。 在?record struct
?中,編譯器合成公共讀寫屬性。 編譯器不會在不包含?record
?修飾符的?class
?和?struct
?類型中創(chuàng)建主構造函數(shù)參數(shù)的屬性。
04.04.05 Record 示例
下面的示例定義了一個公共記錄,它使用位置參數(shù)來聲明和實例化記錄。 然后,它會輸出類型名稱和屬性值:
public record Person(string FirstName, string LastName);public static class Program
{public static void Main(){Person person = new("Nancy", "Davolio");Console.WriteLine(person);// output: Person { FirstName = Nancy, LastName = Davolio }}}
下面的示例演示了記錄中的值相等性:
public record Person(string FirstName, string LastName, string[] PhoneNumbers);
public static class Program
{public static void Main(){var phoneNumbers = new string[2];Person person1 = new("Nancy", "Davolio", phoneNumbers);Person person2 = new("Nancy", "Davolio", phoneNumbers);Console.WriteLine(person1 == person2); // output: Trueperson1.PhoneNumbers[0] = "555-1234";Console.WriteLine(person1 == person2); // output: TrueConsole.WriteLine(ReferenceEquals(person1, person2)); // output: False}
}
下面的示例演示如何使用?with
?表達式來復制不可變對象和更改其中的一個屬性:
public record Person(string FirstName, string LastName)
{public required string[] PhoneNumbers { get; init; }
}public class Program
{public static void Main(){Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };Console.WriteLine(person1);// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }Person person2 = person1 with { FirstName = "John" };Console.WriteLine(person2);// output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }Console.WriteLine(person1 == person2); // output: Falseperson2 = person1 with { PhoneNumbers = new string[1] };Console.WriteLine(person2);// output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }Console.WriteLine(person1 == person2); // output: Falseperson2 = person1 with { };Console.WriteLine(person1 == person2); // output: True}
}
有關詳細信息,請查看記錄(C# 參考)。
04.05?接口 interface - 定義多種類型的行為
接口包含非抽象?class?或?struct?必須實現(xiàn)的一組相關功能的定義。 接口可以定義?static
?方法,此類方法必須具有實現(xiàn)。 接口可為成員定義默認實現(xiàn)。 接口不能聲明實例數(shù)據(jù),如字段、自動實現(xiàn)的屬性或類似屬性的事件。
例如,使用接口可以在類中包括來自多個源的行為。 該功能在 C# 中十分重要,因為該語言不支持類的多重繼承。 此外,如果要模擬結構的繼承,也必須使用接口,因為它們無法實際從另一個結構或類繼承。
04.05.01 接口
可使用?interface?關鍵字定義接口,如以下示例所示。
interface IEquatable<T>
{bool Equals(T obj);
}
接口名稱必須是有效的 C#?標識符名稱。 按照約定,接口名稱以大寫字母?I
?開頭。
實現(xiàn)?IEquatable<T>?接口的任何類或結構都必須包含與該接口指定的簽名匹配的?Equals?方法的定義。 因此,可以依靠實現(xiàn)?IEquatable<T>
?的類型?T
?的類來包含?Equals
?方法,類的實例可以通過該方法確定它是否等于相同類的另一個實例。
IEquatable<T>
?的定義不為?Equals
?提供實現(xiàn)。 類或結構可以實現(xiàn)多個接口,但是類只能從單個類繼承。
有關抽象類的詳細信息,請參閱抽象類、密封類及類成員。
接口可以包含實例方法、屬性、事件、索引器或這四種成員類型的任意組合。 接口可以包含靜態(tài)構造函數(shù)、字段、常量或運算符。 從 C# 11 開始,非字段接口成員可以是?static abstract
。 接口不能包含實例字段、實例構造函數(shù)或終結器。 接口成員默認是公共的,可以顯式指定可訪問性修飾符(如?public
、protected
、internal
、private
、protected internal
?或?private protected
)。?private
?成員必須有默認實現(xiàn)。
若要實現(xiàn)接口成員,實現(xiàn)類的對應成員必須是公共、非靜態(tài),并且具有與接口成員相同的名稱和簽名。
備注
當接口聲明靜態(tài)成員時,實現(xiàn)該接口的類型也可能聲明具有相同簽名的靜態(tài)成員。 它們是不同的,并且由聲明成員的類型唯一標識。 在類型中聲明的靜態(tài)成員不會覆蓋接口中聲明的靜態(tài)成員。
實現(xiàn)接口的類或結構必須為所有已聲明的成員提供實現(xiàn),而非接口提供的默認實現(xiàn)。 但是,如果基類實現(xiàn)接口,則從基類派生的任何類都會繼承該實現(xiàn)。
下面的示例演示?IEquatable<T>?接口的實現(xiàn)。 實現(xiàn)類?Car
?必須提供?Equals?方法的實現(xiàn)。
public class Car : IEquatable<Car>
{public string? Make { get; set; }public string? Model { get; set; }public string? Year { get; set; }// Implementation of IEquatable<T> interfacepublic bool Equals(Car? car){return (this.Make, this.Model, this.Year) ==(car?.Make, car?.Model, car?.Year);}
}
類的屬性和索引器可以為接口中定義的屬性或索引器定義額外的訪問器。 例如,接口可能會聲明包含?get?取值函數(shù)的屬性。 實現(xiàn)此接口的類可以聲明包含?get
?和?get
?取值函數(shù)的同一屬性。 但是,如果屬性或索引器使用顯式實現(xiàn),則訪問器必須匹配。 有關顯式實現(xiàn)的詳細信息,請參閱顯式接口實現(xiàn)和接口屬性。
接口可從一個或多個接口繼承。 派生接口從其基接口繼承成員。 實現(xiàn)派生接口的類必須實現(xiàn)派生接口中的所有成員,包括派生接口的基接口的所有成員。 該類可能會隱式轉換為派生接口或任何其基接口。 類可能通過它繼承的基類或通過其他接口繼承的接口來多次包含某個接口。 但是,類只能提供接口的實現(xiàn)一次,并且僅當類將接口作為類定義的一部分 (class ClassName : InterfaceName
) 進行聲明時才能提供。 如果由于繼承實現(xiàn)接口的基類而繼承了接口,則基類會提供接口的成員的實現(xiàn)。 但是,派生類可以重新實現(xiàn)任何虛擬接口成員,而不是使用繼承的實現(xiàn)。 當接口聲明方法的默認實現(xiàn)時,實現(xiàn)該接口的任何類都會繼承該實現(xiàn)(你需要將類實例強制轉換為接口類型,才能訪問接口成員上的默認實現(xiàn))。
基類還可以使用虛擬成員實現(xiàn)接口成員。 在這種情況下,派生類可以通過重寫虛擬成員來更改接口行為。 有關虛擬成員的詳細信息,請參閱多態(tài)性。
04.05.02?接口摘要
接口具有以下屬性:
- 在 8.0 以前的 C# 版本中,接口類似于只有抽象成員的抽象基類。 實現(xiàn)接口的類或結構必須實現(xiàn)其所有成員。
- 從 C# 8.0 開始,接口可以定義其部分或全部成員的默認實現(xiàn)。 實現(xiàn)接口的類或結構不一定要實現(xiàn)具有默認實現(xiàn)的成員。 有關詳細信息,請參閱默認接口方法。
- 接口無法直接進行實例化。 其成員由實現(xiàn)接口的任何類或結構來實現(xiàn)。
- 一個類或結構可以實現(xiàn)多個接口。 一個類可以繼承一個基類,還可實現(xiàn)一個或多個接口。
04.06?泛型類和方法
泛型向 .NET 引入了類型參數(shù)的概念。 泛型支持設計類和方法,你可在在代碼中使用該類或方法時,再定義一個或多個類型參數(shù)的規(guī)范。 例如,通過使用泛型類型參數(shù)?T
,可以編寫其他客戶端代碼能夠使用的單個類,而不會產(chǎn)生運行時轉換或裝箱操作的成本或風險,如下所示:
// Declare the generic class.
public class GenericList<T>
{public void Add(T input) { }
}
class TestGenericList
{private class ExampleClass { }static void Main(){// Declare a list of type int.GenericList<int> list1 = new GenericList<int>();list1.Add(1);// Declare a list of type string.GenericList<string> list2 = new GenericList<string>();list2.Add("");// Declare a list of type ExampleClass.GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();list3.Add(new ExampleClass());}
}
泛型類和泛型方法兼具可重用性、類型安全性和效率,這是非泛型類和非泛型方法無法實現(xiàn)的。 在編譯過程中將泛型類型參數(shù)替換為類型參數(shù)。 在前面的示例中,編譯器會使用?int
?替換?T
。 泛型通常與集合以及作用于集合的方法一起使用。?System.Collections.Generic?命名空間包含幾個基于泛型的集合類。 不建議使用非泛型集合(如?ArrayList),并且僅出于兼容性目的而維護非泛型集合。 有關詳細信息,請參閱?.NET 中的泛型。
04.06.01 關于泛型
你也可創(chuàng)建自定義泛型類型和泛型方法,以提供自己的通用解決方案,設計類型安全的高效模式。 以下代碼示例演示了出于演示目的的簡單泛型鏈接列表類。 (大多數(shù)情況下,應使用 .NET 提供的?List<T>?類,而不是自行創(chuàng)建類。)在通常使用具體類型來指示列表中所存儲項的類型的情況下,可使用類型參數(shù)?T
:
- 在?
AddHead
?方法中作為方法參數(shù)的類型。 - 在?
Node
?嵌套類中作為?Data
?屬性的返回類型。 - 在嵌套類中作為私有成員?
data
?的類型。
T
?可用于?Node
?嵌套類。 如果使用具體類型實例化?GenericList<T>
(例如,作為?GenericList<int>
),則出現(xiàn)的所有?T
?都將替換為?int
。
// type parameter T in angle brackets
public class GenericList<T>
{// The nested class is also generic on T.private class Node{// T used in non-generic constructor.public Node(T t){next = null;data = t;}private Node? next;public Node? Next{get { return next; }set { next = value; }}// T as private member data type.private T data;// T as return type of property.public T Data{get { return data; }set { data = value; }}}private Node? head;// constructorpublic GenericList(){head = null;}// T as method parameter type:public void AddHead(T t){Node n = new Node(t);n.Next = head;head = n;}public IEnumerator<T> GetEnumerator(){Node? current = head;while (current != null){yield return current.Data;current = current.Next;}}
}
以下代碼示例演示了客戶端代碼如何使用泛型?GenericList<T>
?類來創(chuàng)建整數(shù)列表。 如果更改類型參數(shù),以下代碼將創(chuàng)建字符串列表或任何其他自定義類型:
class TestGenericList
{static void Main(){// int is the type argumentGenericList<int> list = new GenericList<int>();for (int x = 0; x < 10; x++){list.AddHead(x);}foreach (int i in list){System.Console.Write(i + " ");}System.Console.WriteLine("\nDone");}
}
?備注
泛型類型不限于類。 前面的示例使用了?class
?類型,但你可以定義泛型?interface
?和?struct
?類型,包括?record
?類型。
04.06.02 泛型概述
- 使用泛型類型可以最大限度地重用代碼、保護類型安全性以及提高性能。
- 泛型最常見的用途是創(chuàng)建集合類。
- .NET 類庫在?System.Collections.Generic?命名空間中包含幾個新的泛型集合類。 應盡可能使用泛型集合來代替某些類,如?System.Collections?命名空間中的?ArrayList。
- 可以創(chuàng)建自己的泛型接口、泛型類、泛型方法、泛型事件和泛型委托。
- 可以對泛型類進行約束以訪問特定數(shù)據(jù)類型的方法。
- 可以使用反射在運行時獲取有關泛型數(shù)據(jù)類型中使用的類型的信息。
04.07?匿名類型
匿名類型提供了一種方便的方法,可用來將一組只讀屬性封裝到單個對象中,而無需首先顯式定義一個類型。 類型名由編譯器生成,并且不能在源代碼級使用。 每個屬性的類型由編譯器推斷。
可結合使用?new?運算符和對象初始值設定項創(chuàng)建匿名類型。 有關對象初始值設定項的詳細信息,請參閱對象和集合初始值設定項。
以下示例顯示了用兩個名為?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?子句中,以便返回源序列中每個對象的屬性子集。 有關查詢的詳細信息,請參閱C# 中的 LINQ。
匿名類型包含一個或多個公共只讀屬性。 包含其他種類的類成員(如方法或事件)為無效。 用來初始化屬性的表達式不能為?null
、匿名函數(shù)或指針類型。
最常見的方案是用其他類型的屬性初始化匿名類型。 在下面的示例中,假定名為?Product
?的類存在。 類?Product
?包括?Color
?和?Price
?屬性,以及你不感興趣的其他屬性。 變量?Product
products
?是 對象的集合。 匿名類型聲明以?new
?關鍵字開始。 聲明初始化了一個只使用?Product
?的兩個屬性的新類型。 使用匿名類型會導致在查詢中返回的數(shù)據(jù)量變少。
如果你沒有在匿名類型中指定成員名稱,編譯器會為匿名類型成員指定與用于初始化這些成員的屬性相同的名稱。 需要為使用表達式初始化的屬性提供名稱,如下面的示例所示。 在下面示例中,匿名類型的屬性名稱都為?Price
Color
?和 。
var productQuery =from prod in productsselect new { prod.Color, prod.Price };foreach (var v in productQuery)
{Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
提示
可以使用 .NET 樣式規(guī)則?IDE0037?強制執(zhí)行是首選推斷成員名稱還是顯式成員名稱。
還可以按另一種類型(類、結構或另一個匿名類型)的對象定義字段。 它通過使用保存此對象的變量來完成,如以下示例中所示,其中兩個匿名類型是使用已實例化的用戶定義類型創(chuàng)建的。 在這兩種情況下,匿名類型?shipment
?和?shipmentWithBonus
?中的?product
?字段的類型均為?Product
,其中包含每個字段的默認值。?bonus
?字段將是編譯器創(chuàng)建的匿名類型。
var product = new Product();
var bonus = new { note = "You won!" };
var shipment = new { address = "Nowhere St.", product };
var shipmentWithBonus = new { address = "Somewhere St.", product, bonus };
通常,當使用匿名類型來初始化變量時,可以通過使用?var?將變量作為隱式鍵入的本地變量來進行聲明。 類型名稱無法在變量聲明中給出,因為只有編譯器能訪問匿名類型的基礎名稱。 有關?var
?的詳細信息,請參閱隱式類型本地變量。
可通過將隱式鍵入的本地變量與隱式鍵入的數(shù)組相結合創(chuàng)建匿名鍵入的元素的數(shù)組,如下面的示例所示。
var anonArray = new[] { new { name = "apple", diam = 4 }, new { name = "grape", diam = 1 }
};
匿名類型是?class?類型,它們直接派生自?object,并且無法強制轉換為除?object?外的任何類型。 雖然你的應用程序不能訪問它,編譯器還是提供了每一個匿名類型的名稱。 從公共語言運行時的角度來看,匿名類型與任何其他引用類型沒有什么不同。
如果程序集中的兩個或多個匿名對象初始值指定了屬性序列,這些屬性采用相同順序且具有相同的名稱和類型,則編譯器將對象視為相同類型的實例。 它們共享同一編譯器生成的類型信息。
匿名類型支持采用?with 表達式形式的非破壞性修改。 這使你能夠創(chuàng)建匿名類型的新實例,其中一個或多個屬性具有新值:
var apple = new { Item = "apples", Price = 1.35 };
var onSale = apple with { Price = 0.79 };
Console.WriteLine(apple);
Console.WriteLine(onSale);
無法將字段、屬性、時間或方法的返回類型聲明為具有匿名類型。 同樣,你不能將方法、屬性、構造函數(shù)或索引器的形參聲明為具有匿名類型。 要將匿名類型或包含匿名類型的集合作為參數(shù)傳遞給某一方法,可將參數(shù)作為類型?object
?進行聲明。 但是,對匿名類型使用?object
?違背了強類型的目的。 如果必須存儲查詢結果或者必須將查詢結果傳遞到方法邊界外部,請考慮使用普通的命名結構或類而不是匿名類型。
由于匿名類型上的?Equals?和?GetHashCode?方法是根據(jù)方法屬性的?Equals
?和?GetHashCode
?定義的,因此僅當同一匿名類型的兩個實例的所有屬性都相等時,這兩個實例才相等。
備注
匿名類型的輔助功能級別為?internal
,因此在不同程序集中定義的兩種匿名類型并非同一類型。 因此,當在不同的程序集中進行定義時,匿名類型的實例不能彼此相等,即使其所有屬性都相等。
匿名類型確實會重寫?ToString?方法,將用大括號括起來的每個屬性的名稱和?ToString
?輸出連接起來。
var v = new { Title = "Hello", Age = 24 };Console.WriteLine(v.ToString()); // "{ Title = Hello, Age = 24 }"
05 面向對象
05.01?C# 中的類、結構和記錄概述
在 C# 中,某個類型(類、結構或記錄)的定義的作用類似于藍圖,指定該類型可以進行哪些操作。 從本質上說,對象是按照此藍圖分配和配置的內存塊。 本文概述了這些藍圖及其功能。?本系列的下一篇文章介紹對象。
05.01.02 封裝
封裝有時稱為面向對象的編程的第一支柱或原則。 類或結構可以指定自己的每個成員對外部代碼的可訪問性。 可以隱藏不得在類或程序集外部使用的方法和變量,以限制編碼錯誤或惡意攻擊發(fā)生的可能性。 有關詳細信息,請參閱面向對象的編程教程。
05.01.03 成員
類型的成員包括所有方法、字段、常量、屬性和事件。 C# 沒有全局變量或方法,這一點其他某些語言不同。 即使是編程的入口點(Main
?方法),也必須在類或結構中聲明(使用頂級語句時,隱式聲明)。
下面列出了所有可以在類、結構或記錄中聲明的各種成員。
- 字段
- 常量
- 屬性
- 方法
- 構造函數(shù)
- 事件
- 終結器
- 索引器
- 運算符
- 嵌套類型
有關詳細信息,請參見成員。
05.01.04 可訪問性
一些方法和屬性可供類或結構外部的代碼(稱為“客戶端代碼”)調用或訪問。 另一些方法和屬性只能在類或結構本身中使用。 請務必限制代碼的可訪問性,僅供預期的客戶端代碼進行訪問。 需要使用以下訪問修飾符指定類型及其成員對客戶端代碼的可訪問性:
- public
- 受保護
- internal
- protected internal
- private
- 專用受保護。
可訪問性的默認值為?private
。
05.01.05 繼承
類(而非結構)支持繼承的概念。 派生自另一個類(稱為基類)的類自動包含基類的所有公共、受保護和內部成員(其構造函數(shù)和終結器除外)。
可以將類聲明為?abstract,即一個或多個方法沒有實現(xiàn)代碼。 盡管抽象類無法直接實例化,但可以作為提供缺少實現(xiàn)代碼的其他類的基類。 類還可以聲明為?sealed,以阻止其他類繼承。
有關詳細信息,請參閱繼承和多態(tài)性。
05.01.06 界面
類、結構和記錄可以實現(xiàn)多個接口。 從接口實現(xiàn)意味著類型實現(xiàn)接口中定義的所有方法。 有關詳細信息,請參閱接口。
05.01.07 泛型類型
類、結構和記錄可以使用一個或多個類型參數(shù)進行定義。 客戶端代碼在創(chuàng)建類型實例時提供類型。 例如,System.Collections.Generic?命名空間中的?List<T>?類就是用一個類型參數(shù)定義的。 客戶端代碼創(chuàng)建?List<string>
?或?List<int>
?的實例來指定列表將包含的類型。 有關詳細信息,請參閱泛型。
05.01.08 靜態(tài)類型
類(而非結構或記錄)可以聲明為static
。 靜態(tài)類只能包含靜態(tài)成員,不能使用?new
?關鍵字進行實例化。 在程序加載時,類的一個副本會加載到內存中,而其成員則可通過類名進行訪問。 類、結構和記錄可以包含靜態(tài)成員。 有關詳細信息,請參閱靜態(tài)類和靜態(tài)類成員。
05.01.09 嵌套類型
類、結構和記錄可以嵌套在其他類、結構和記錄中。 有關詳細信息,請參閱嵌套類型。
05.01.10 分部類型
可以在一個代碼文件中定義類、結構或方法的一部分,并在其他代碼文件中定義另一部分。 有關詳細信息,請參閱分部類和方法。
05.01.11 對象初始值設定項
可以通過將值分配給屬性來實例化和初始化類或結構對象以及對象集合。 有關詳細信息,請參閱如何使用對象初始值設定項初始化對象。
05.01.12 匿名類型
在不方便或不需要創(chuàng)建命名類的情況下,可以使用匿名類型。 匿名類型由其命名數(shù)據(jù)成員定義。 有關詳細信息,請參閱匿名類型。
05.01.13 擴展方法
可以通過創(chuàng)建單獨的類型來“擴展”類,而無需創(chuàng)建派生類。 該類型包含可以調用的方法,就像它們屬于原始類型一樣。 有關詳細信息,請參閱擴展方法。
05.01.14 隱式類型的局部變量
在類或結構方法中,可以使用隱式類型指示編譯器在編譯時確定變量類型。 有關詳細信息,請參閱?var(C# 參考)。
05.01.15 記錄
C# 9 引入了?record
?類型,可創(chuàng)建此引用類型而不創(chuàng)建類或結構。 記錄是帶有內置行為的類,用于將數(shù)據(jù)封裝在不可變類型中。 C# 10 引入了?record struct
?值類型。 記錄(record class
?或?record struct
)提供以下功能:
- 用于創(chuàng)建具有不可變屬性的引用類型的簡明語法。
- 值相等性。 兩個記錄類型的變量在它們的類型和兩個記錄中每個字段的值都相同時,它們是相等的。 類使用引用相等性,即:如果類類型的兩個變量引用同一對象,則這兩個變量是相等的。
- 非破壞性變化的簡明語法。 使用?
with
?表達式,可以創(chuàng)建作為現(xiàn)有實例副本的新記錄實例,但更改了指定的屬性值。 - 顯示的內置格式設置。?
ToString
?方法輸出記錄類型名稱以及公共屬性的名稱和值。 - 支持記錄類中的繼承層次結構。 記錄類支持繼承。 記錄結構不支持繼承。
有關詳細信息,請參閱記錄。
05.02?對象 - 創(chuàng)建類型的實例
類或結構定義的作用類似于藍圖,指定該類型可以進行哪些操作。 從本質上說,對象是按照此藍圖分配和配置的內存塊。 程序可以創(chuàng)建同一個類的多個對象。 對象也稱為實例,可以存儲在命名變量中,也可以存儲在數(shù)組或集合中。 使用這些變量來調用對象方法及訪問對象公共屬性的代碼稱為客戶端代碼。 在 C# 等面向對象的語言中,典型的程序由動態(tài)交互的多個對象組成。
?備注
靜態(tài)類型的行為與此處介紹的不同。 有關詳細信息,請參閱靜態(tài)類和靜態(tài)類成員。
05.02.01 結構實例與類實例
由于類是引用類型,因此類對象的變量引用該對象在托管堆上的地址。 如果將同一類型的第二個變量分配給第一個變量,則兩個變量都引用該地址的對象。 本文稍后部分將更詳細地討論這一點。
類的實例是使用?new?運算符創(chuàng)建的。 在下面的示例中,Person
?為類型,person1
?和?person2
?為該類型的實例(即對象)。
using System;public class Person
{public string Name { get; set; }public int Age { get; set; }public Person(string name, int age){Name = name;Age = age;}// Other properties, methods, events...
}class Program
{static void Main(){Person person1 = new Person("Leopold", 6);Console.WriteLine("person1 Name = {0} Age = {1}", person1.Name, person1.Age);// Declare new person, assign person1 to it.Person person2 = person1;// Change the name of person2, and person1 also changes.person2.Name = "Molly";person2.Age = 16;Console.WriteLine("person2 Name = {0} Age = {1}", person2.Name, person2.Age);Console.WriteLine("person1 Name = {0} Age = {1}", person1.Name, person1.Age);}
}
/*Output:person1 Name = Leopold Age = 6person2 Name = Molly Age = 16person1 Name = Molly Age = 16
*/
由于結構是值類型,因此結構對象的變量具有整個對象的副本。 結構的實例也可使用?new
?運算符來創(chuàng)建,但這不是必需的,如下面的示例所示:
using System;namespace Example
{public struct Person{public string Name;public int Age;public Person(string name, int age){Name = name;Age = age;}}public class Application{static void Main(){// Create struct instance and initialize by using "new".// Memory is allocated on thread stack.Person p1 = new Person("Alex", 9);Console.WriteLine("p1 Name = {0} Age = {1}", p1.Name, p1.Age);// Create new struct object. Note that struct can be initialized// without using "new".Person p2 = p1;// Assign values to p2 members.p2.Name = "Spencer";p2.Age = 7;Console.WriteLine("p2 Name = {0} Age = {1}", p2.Name, p2.Age);// p1 values remain unchanged because p2 is copy.Console.WriteLine("p1 Name = {0} Age = {1}", p1.Name, p1.Age);}}/*Output:p1 Name = Alex Age = 9p2 Name = Spencer Age = 7p1 Name = Alex Age = 9*/
}
p1
?和?p2
?的內存在線程堆棧上進行分配。 該內存隨聲明它的類型或方法一起回收。 這就是在賦值時復制結構的一個原因。 相比之下,當對類實例對象的所有引用都超出范圍時,為該類實例分配的內存將由公共語言運行時自動回收(垃圾回收)。 無法像在 C++ 中那樣明確地銷毀類對象。 有關 .NET 中的垃圾回收的詳細信息,請參閱垃圾回收。
備注
公共語言運行時中高度優(yōu)化了托管堆上內存的分配和釋放。 在大多數(shù)情況下,在堆上分配類實例與在堆棧上分配結構實例在性能成本上沒有顯著的差別。
05.02.02 對象標識與值相等性
在比較兩個對象是否相等時,首先必須明確是想知道兩個變量是否表示內存中的同一對象,還是想知道這兩個對象的一個或多個字段的值是否相等。 如果要對值進行比較,則必須考慮這兩個對象是值類型(結構)的實例,還是引用類型(類、委托、數(shù)組)的實例。
-
若要確定兩個類實例是否引用內存中的同一位置(這意味著它們具有相同的標識),可使用靜態(tài)?Object.Equals?方法。 (System.Object?是所有值類型和引用類型的隱式基類,其中包括用戶定義的結構和類。)
-
若要確定兩個結構實例中的實例字段是否具有相同的值,可使用?ValueType.Equals?方法。 由于所有結構都隱式繼承自?System.ValueType,因此可以直接在對象上調用該方法,如以下示例所示
// Person is defined in the previous example.//public struct Person //{ // public string Name; // public int Age; // public Person(string name, int age) // { // Name = name; // Age = age; // } //}Person p1 = new Person("Wallace", 75); Person p2 = new Person("", 42); p2.Name = "Wallace"; p2.Age = 75;if (p2.Equals(p1))Console.WriteLine("p2 and p1 have the same values.");// Output: p2 and p1 have the same values.
Equals
?的?System.ValueType?實現(xiàn)在某些情況下使用裝箱和反射。 若要了解如何提供特定于類型的高效相等性算法,請參閱如何為類型定義值相等性。 記錄是使用值語義實現(xiàn)相等性的引用類型。 -
若要確定兩個類實例中字段的值是否相等,可以使用?Equals?方法或?== 運算符。 但是,只有類通過重寫或重載提供關于那種類型對象的“相等”含義的自定義時,才能使用它們。 類也可能實現(xiàn)?IEquatable<T>?接口或?IEqualityComparer<T>?接口。 這兩個接口都提供可用于測試值相等性的方法。 設計好替代?
Equals
?的類后,請務必遵循如何為類型定義值相等性和?Object.Equals(Object)?中介紹的準則。
05.03?繼承 - 派生用于創(chuàng)建更具體的行為的類型
繼承(以及封裝和多態(tài)性)是面向對象的編程的三個主要特征之一。 通過繼承,可以創(chuàng)建新類,以便重用、擴展和修改在其他類中定義的行為。 其成員被繼承的類稱為“基類”,繼承這些成員的類稱為“派生類”。 派生類只能有一個直接基類。 但是,繼承是可傳遞的。 如果?ClassC
?派生自?ClassB
,并且?ClassB
?派生自?ClassA
,則?ClassC
?將繼承在?ClassB
?和?ClassA
?中聲明的成員。
備注
結構不支持繼承,但它們可以實現(xiàn)接口。
從概念上講,派生類是基類的專門化。 例如,如果有一個基類?Animal
,則可以有一個名為?Mammal
?的派生類,以及另一個名為?Reptile
?的派生類。?Mammal
?是?Animal
,Reptile
?也是?Animal
,但每個派生類表示基類的不同專門化。
接口聲明可以為其成員定義默認實現(xiàn)。 這些實現(xiàn)通過派生接口和實現(xiàn)這些接口的類來繼承。 有關默認接口方法的詳細信息,請參閱關于接口的文章。
定義要從其他類派生的類時,派生類會隱式獲得基類的所有成員(除了其構造函數(shù)和終結器)。 派生類可以重用基類中的代碼,而無需重新實現(xiàn)。 可以在派生類中添加更多成員。 派生類擴展了基類的功能。
下圖顯示一個類?WorkItem
,它表示某個業(yè)務流程中的工作項。 像所有類一樣,它派生自?System.Object?且繼承其所有方法。?WorkItem
?會添加其自己的六個成員。 這些成員中包括一個構造函數(shù),因為不會繼承構造函數(shù)。 類?ChangeRequest
?繼承自?WorkItem
,表示特定類型的工作項。?ChangeRequest
?將另外兩個成員添加到它從?WorkItem
?和?Object?繼承的成員中。 它必須添加自己的構造函數(shù),并且還添加了?originalItemID
。 屬性?originalItemID
?使?ChangeRequest
?實例可以與向其應用更改請求的原始?WorkItem
?相關聯(lián)。
下面的示例演示如何在 C# 中表示前面圖中所示的類關系。 該示例還演示了?WorkItem
?替代虛方法?Object.ToString?的方式,以及?ChangeRequest
?類繼承該方法的?WorkItem
?的實現(xiàn)方式。 第一個塊定義類:
// WorkItem implicitly inherits from the Object class.
public class WorkItem
{// Static field currentID stores the job ID of the last WorkItem that// has been created.private static int currentID;//Properties.protected int ID { get; set; }protected string Title { get; set; }protected string Description { get; set; }protected TimeSpan jobLength { get; set; }// Default constructor. If a derived class does not invoke a base-// class constructor explicitly, the default constructor is called// implicitly.public WorkItem(){ID = 0;Title = "Default title";Description = "Default description.";jobLength = new TimeSpan();}// Instance constructor that has three parameters.public WorkItem(string title, string desc, TimeSpan joblen){this.ID = GetNextID();this.Title = title;this.Description = desc;this.jobLength = joblen;}// Static constructor to initialize the static member, currentID. This// constructor is called one time, automatically, before any instance// of WorkItem or ChangeRequest is created, or currentID is referenced.static WorkItem() => currentID = 0;// currentID is a static field. It is incremented each time a new// instance of WorkItem is created.protected int GetNextID() => ++currentID;// Method Update enables you to update the title and job length of an// existing WorkItem object.public void Update(string title, TimeSpan joblen){this.Title = title;this.jobLength = joblen;}// Virtual method override of the ToString method that is inherited// from System.Object.public override string ToString() =>$"{this.ID} - {this.Title}";
}// ChangeRequest derives from WorkItem and adds a property (originalItemID)
// and two constructors.
public class ChangeRequest : WorkItem
{protected int originalItemID { get; set; }// Constructors. Because neither constructor calls a base-class// constructor explicitly, the default constructor in the base class// is called implicitly. The base class must contain a default// constructor.// Default constructor for the derived class.public ChangeRequest() { }// Instance constructor that has four parameters.public ChangeRequest(string title, string desc, TimeSpan jobLen,int originalID){// The following properties and the GetNexID method are inherited// from WorkItem.this.ID = GetNextID();this.Title = title;this.Description = desc;this.jobLength = jobLen;// Property originalItemID is a member of ChangeRequest, but not// of WorkItem.this.originalItemID = originalID;}
}
下一個塊顯示如何使用基類和派生類:
// Create an instance of WorkItem by using the constructor in the
// base class that takes three arguments.
WorkItem item = new WorkItem("Fix Bugs","Fix all bugs in my code branch",new TimeSpan(3, 4, 0, 0));// Create an instance of ChangeRequest by using the constructor in
// the derived class that takes four arguments.
ChangeRequest change = new ChangeRequest("Change Base Class Design","Add members to the class",new TimeSpan(4, 0, 0),1);// Use the ToString method defined in WorkItem.
Console.WriteLine(item.ToString());// Use the inherited Update method to change the title of the
// ChangeRequest object.
change.Update("Change the Design of the Base Class",new TimeSpan(4, 0, 0));// ChangeRequest inherits WorkItem's override of ToString.
Console.WriteLine(change.ToString());
/* Output:1 - Fix Bugs2 - Change the Design of the Base Class
*/
05.03.01 抽象方法和虛方法
基類將方法聲明為?virtual?時,派生類可以使用其自己的實現(xiàn)override該方法。 如果基類將成員聲明為?abstract,則必須在直接繼承自該類的任何非抽象類中重寫該方法。 如果派生類本身是抽象的,則它會繼承抽象成員而不會實現(xiàn)它們。 抽象和虛擬成員是多形性(面向對象的編程的第二個主要特征)的基礎。 有關詳細信息,請參閱多態(tài)性。
05.03.02 抽象基類
如果要通過使用?new?運算符來防止直接實例化,則可以將類聲明為抽象。 只有當一個新類派生自該類時,才能使用抽象類。 抽象類可以包含一個或多個本身聲明為抽象的方法簽名。 這些簽名指定參數(shù)和返回值,但沒有任何實現(xiàn)(方法體)。 抽象類不必包含抽象成員;但是,如果類包含抽象成員,則類本身必須聲明為抽象。 本身不抽象的派生類必須為來自抽象基類的任何抽象方法提供實現(xiàn)。
05.03.03 接口
接口是定義一組成員的引用類型。 實現(xiàn)該接口的所有類和結構都必須實現(xiàn)這組成員。 接口可以為其中任何成員或全部成員定義默認實現(xiàn)。 類可以實現(xiàn)多個接口,即使它只能派生自單個直接基類。
接口用于為類定義特定功能,這些功能不一定具有“is a (是)”關系。 例如,System.IEquatable<T>?接口可由任何類或結構實現(xiàn),以確定該類型的兩個對象是否等效(但是由該類型定義等效性)。?IEquatable<T>?不表示基類和派生類之間存在的同一種“是”關系(例如,Mammal
?是?Animal
)。 有關詳細信息,請參閱接口。
05.03.04 防止進一步派生
類可以通過將自己或成員聲明為?sealed,來防止其他類繼承自它或繼承自其任何成員。
05.03.05 基類成員的派生類隱藏
派生類可以通過使用相同名稱和簽名聲明成員來隱藏基類成員。?new?修飾符可以用于顯式指示成員不應作為基類成員的重寫。 使用?new?不是必需的,但如果未使用?new,則會產(chǎn)生編譯器警告。 有關詳細信息,請參閱使用 Override 和 New 關鍵字進行版本控制和了解何時使用 Override 和 New 關鍵字。
05.03?多形性
多態(tài)性常被視為自封裝和繼承之后,面向對象的編程的第三個支柱。 Polymorphism(多態(tài)性)是一個希臘詞,指“多種形態(tài)”,多態(tài)性具有兩個截然不同的方面:
- 在運行時,在方法參數(shù)和集合或數(shù)組等位置,派生類的對象可以作為基類的對象處理。 在出現(xiàn)此多形性時,該對象的聲明類型不再與運行時類型相同。
- 基類可以定義并實現(xiàn)虛方法,派生類可以重寫這些方法,即派生類提供自己的定義和實現(xiàn)。 在運行時,客戶端代碼調用該方法,CLR 查找對象的運行時類型,并調用虛方法的重寫方法。 你可以在源代碼中調用基類的方法,執(zhí)行該方法的派生類版本。
虛方法允許你以統(tǒng)一方式處理多組相關的對象。 例如,假定你有一個繪圖應用程序,允許用戶在繪圖圖面上創(chuàng)建各種形狀。 你在編譯時不知道用戶將創(chuàng)建哪些特定類型的形狀。 但應用程序必須跟蹤創(chuàng)建的所有類型的形狀,并且必須更新這些形狀以響應用戶鼠標操作。 你可以使用多態(tài)性通過兩個基本步驟解決這一問題:
- 創(chuàng)建一個類層次結構,其中每個特定形狀類均派生自一個公共基類。
- 使用虛方法通過對基類方法的單個調用來調用任何派生類上的相應方法。
首先,創(chuàng)建一個名為?Rectangle
Shape
?的基類,并創(chuàng)建一些派生類,例如?Triangle
Circle
、 和 。 為?Shape
?類提供一個名為?Draw
?的虛擬方法,并在每個派生類中重寫該方法以繪制該類表示的特定形狀。 創(chuàng)建?List<Shape>
?對象,并向其添加?Circle
、Triangle
?和?Rectangle
。
public class Shape
{// A few example memberspublic int X { get; private set; }public int Y { get; private set; }public int Height { get; set; }public int Width { get; set; }// Virtual methodpublic virtual void Draw(){Console.WriteLine("Performing base class drawing tasks");}
}public class Circle : Shape
{public override void Draw(){// Code to draw a circle...Console.WriteLine("Drawing a circle");base.Draw();}
}
public class Rectangle : Shape
{public override void Draw(){// Code to draw a rectangle...Console.WriteLine("Drawing a rectangle");base.Draw();}
}
public class Triangle : Shape
{public override void Draw(){// Code to draw a triangle...Console.WriteLine("Drawing a triangle");base.Draw();}
}
若要更新繪圖圖面,請使用?foreach?循環(huán)對該列表進行循環(huán)訪問,并對其中的每個?Shape
?對象調用?Draw
?方法。 雖然列表中的每個對象都具有聲明類型?Shape
,但調用的將是運行時類型(該方法在每個派生類中的重寫版本)。
// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used wherever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
var shapes = new List<Shape>
{new Rectangle(),new Triangle(),new Circle()
};// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (var shape in shapes)
{shape.Draw();
}
/* Output:Drawing a rectanglePerforming base class drawing tasksDrawing a trianglePerforming base class drawing tasksDrawing a circlePerforming base class drawing tasks
*/
在 C# 中,每個類型都是多態(tài)的,因為包括用戶定義類型在內的所有類型都繼承自?Object。
05.03.01 多形性概述
05.03.02 虛擬成員
當派生類從基類繼承時,它包括基類的所有成員。 基類中聲明的所有行為都是派生類的一部分。 這使派生類的對象能夠被視為基類的對象。 訪問修飾符(public
、protected
、private
?等)確定是否可以從派生類實現(xiàn)訪問這些成員。 通過虛擬方法,設計器可以選擇不同的派生類行為:
- 派生類可以重寫基類中的虛擬成員,并定義新行為。
- 派生類可能會繼承最接近的基類方法而不重寫方法,同時保留現(xiàn)有的行為,但允許進一步派生的類重寫方法。
- 派生類可以定義隱藏基類實現(xiàn)的成員的新非虛實現(xiàn)。
僅當基類成員聲明為?virtual?或?abstract?時,派生類才能重寫基類成員。 派生成員必須使用?override?關鍵字顯式指示該方法將參與虛調用。 以下代碼提供了一個示例:
public class BaseClass
{public virtual void DoWork() { }public virtual int WorkProperty{get { return 0; }}
}
public class DerivedClass : BaseClass
{public override void DoWork() { }public override int WorkProperty{get { return 0; }}
}
字段不能是虛擬的,只有方法、屬性、事件和索引器才可以是虛擬的。 當派生類重寫某個虛擬成員時,即使該派生類的實例被當作基類的實例訪問,也會調用該成員。 以下代碼提供了一個示例:
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.BaseClass A = B;
A.DoWork(); // Also calls the new method.
虛方法和屬性允許派生類擴展基類,而無需使用方法的基類實現(xiàn)。 有關詳細信息,請參閱使用 Override 和 New 關鍵字進行版本控制。 接口提供另一種方式來定義將實現(xiàn)留給派生類的方法或方法集。
05.03.03 使用新成員隱藏基類成員
如果希望派生類具有與基類中的成員同名的成員,則可以使用?new?關鍵字隱藏基類成員。?new
?關鍵字放置在要替換的類成員的返回類型之前。 以下代碼提供了一個示例:
public class BaseClass
{public void DoWork() { WorkField++; }public int WorkField;public int WorkProperty{get { return 0; }}
}public class DerivedClass : BaseClass
{public new void DoWork() { WorkField++; }public new int WorkField;public new int WorkProperty{get { return 0; }}
}
通過將派生類的實例強制轉換為基類的實例,可以從客戶端代碼訪問隱藏的基類成員。 例如:
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.BaseClass A = (BaseClass)B;
A.DoWork(); // Calls the old method.
05.03.04 阻止派生類重寫虛擬成員
無論在虛擬成員和最初聲明虛擬成員的類之間已聲明了多少個類,虛擬成員都是虛擬的。 如果類?A
?聲明了一個虛擬成員,類?B
?從?A
?派生,類?C
?從類?B
?派生,則不管類?B
?是否為虛擬成員聲明了重寫,類?C
?都會繼承該虛擬成員,并可以重寫它。 以下代碼提供了一個示例:
public class A
{public virtual void DoWork() { }
}
public class B : A
{public override void DoWork() { }
}
派生類可以通過將重寫聲明為?sealed?來停止虛擬繼承。 停止繼承需要在類成員聲明中的?override
?關鍵字前面放置?sealed
?關鍵字。 以下代碼提供了一個示例:
public class C : B
{public sealed override void DoWork() { }
}
在上一個示例中,方法?DoWork
?對從?C
?派生的任何類都不再是虛擬方法。 即使它們轉換為類型?B
?或類型?A
,它對于?C
?的實例仍然是虛擬的。 通過使用?new
?關鍵字,密封的方法可以由派生類替換,如下面的示例所示:
public class D : C
{public new void DoWork() { }
}
在此情況下,如果在?D
?中使用類型為?D
?的變量調用?DoWork
,被調用的將是新的?DoWork
。 如果使用類型為?C
、B
?或?A
?的變量訪問?D
?的實例,對?DoWork
?的調用將遵循虛擬繼承的規(guī)則,即把這些調用傳送到類?C
?的?DoWork
?實現(xiàn)。
05.03.05 從派生類訪問基類虛擬成員
已替換或重寫某個方法或屬性的派生類仍然可以使用?base
?關鍵字訪問基類的該方法或屬性。 以下代碼提供了一個示例:
public class Base
{public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{public override void DoWork(){//Perform Derived's work here//...// Call DoWork on base classbase.DoWork();}
}
有關詳細信息,請參閱?base。
?備注
建議虛擬成員在它們自己的實現(xiàn)中使用?base
?來調用該成員的基類實現(xiàn)。 允許基類行為發(fā)生使得派生類能夠集中精力實現(xiàn)特定于派生類的行為。 未調用基類實現(xiàn)時,由派生類負責使它們的行為與基類的行為兼容。
06?異常和異常處理
C# 語言的異常處理功能有助于處理在程序運行期間發(fā)生的任何意外或異常情況。 異常處理功能使用?try
、catch
?和?finally
?關鍵字來嘗試執(zhí)行可能失敗的操作、在你確定合理的情況下處理故障,以及在事后清除資源。 公共語言運行時 (CLR)、.NET/第三方庫或應用程序代碼都可生成異常。 異常是使用?throw
?關鍵字創(chuàng)建而成。
在許多情況下,異常并不是由代碼直接調用的方法拋出,而是由調用堆棧中再往下的另一方法拋出。 如果發(fā)生這種異常,CLR 會展開堆棧,同時針對特定異常類型查找包含?catch
?代碼塊的方法,并執(zhí)行它找到的首個此類?catch
?代碼塊。 如果在調用堆棧中找不到相應的?catch
?代碼塊,將會終止進程并向用戶顯示消息。
在以下示例中,方法用于測試除數(shù)是否為零,并捕獲相應的錯誤。 如果沒有異常處理功能,此程序將終止,并顯示?DivideByZeroException was unhandled?錯誤。
public class ExceptionTest
{static double SafeDivision(double x, double y){if (y == 0)throw new DivideByZeroException();return x / y;}public static void Main(){// Input for test purposes. Change the values to see// exception handling behavior.double a = 98, b = 0;double result;try{result = SafeDivision(a, b);Console.WriteLine("{0} divided by {1} = {2}", a, b, result);}catch (DivideByZeroException){Console.WriteLine("Attempted divide by zero.");}}
}
06.01 異常概述
異常具有以下屬性:
- 異常是最終全都派生自?
System.Exception
?的類型。 - 在可能拋出異常的語句周圍使用?
try
?代碼塊。 - 在?
try
?代碼塊中出現(xiàn)異常后,控制流會跳轉到調用堆棧中任意位置上的首個相關異常處理程序。 在 C# 中,catch
?關鍵字用于定義異常處理程序。 - 如果給定的異常沒有對應的異常處理程序,那么程序會停止執(zhí)行,并顯示錯誤消息。
- 除非可以處理異常并讓應用程序一直處于已知狀態(tài),否則不捕獲異常。 如果捕獲?
System.Exception
,使用?catch
?代碼塊末尾的?throw
?關鍵字重新拋出異常。 - 如果?
catch
?代碼塊定義異常變量,可以用它來詳細了解所發(fā)生的異常類型。 - 使用?
throw
?關鍵字,程序可以顯式生成異常。 - 異常對象包含錯誤詳細信息,如調用堆棧的狀態(tài)和錯誤的文本說明。
- 即使引發(fā)異常,
finally
?代碼塊中的代碼仍會執(zhí)行。 使用?finally
?代碼塊可釋放資源。例如,關閉在?try
?代碼塊中打開的任何流或文件。 - .NET 中的托管異常在 Win32 結構化異常處理機制的基礎之上實現(xiàn)。 有關詳細信息,請參閱結構化異常處理 (C/C++)?和速成教程:深入了解 Win32 結構化異常處理。
06.02 使用異常
在 C# 中,程序中的運行時錯誤通過使用一種稱為“異?!钡臋C制在程序中傳播。 異常由遇到錯誤的代碼引發(fā),由能夠更正錯誤的代碼捕捉。 異??捎?.NET 運行時或由程序中的代碼引發(fā)。 一旦引發(fā)了一個異常,此異常會在調用堆棧中傳播,直到找到針對它的?catch
?語句。 未捕獲的異常由系統(tǒng)提供的通用異常處理程序處理,該處理程序會顯示一個對話框。
異常由從?Exception?派生的類表示。 此類標識異常的類型,并包含詳細描述異常的屬性。 引發(fā)異常涉及創(chuàng)建異常派生類的實例,配置異常的屬性(可選),然后使用?throw
?關鍵字引發(fā)該對象。 例如:
class CustomException : Exception
{public CustomException(string message){}
}
private static void TestThrow()
{throw new CustomException("Custom exception in TestThrow()");
}
引發(fā)異常后,運行時將檢查當前語句,以確定它是否在?try
?塊內。 如果在,則將檢查與?try
?塊關聯(lián)的所有?catch
?塊,以確定它們是否可以捕獲該異常。?Catch
?塊通常會指定異常類型;如果該?catch
?塊的類型與異常或異常的基類的類型相同,則該?catch
?塊可處理該方法。 例如:
try
{TestThrow();
}
catch (CustomException ex)
{System.Console.WriteLine(ex.ToString());
}
如果引發(fā)異常的語句不在?try
?塊內或者包含該語句的?try
?塊沒有匹配的?catch
?塊,則運行時將檢查調用方法中是否有?try
?語句和?catch
?塊。 運行時將繼續(xù)調用堆棧,搜索兼容的?catch
?塊。 在找到并執(zhí)行?catch
?塊之后,控制權將傳遞給?catch
?塊之后的下一個語句。
一個?try
?語句可包含多個?catch
?塊。 將執(zhí)行第一個能夠處理該異常的?catch
?語句;將忽略任何后續(xù)的?catch
?語句,即使它們是兼容的也是如此。 按從最具有針對性(或派生程度最高)到最不具有針對性的順序對 catch 塊排列。 例如:
using System;
using System.IO;namespace Exceptions
{public class CatchOrder{public static void Main(){try{using (var sw = new StreamWriter("./test.txt")){sw.WriteLine("Hello");}}// Put the more specific exceptions first.catch (DirectoryNotFoundException ex){Console.WriteLine(ex);}catch (FileNotFoundException ex){Console.WriteLine(ex);}// Put the least specific exception last.catch (IOException ex){Console.WriteLine(ex);}Console.WriteLine("Done");}}
}
執(zhí)行?catch
?塊之前,運行時會檢查?finally
?塊。?Finally
?塊使程序員可以清除中止的?try
?塊可能遺留下的任何模糊狀態(tài),或者釋放任何外部資源(例如圖形句柄、數(shù)據(jù)庫連接或文件流),而無需等待垃圾回收器在運行時完成這些對象。 例如:
static void TestFinally()
{FileStream? file = null;//Change the path to something that works on your machine.FileInfo fileInfo = new System.IO.FileInfo("./file.txt");try{file = fileInfo.OpenWrite();file.WriteByte(0xF);}finally{// Closing the file allows you to reopen it immediately - otherwise IOException is thrown.file?.Close();}try{file = fileInfo.OpenWrite();Console.WriteLine("OpenWrite() succeeded");}catch (IOException){Console.WriteLine("OpenWrite() failed");}
}
如果?WriteByte()
?引發(fā)了異常并且未調用?file.Close()
,則第二個?try
?塊中嘗試重新打開文件的代碼將會失敗,并且文件將保持鎖定狀態(tài)。 由于即使引發(fā)異常也會執(zhí)行?finally
?塊,前一示例中的?finally
?塊可使文件正確關閉,從而有助于避免錯誤。
如果引發(fā)異常之后沒有在調用堆棧上找到兼容的?catch
?塊,則會出現(xiàn)以下三種情況之一:
- 如果異常存在于終結器內,將中止終結器,并調用基類終結器(如果有)。
- 如果調用堆棧包含靜態(tài)構造函數(shù)或靜態(tài)字段初始值設定項,將引發(fā)?TypeInitializationException,同時將原始異常分配給新異常的?InnerException?屬性。
- 如果到達線程的開頭,則終止線程。
06.03 異常處理
C# 程序員使用?try?塊來對可能受異常影響的代碼進行分區(qū)。 關聯(lián)的?catch?塊用于處理生成的任何異常。?finally?塊包含無論?try
?塊中是否引發(fā)異常都會運行的代碼,如發(fā)布?try
?塊中分配的資源。?try
?塊需要一個或多個關聯(lián)的?catch
?塊或一個?finally
?塊,或兩者皆之。
下面的示例演示?try-catch
?語句、try-finally
?語句和?try-catch-finally
?語句。
try
{// Code to try goes here.
}
catch (SomeSpecificException ex)
{// Code to handle the exception goes here.// Only catch exceptions that you know how to handle.// Never catch base class System.Exception without// rethrowing it at the end of the catch block.
}
C#
try
{// Code to try goes here.
}
finally
{// Code to execute after the try block goes here.
}
C#
try
{// Code to try goes here.
}
catch (SomeSpecificException ex)
{// Code to handle the exception goes here.
}
finally
{// Code to execute after the try (and possibly catch) blocks// goes here.
}
一個不具有?catch
?或?finally
?塊的?try
?塊會導致編譯器錯誤。
06.03.01 catch 塊
catch
?塊可以指定要捕獲的異常的類型。 該類型規(guī)范稱為異常篩選器。 異常類型應派生自?Exception。 一般情況下,不要將?Exception?指定為異常篩選器,除非了解如何處理可能在?try
?塊中引發(fā)的所有異常,或者已在?catch
?塊的末尾處包括了?throw?語句。
可將具有不同異常類的多個?catch
?塊鏈接在一起。 代碼中?catch
?塊的計算順序為從上到下,但針對引發(fā)的每個異常,僅執(zhí)行一個?catch
?塊。 將執(zhí)行指定所引發(fā)的異常的確切類型或基類的第一個?catch
?塊。 如果沒有?catch
?塊指定匹配的異常類,則將選擇不具有類型的?catch
?塊(如果語句中存在)。 務必首先定位具有最具體的(即,最底層派生的)異常類的?catch
?塊。
當以下條件為 true 時,捕獲異常:
- 能夠很好地理解可能會引發(fā)異常的原因,并且可以實現(xiàn)特定的恢復,例如捕獲?FileNotFoundException?對象時提示用戶輸入新文件名。
- 可以創(chuàng)建和引發(fā)一個新的、更具體的異常。
int GetInt(int[] array, int index) {try{return array[index];}catch (IndexOutOfRangeException e){throw new ArgumentOutOfRangeException("Parameter index is out of range.", e);} }
- 想要先對異常進行部分處理,然后再將其傳遞以進行更多處理。 在下面的示例中,
catch
?塊用于在重新引發(fā)異常之前將條目添加到錯誤日志。try {// Try to access a resource. } catch (UnauthorizedAccessException e) {// Call a custom error logging procedure.LogError(e);// Re-throw the error.throw; }
還可以指定異常篩選器,以向 catch 子句添加布爾表達式。 異常篩選器表明僅當條件為 true 時,特定 catch 子句才匹配。 在以下示例中,兩個 catch 子句均使用相同的異常類,但是會檢查其他條件以創(chuàng)建不同的錯誤消息:
int GetInt(int[] array, int index)
{try{return array[index];}catch (IndexOutOfRangeException e) when (index < 0) {throw new ArgumentOutOfRangeException("Parameter index cannot be negative.", e);}catch (IndexOutOfRangeException e){throw new ArgumentOutOfRangeException("Parameter index cannot be greater than the array size.", e);}
}
始終返回?false
?的異常篩選器可用于檢查所有異常,但不可用于處理異常。 典型用途是記錄異常:
public class ExceptionFilter
{public static void Main(){try{string? s = null;Console.WriteLine(s.Length);}catch (Exception e) when (LogException(e)){}Console.WriteLine("Exception must have been handled");}private static bool LogException(Exception e){Console.WriteLine($"\tIn the log routine. Caught {e.GetType()}");Console.WriteLine($"\tMessage: {e.Message}");return false;}
}
LogException
?方法始終返回?false
,使用此異常篩選器的?catch
?子句均不匹配。 catch 子句可以是通用的,使用?System.Exception后面的子句可以處理更具體的異常類。
06.03.02 finally 塊
finally
?塊讓你可以清理在?try
?塊中所執(zhí)行的操作。 如果存在?finally
?塊,將在執(zhí)行?try
?塊和任何匹配的?catch
?塊之后,最后執(zhí)行它。 無論是否會引發(fā)異?;蛘业狡ヅ洚惓n愋偷?catch
?塊,finally
?塊都將始終運行。
finally
?塊可用于發(fā)布資源(如文件流、數(shù)據(jù)庫連接和圖形句柄)而無需等待運行時中的垃圾回收器來完成對象。
在下面的示例中,finally
?塊用于關閉在?try
?塊中打開的文件。 請注意,在關閉文件之前,將檢查文件句柄的狀態(tài)。 如果?try
?塊不能打開文件,則文件句柄仍將具有值?null
?且?finally
?塊不會嘗試將其關閉。 或者,如果在?try
?塊中成功打開文件,則?finally
?塊將關閉打開的文件。
FileStream? file = null;
FileInfo fileinfo = new System.IO.FileInfo("./file.txt");
try
{file = fileinfo.OpenWrite();file.WriteByte(0xF);
}
finally
{// Check for null because OpenWrite might have failed.file?.Close();
}
有關詳細信息,請參閱?C# 語言規(guī)范中的異常和?try 語句。 該語言規(guī)范是 C# 語法和用法的權威資料。
06.04 創(chuàng)建和引發(fā)異常
異常用于指示在運行程序時發(fā)生了錯誤。 此時將創(chuàng)建一個描述錯誤的異常對象,然后使用?throw?語句或表達式引發(fā)。 然后,運行時搜索最兼容的異常處理程序。
當存在下列一種或多種情況時,程序員應引發(fā)異常:
-
方法無法完成其定義的功能。 例如,如果一種方法的參數(shù)具有無效的值:
static void CopyObject(SampleClass original) {_ = original ?? throw new ArgumentException("Parameter cannot be null", nameof(original)); }
-
根據(jù)對象的狀態(tài),對某個對象進行不適當?shù)恼{用。 一個示例可能是嘗試寫入只讀文件。 在對象狀態(tài)不允許操作的情況下,引發(fā)?InvalidOperationException?的實例或基于此類的派生的對象。 以下代碼是引發(fā)?InvalidOperationException?對象的方法示例:
public class ProgramLog {FileStream logFile = null!;public void OpenLog(FileInfo fileName, FileMode mode) { }public void WriteLog(){if (!logFile.CanWrite){throw new InvalidOperationException("Logfile cannot be read-only");}// Else write data to the log and return.} }
-
方法的參數(shù)引發(fā)了異常。 在這種情況下,應捕獲原始異常,并創(chuàng)建?ArgumentException?實例。 應將原始異常作為?InnerException?參數(shù)傳遞給?ArgumentException?的構造函數(shù):
static int GetValueFromArray(int[] array, int index) {try{return array[index];}catch (IndexOutOfRangeException e){throw new ArgumentOutOfRangeException("Parameter index is out of range.", e);} }
?備注
前面的示例演示了如何使用?
InnerException
?屬性。 這是有意簡化的。 在實踐中,應先檢查索引是否在范圍內,然后再使用它。 當參數(shù)成員引發(fā)在調用成員之前無法預料到的異常時,可以使用此方法來包裝異常。
異常包含一個名為?StackTrace?的屬性。 此字符串包含當前調用堆棧上的方法的名稱,以及為每個方法引發(fā)異常的位置(文件名和行號)。?StackTrace?對象由公共語言運行時 (CLR) 從?throw
?語句的位置點自動創(chuàng)建,因此必須從堆棧跟蹤的開始點引發(fā)異常。
所有異常都包含一個名為?Message?的屬性。 應設置此字符串來解釋發(fā)生異常的原因。 不應將安全敏感的信息放在消息文本中。 除?Message?以外,ArgumentException?也包含一個名為?ParamName?的屬性,應將該屬性設置為導致引發(fā)異常的參數(shù)的名稱。 在屬性資源庫中,ParamName?應設置為?value
。
公共的受保護方法在無法完成其預期功能時將引發(fā)異常。 引發(fā)的異常類是符合錯誤條件的最具體的可用異常。 這些異常應編寫為類功能的一部分,并且原始類的派生類或更新應保留相同的行為以實現(xiàn)后向兼容性。
06.04.01 引發(fā)異常時應避免的情況
以下列表標識了引發(fā)異常時要避免的做法:
- 不要使用異常在正常執(zhí)行過程中更改程序的流。 使用異常來報告和處理錯誤條件。
- 只能引發(fā)異常,而不能作為返回值或參數(shù)返回異常。
- 請勿有意從自己的源代碼中引發(fā)?System.Exception、System.SystemException、System.NullReferenceException?或?System.IndexOutOfRangeException。
- 不要創(chuàng)建可在調試模式下引發(fā),但不會在發(fā)布模式下引發(fā)的異常。 若要在開發(fā)階段確定運行時錯誤,請改用調試斷言。
06.04.02 任務返回方法中的異常
使用?async
?修飾符聲明的方法在出現(xiàn)異常時,有一些特殊的注意事項。 方法?async
?中引發(fā)的異常會存儲在返回的任務中,直到任務即將出現(xiàn)時才會出現(xiàn)。 有關存儲的異常的詳細信息,請參閱異步異常。
建議在輸入方法的異步部分之前驗證參數(shù)并引發(fā)任何相應的異常,例如?ArgumentException?和?ArgumentNullException。 也就是說,在開始工作之前,這些驗證異常應同步出現(xiàn)。 以下代碼片段演示了一個示例,其中,如果引發(fā)異常,ArgumentException?個異常將同步出現(xiàn),而?InvalidOperationException?個將存儲在返回的任務中。
C#復制
// Non-async, task-returning method.
// Within this method (but outside of the local function),
// any thrown exceptions emerge synchronously.
public static Task<Toast> ToastBreadAsync(int slices, int toastTime)
{if (slices is < 1 or > 4){throw new ArgumentException("You must specify between 1 and 4 slices of bread.",nameof(slices));}if (toastTime < 1){throw new ArgumentException("Toast time is too short.", nameof(toastTime));}return ToastBreadAsyncCore(slices, toastTime);// Local async function.// Within this function, any thrown exceptions are stored in the task.static async Task<Toast> ToastBreadAsyncCore(int slices, int time){for (int slice = 0; slice < slices; slice++){Console.WriteLine("Putting a slice of bread in the toaster");}// Start toasting.await Task.Delay(time);if (time > 2_000){throw new InvalidOperationException("The toaster is on fire!");}Console.WriteLine("Toast is ready!");return new Toast();}
}
06.04.03 定義異常的類別
程序可以引發(fā)?System?命名空間中的預定義異常類(前面提到的情況除外),或通過從?Exception?派生來創(chuàng)建其自己的異常類。 派生類應該至少定義三個構造函數(shù):一個無參數(shù)構造函數(shù)、一個用于設置消息屬性,還有一個用于設置?Message?和?InnerException?屬性。 例如:
C#復制
[Serializable]
public class InvalidDepartmentException : Exception
{public InvalidDepartmentException() : base() { }public InvalidDepartmentException(string message) : base(message) { }public InvalidDepartmentException(string message, Exception inner) : base(message, inner) { }
}
當新屬性提供的數(shù)據(jù)有助于解決異常時,將新屬性添加到異常類中。 如果將新屬性添加到派生異常類中,則應替代?ToString()
?以返回添加的信息。
06.04?編譯器生成的異常
當基本操作失敗時,.NET 運行時會自動引發(fā)一些異常。 這些異常及其錯誤條件在下表中列出。
例外 | 描述 |
---|---|
ArithmeticException | 算術運算期間出現(xiàn)的異常的基類,例如?DivideByZeroException?和?OverflowException。 |
ArrayTypeMismatchException | 由于元素的實際類型與數(shù)組的實際類型不兼容而導致數(shù)組無法存儲給定元素時引發(fā)。 |
DivideByZeroException | 嘗試將整數(shù)值除以零時引發(fā)。 |
IndexOutOfRangeException | 索引小于零或超出數(shù)組邊界時,嘗試對數(shù)組編制索引時引發(fā)。 |
InvalidCastException | 從基類型顯式轉換為接口或派生類型在運行時失敗時引發(fā)。 |
NullReferenceException | 嘗試引用值為?null?的對象時引發(fā)。 |
OutOfMemoryException | 嘗試使用新運算符分配內存失敗時引發(fā)。 此異常表示可用于公共語言運行時的內存已用盡。 |
OverflowException | checked ?上下文中的算術運算溢出時引發(fā)。 |
StackOverflowException | 執(zhí)行堆棧由于有過多掛起的方法調用而用盡時引發(fā);通常表示非常深的遞歸或無限遞歸。 |
TypeInitializationException | 靜態(tài)構造函數(shù)引發(fā)異常并且沒有兼容的?catch ?子句來捕獲異常時引發(fā)。 |
07 C#語法的骨架,語句概要(更詳細的在后面)
07.01?語句概要
程序執(zhí)行的操作采用語句表達。 常見操作包括聲明變量、賦值、調用方法、循環(huán)訪問集合,以及根據(jù)給定條件分支到一個或另一個代碼塊。 語句在程序中的執(zhí)行順序稱為“控制流”或“執(zhí)行流”。 根據(jù)程序對運行時所收到的輸入的響應,在程序每次運行時控制流可能有所不同。
語句可以是以分號結尾的單行代碼,也可以是語句塊中的一系列單行語句。 語句塊括在括號 {} 中,并且可以包含嵌套塊。 以下代碼演示了兩個單行語句示例和一個多行語句塊:
public static void Main(){// Declaration statement.int counter;// Assignment statement.counter = 1;// Error! This is an expression, not an expression statement.// counter + 1;// Declaration statements with initializers are functionally// equivalent to declaration statement followed by assignment statement:int[] radii = [15, 32, 108, 74, 9]; // Declare and initialize an array.const double pi = 3.14159; // Declare and initialize constant.// foreach statement block that contains multiple statements.foreach (int radius in radii){// Declaration statement with initializer.double circumference = pi * (2 * radius);// Expression statement (method invocation). A single-line// statement can span multiple text lines because line breaks// are treated as white space, which is ignored by the compiler.System.Console.WriteLine("Radius of circle #{0} is {1}. Circumference = {2:N2}",counter, radius, circumference);// Expression statement (postfix increment).counter++;} // End of foreach statement block} // End of Main method body.
} // End of SimpleStatements class.
/*Output:Radius of circle #1 = 15. Circumference = 94.25Radius of circle #2 = 32. Circumference = 201.06Radius of circle #3 = 108. Circumference = 678.58Radius of circle #4 = 74. Circumference = 464.96Radius of circle #5 = 9. Circumference = 56.55
*/
07.02 語句的類型
下表列出了 C# 中的各種語句類型及其關聯(lián)的關鍵字,并提供指向包含詳細信息的主題的鏈接:
類別 | C# 關鍵字/說明 |
---|---|
聲明語句 | 聲明語句引入新的變量或常量。 變量聲明可以選擇為變量賦值。 在常量聲明中必須賦值。 |
表達式語句 | 用于計算值的表達式語句必須在變量中存儲該值。 |
選擇語句 | 選擇語句用于根據(jù)一個或多個指定條件分支到不同的代碼段。 有關詳情,請參閱以下主題:
|
迭代語句 | 迭代語句用于遍歷集合(如數(shù)組),或重復執(zhí)行同一組語句直到滿足指定的條件。 有關詳情,請參閱以下主題:
|
跳轉語句 | 跳轉語句將控制轉移給另一代碼段。 有關詳情,請參閱以下主題:
|
異常處理語句 | 異常處理語句用于從運行時發(fā)生的異常情況正常恢復。 有關詳情,請參閱以下主題:
|
checked?和?unchecked | checked ?和?unchecked ?語句用于指定將結果存儲在變量中、但該變量過小而不能容納結果值時,是否允許整型數(shù)值運算導致溢出。 |
await ?語句 | 如果用?async?修飾符標記方法,則可以使用該方法中的?await?運算符。 在控制到達異步方法的?await ?表達式時,控制將返回到調用方,該方法中的進程將掛起,直到等待的任務完成為止。 任務完成后,可以在方法中恢復執(zhí)行。有關簡單示例,請參閱方法的“異步方法”一節(jié)。 有關詳細信息,請參閱?async 和 await 的異步編程。 |
yield return ?語句 | 迭代器對集合執(zhí)行自定義迭代,如列表或數(shù)組。 迭代器使用?yield return?語句返回元素,每次返回一個。 到達?yield return ?語句時,會記住當前在代碼中的位置。 下次調用迭代器時,將從該位置重新開始執(zhí)行。有關更多信息,請參見?迭代器。 |
fixed ?語句 | fixed 語句禁止垃圾回收器重定位可移動的變量。 有關詳細信息,請參閱?fixed。 |
lock ?語句 | lock 語句用于限制一次僅允許一個線程訪問代碼塊。 有關詳細信息,請參閱?lock。 |
帶標簽的語句 | 可以為語句指定一個標簽,然后使用?goto?關鍵字跳轉到該帶標簽的語句。 (參見下一行中的示例。) |
空語句 | 空語句只含一個分號。 不執(zhí)行任何操作,可以在需要語句但不需要執(zhí)行任何操作的地方使用。 |
07.02.01 聲明語句
以下代碼顯示了具有和不具有初始賦值的變量聲明的示例,以及具有必要初始化的常量聲明。
// Variable declaration statements.
double area;
double radius = 2;// Constant declaration statement.
const double pi = 3.14159;
07.02.02 表達式語句
以下代碼顯示了表達式語句的示例,包括賦值、使用賦值創(chuàng)建對象和方法調用。
C#
// Expression statement (assignment).
area = 3.14 * (radius * radius);// Error. Not statement because no assignment:
//circ * 2;// Expression statement (method invocation).
System.Console.WriteLine();// Expression statement (new object creation).
System.Collections.Generic.List<string> strings =new System.Collections.Generic.List<string>();
07.02.03 空語句
以下示例演示了空語句的兩種用法:
C#
void ProcessMessages()
{while (ProcessMessage()); // Statement needed here.
}void F()
{//...if (done) goto exit;
//...
exit:; // Statement needed here.
}
07.02.04 嵌入式語句
某些語句(如迭代語句)后面始終跟有一條嵌入式語句。 此嵌入式語句可以是單個語句,也可以是語句塊中括在括號 {} 內的多個語句。 甚至可以在括號 {} 內包含單行嵌入式語句,如以下示例所示:
C#
// Recommended style. Embedded statement in block.
foreach (string s in System.IO.Directory.GetDirectories(System.Environment.CurrentDirectory))
{System.Console.WriteLine(s);
}// Not recommended.
foreach (string s in System.IO.Directory.GetDirectories(System.Environment.CurrentDirectory))System.Console.WriteLine(s);
未括在括號 {} 內的嵌入式語句不能作為聲明語句或帶標簽的語句。 下面的示例對此進行了演示:
C#
if(pointB == true)//Error CS1023:int radius = 5;
將該嵌入式語句放在語句塊中以修復錯誤:
C#
if (b == true)
{// OK:System.DateTime d = System.DateTime.Now;System.Console.WriteLine(d.ToLongDateString());
}
07.02.05 嵌套語句塊
語句塊可以嵌套,如以下代碼所示:
C#
foreach (string s in System.IO.Directory.GetDirectories(System.Environment.CurrentDirectory))
{if (s.StartsWith("CSharp")){if (s.EndsWith("TempFolder")){return s;}}
}
return "Not found.";
07.02.06 無法訪問的語句
如果編譯器認為在任何情況下控制流都無法到達特定語句,將生成警告 CS0162,如下例所示:
C#
// An over-simplified example of unreachable code.
const int val = 5;
if (val < 4)
{System.Console.WriteLine("I'll never write anything."); //CS0162
}
07.03?相等性 == 比較(C# 編程指南)
有時需要比較兩個值是否相等。 在某些情況下,測試的是“值相等性”,也稱為“等效性”,這意味著兩個變量包含的值相等。 在其他情況下,必須確定兩個變量是否引用內存中的同一基礎對象。 此類型的相等性稱為“引用相等性”或“標識”。 本主題介紹這兩種相等性,并提供指向其他主題的鏈接,供用戶了解詳細信息。
07.03.01 引用相等性
引用相等性指兩個對象引用均引用同一基礎對象。 這可以通過簡單的賦值來實現(xiàn),如下面的示例所示。
C#
using System;
class Test
{public int Num { get; set; }public string Str { get; set; }public static void Main(){Test a = new Test() { Num = 1, Str = "Hi" };Test b = new Test() { Num = 1, Str = "Hi" };bool areEqual = System.Object.ReferenceEquals(a, b);// False:System.Console.WriteLine("ReferenceEquals(a, b) = {0}", areEqual);// Assign b to a.b = a;// Repeat calls with different results.areEqual = System.Object.ReferenceEquals(a, b);// True:System.Console.WriteLine("ReferenceEquals(a, b) = {0}", areEqual);}
}
在此代碼中,創(chuàng)建了兩個對象,但在賦值語句后,這兩個引用所引用的是同一對象。 因此,它們具有引用相等性。 使用?ReferenceEquals?方法確定兩個引用是否引用同一對象。
引用相等性的概念僅適用于引用類型。 由于在將值類型的實例賦給變量時將產(chǎn)生值的副本,因此值類型對象無法具有引用相等性。 因此,永遠不會有兩個未裝箱結構引用內存中的同一位置。 此外,如果使用?ReferenceEquals?比較兩個值類型,結果將始終為?false
,即使對象中包含的值都相同也是如此。 這是因為會將每個變量裝箱到單獨的對象實例中。 有關詳細信息,請參閱如何測試引用相等性(標識)。
07.03.02 值相等性
值相等性指兩個對象包含相同的一個或多個值。 對于基元值類型(例如?int?或?bool),針對值相等性的測試簡單明了。 可以使用?==?運算符,如下面的示例所示。
C#
int a = GetOriginalValue();
int b = GetCurrentValue(); // Test for value equality.
if (b == a)
{ // The two integers are equal.
}
對于大多數(shù)其他類型,針對值相等性的測試較為復雜,因為它需要用戶了解類型對值相等性的定義方式。 對于具有多個字段或屬性的類和結構,值相等性的定義通常指所有字段或屬性都具有相同的值。 例如,如果 pointA.X 等于 pointB.X,并且 pointA.Y 等于 pointB.Y,則可以將兩個?Point
?對象定義為相等。 對記錄來說,值相等性是指如果記錄類型的兩個變量類型相匹配,且所有屬性和字段值都一致,那么記錄類型的兩個變量是相等的。
但是,并不要求類型中的所有字段均相等。 只需子集相等即可。 比較不具所有權的類型時,應確保明確了解相等性對于該類型是如何定義的。 若要詳細了解如何在自己的類和結構中定義值相等性,請參閱如何為類型定義值相等性。
07.03.03 浮點值的值相等性
由于二進制計算機上的浮點算法不精確,因此浮點值(double?和?float)的相等比較會出現(xiàn)問題。 有關更多信息,請參閱?System.Double?主題中的備注部分。
07.03.04 如何為類或結構定義值相等性(C# 編程指南)
記錄自動實現(xiàn)值相等性。 當你的類型為數(shù)據(jù)建模并應實現(xiàn)值相等性時,請考慮定義?record
?而不是?class
。
定義類或結構時,需確定為類型創(chuàng)建值相等性(或等效性)的自定義定義是否有意義。 通常,預期將類型的對象添加到集合時,或者這些對象主要用于存儲一組字段或屬性時,需實現(xiàn)值相等性。 可以基于類型中所有字段和屬性的比較結果來定義值相等性,也可以基于子集進行定義。
在任何一種情況下,類和結構中的實現(xiàn)均應遵循 5 個等效性保證條件(對于以下規(guī)則,假設?x
、y
?和?z
?都不為 null):
-
自反屬性:
x.Equals(x)
?將返回?true
。 -
對稱屬性:
x.Equals(y)
?返回與?y.Equals(x)
?相同的值。 -
可傳遞屬性:如果?
(x.Equals(y) && y.Equals(z))
?返回?true
,則?x.Equals(z)
?將返回?true
。 -
只要未修改 x 和 y 引用的對象,
x.Equals(y)
?的連續(xù)調用就將返回相同的值。 -
任何非 null 值均不等于 null。 然而,當?
x
?為 null 時,x.Equals(y)
?將引發(fā)異常。 這會違反規(guī)則 1 或 2,具體取決于?Equals
?的參數(shù)。
定義的任何結構都已具有其從?Object.Equals(Object)?方法的?System.ValueType?替代中繼承的值相等性的默認實現(xiàn)。 此實現(xiàn)使用反射來檢查類型中的所有字段和屬性。 盡管此實現(xiàn)可生成正確的結果,但與專門為類型編寫的自定義實現(xiàn)相比,它的速度相對較慢。
類和結構的值相等性的實現(xiàn)詳細信息有所不同。 但是,類和結構都需要相同的基礎步驟來實現(xiàn)相等性:
-
替代虛擬?Object.Equals(Object)?方法。 大多數(shù)情況下,
bool Equals( object obj )
?實現(xiàn)應只調入作為?System.IEquatable<T>?接口的實現(xiàn)的類型特定?Equals
?方法。 (請參閱步驟 2。) -
通過提供類型特定的?
Equals
?方法實現(xiàn)?System.IEquatable<T>?接口。 實際的等效性比較將在此接口中執(zhí)行。 例如,可能決定通過僅比較類型中的一兩個字段來定義相等性。 不會從?Equals
?引發(fā)異常。 對于與繼承相關的類:-
此方法應僅檢查類中聲明的字段。 它應調用?
base.Equals
?來檢查基類中的字段。 (如果類型直接從?Object?中繼承,則不會調用?base.Equals
,因為?Object.Equals(Object)?的?Object?實現(xiàn)會執(zhí)行引用相等性檢查。) -
僅當要比較的變量的運行時類型相同時,才應將兩個變量視為相等。 此外,如果變量的運行時和編譯時類型不同,請確保使用運行時類型的?
Equals
?方法的?IEquatable
?實現(xiàn)。 確保始終正確比較運行時類型的一種策略是僅在?sealed
?類中實現(xiàn)?IEquatable
。 有關詳細信息,請參閱本文后續(xù)部分的類示例。
-
-
可選,但建議這樣做:重載?==?和?!=?運算符。
-
替代?Object.GetHashCode,以便具有值相等性的兩個對象生成相同的哈希代碼。
-
可選:若要支持“大于”或“小于”定義,請為類型實現(xiàn)?IComparable<T>?接口,并同時重載?<?和?>?運算符。
?備注
可以使用記錄來獲取值相等性語義,而不需要任何不必要的樣板代碼。
07.03.05 類class == 示例
下面的示例演示如何在類(引用類型)中實現(xiàn)值相等性。
C#
namespace ValueEqualityClass;class TwoDPoint : IEquatable<TwoDPoint>
{public int X { get; private set; }public int Y { get; private set; }public TwoDPoint(int x, int y){if (x is (< 1 or > 2000) || y is (< 1 or > 2000)){throw new ArgumentException("Point must be in range 1 - 2000");}this.X = x;this.Y = y;}public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);public bool Equals(TwoDPoint p){if (p is null){return false;}// Optimization for a common success case.if (Object.ReferenceEquals(this, p)){return true;}// If run-time types are not exactly the same, return false.if (this.GetType() != p.GetType()){return false;}// Return true if the fields match.// Note that the base class is not invoked because it is// System.Object, which defines Equals as reference equality.return (X == p.X) && (Y == p.Y);}public override int GetHashCode() => (X, Y).GetHashCode();public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs){if (lhs is null){if (rhs is null){return true;}// Only the left side is null.return false;}// Equals handles case of null on right side.return lhs.Equals(rhs);}public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{public int Z { get; private set; }public ThreeDPoint(int x, int y, int z): base(x, y){if ((z < 1) || (z > 2000)){throw new ArgumentException("Point must be in range 1 - 2000");}this.Z = z;}public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);public bool Equals(ThreeDPoint p){if (p is null){return false;}// Optimization for a common success case.if (Object.ReferenceEquals(this, p)){return true;}// Check properties that this class declares.if (Z == p.Z){// Let base class check its own fields// and do the run-time type comparison.return base.Equals((TwoDPoint)p);}else{return false;}}public override int GetHashCode() => (X, Y, Z).GetHashCode();public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs){if (lhs is null){if (rhs is null){// null == null = true.return true;}// Only the left side is null.return false;}// Equals handles the case of null on right side.return lhs.Equals(rhs);}public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}class Program
{static void Main(string[] args){ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);ThreeDPoint pointC = null;int i = 5;Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));Console.WriteLine("pointA == pointB = {0}", pointA == pointB);Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));TwoDPoint pointD = null;TwoDPoint pointE = null;Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);pointE = new TwoDPoint(3, 4);Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);System.Collections.ArrayList list = new System.Collections.ArrayList();list.Add(new ThreeDPoint(3, 4, 5));Console.WriteLine("pointE.Equals(list[0]): {0}", pointE.Equals(list[0]));// Keep the console window open in debug mode.Console.WriteLine("Press any key to exit.");Console.ReadKey();}
}/* Output:pointA.Equals(pointB) = TruepointA == pointB = Truenull comparison = FalseCompare to some other type = FalseTwo null TwoDPoints are equal: True(pointE == pointA) = False(pointA == pointE) = False(pointA != pointE) = TruepointE.Equals(list[0]): False
*/
在類(引用類型)上,兩種?Object.Equals(Object)?方法的默認實現(xiàn)均執(zhí)行引用相等性比較,而不是值相等性檢查。 實施者替代虛方法時,目的是為其指定值相等性語義。
即使類不重載?==
?和?!=
?運算符,也可將這些運算符與類一起使用。 但是,默認行為是執(zhí)行引用相等性檢查。 在類中,如果重載?Equals
?方法,則應重載?==
?和?!=
?運算符,但這并不是必需的。
?重要
前面的示例代碼可能無法按照預期的方式處理每個繼承方案。 考慮下列代碼:
C#
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
根據(jù)此代碼報告,盡管?z
?值有所不同,但?p1
?等于?p2
。 由于編譯器會根據(jù)編譯時類型選取?IEquatable
?的?TwoDPoint
?實現(xiàn),因而會忽略該差異。
record
?類型的內置值相等性可以正確處理這類場景。 如果?TwoDPoint
?和?ThreeDPoint
?是?record
?類型,則?p1.Equals(p2)
?的結果會是?False
。 有關詳細信息,請參閱?record?類型繼承層次結果中的相等性。
07.03.06 結構record == 示例
下面的示例演示如何在結構(值類型)中實現(xiàn)值相等性:
C#
namespace ValueEqualityStruct
{struct TwoDPoint : IEquatable<TwoDPoint>{public int X { get; private set; }public int Y { get; private set; }public TwoDPoint(int x, int y): this(){if (x is (< 1 or > 2000) || y is (< 1 or > 2000)){throw new ArgumentException("Point must be in range 1 - 2000");}X = x;Y = y;}public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;public override int GetHashCode() => (X, Y).GetHashCode();public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);}class Program{static void Main(string[] args){TwoDPoint pointA = new TwoDPoint(3, 4);TwoDPoint pointB = new TwoDPoint(3, 4);int i = 5;// True:Console.WriteLine("pointA.Equals(pointB) = {0}", pointA.Equals(pointB));// True:Console.WriteLine("pointA == pointB = {0}", pointA == pointB);// True:Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));// False:Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));// False:Console.WriteLine("(pointA == null) = {0}", pointA == null);// True:Console.WriteLine("(pointA != null) = {0}", pointA != null);// False:Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));// CS0019:// Console.WriteLine("pointA == i = {0}", pointA == i);// Compare unboxed to boxed.System.Collections.ArrayList list = new System.Collections.ArrayList();list.Add(new TwoDPoint(3, 4));// True:Console.WriteLine("pointA.Equals(list[0]): {0}", pointA.Equals(list[0]));// Compare nullable to nullable and to non-nullable.TwoDPoint? pointC = null;TwoDPoint? pointD = null;// False:Console.WriteLine("pointA == (pointC = null) = {0}", pointA == pointC);// True:Console.WriteLine("pointC == pointD = {0}", pointC == pointD);TwoDPoint temp = new TwoDPoint(3, 4);pointC = temp;// True:Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);pointD = temp;// True:Console.WriteLine("pointD == (pointC = 3,4) = {0}", pointD == pointC);Console.WriteLine("Press any key to exit.");Console.ReadKey();}}/* Output:pointA.Equals(pointB) = TruepointA == pointB = TrueObject.Equals(pointA, pointB) = TruepointA.Equals(null) = False(pointA == null) = False(pointA != null) = TruepointA.Equals(i) = FalsepointE.Equals(list[0]): TruepointA == (pointC = null) = FalsepointC == pointD = TruepointA == (pointC = 3,4) = TruepointD == (pointC = 3,4) = True*/
}
對于結構,Object.Equals(Object)(System.ValueType?中的替代版本)的默認實現(xiàn)通過使用反射來比較類型中每個字段的值,從而執(zhí)行值相等性檢查。 實施者替代結構中的?Equals
?虛方法時,目的是提供更高效的方法來執(zhí)行值相等性檢查,并選擇根據(jù)結構字段或屬性的某個子集來進行比較。
除非結構顯式重載了?==?和?!=?運算符,否則這些運算符無法對結構進行運算。
07.03.07 如何測試引用相等性(標識)(C# 編程指南)
無需實現(xiàn)任何自定義邏輯,即可支持類型中的引用相等性比較。 此功能由靜態(tài)?Object.ReferenceEquals?方法向所有類型提供。
以下示例演示如何確定兩個變量是否具有引用相等性,即它們引用內存中的同一對象。
該示例還演示?Object.ReferenceEquals?為何始終為值類型返回?false
,以及您為何不應使用?ReferenceEquals?來確定字符串相等性。
07.03.08 示例
C#
using System.Text;namespace TestReferenceEquality
{struct TestStruct{public int Num { get; private set; }public string Name { get; private set; }public TestStruct(int i, string s) : this(){Num = i;Name = s;}}class TestClass{public int Num { get; set; }public string? Name { get; set; }}class Program{static void Main(){// Demonstrate reference equality with reference types.#region ReferenceTypes// Create two reference type instances that have identical values.TestClass tcA = new TestClass() { Num = 1, Name = "New TestClass" };TestClass tcB = new TestClass() { Num = 1, Name = "New TestClass" };Console.WriteLine("ReferenceEquals(tcA, tcB) = {0}",Object.ReferenceEquals(tcA, tcB)); // false// After assignment, tcB and tcA refer to the same object.// They now have reference equality.tcB = tcA;Console.WriteLine("After assignment: ReferenceEquals(tcA, tcB) = {0}",Object.ReferenceEquals(tcA, tcB)); // true// Changes made to tcA are reflected in tcB. Therefore, objects// that have reference equality also have value equality.tcA.Num = 42;tcA.Name = "TestClass 42";Console.WriteLine("tcB.Name = {0} tcB.Num: {1}", tcB.Name, tcB.Num);#endregion// Demonstrate that two value type instances never have reference equality.#region ValueTypesTestStruct tsC = new TestStruct( 1, "TestStruct 1");// Value types are copied on assignment. tsD and tsC have// the same values but are not the same object.TestStruct tsD = tsC;Console.WriteLine("After assignment: ReferenceEquals(tsC, tsD) = {0}",Object.ReferenceEquals(tsC, tsD)); // false#endregion#region stringRefEquality// Constant strings within the same assembly are always interned by the runtime.// This means they are stored in the same location in memory. Therefore,// the two strings have reference equality although no assignment takes place.string strA = "Hello world!";string strB = "Hello world!";Console.WriteLine("ReferenceEquals(strA, strB) = {0}",Object.ReferenceEquals(strA, strB)); // true// After a new string is assigned to strA, strA and strB// are no longer interned and no longer have reference equality.strA = "Goodbye world!";Console.WriteLine("strA = \"{0}\" strB = \"{1}\"", strA, strB);Console.WriteLine("After strA changes, ReferenceEquals(strA, strB) = {0}",Object.ReferenceEquals(strA, strB)); // false// A string that is created at runtime cannot be interned.StringBuilder sb = new StringBuilder("Hello world!");string stringC = sb.ToString();// False:Console.WriteLine("ReferenceEquals(stringC, strB) = {0}",Object.ReferenceEquals(stringC, strB));// The string class overloads the == operator to perform an equality comparison.Console.WriteLine("stringC == strB = {0}", stringC == strB); // true#endregion// Keep the console open in debug mode.Console.WriteLine("Press any key to exit.");Console.ReadKey();}}
}/* Output:ReferenceEquals(tcA, tcB) = FalseAfter assignment: ReferenceEquals(tcA, tcB) = TruetcB.Name = TestClass 42 tcB.Num: 42After assignment: ReferenceEquals(tsC, tsD) = FalseReferenceEquals(strA, strB) = TruestrA = "Goodbye world!" strB = "Hello world!"After strA changes, ReferenceEquals(strA, strB) = FalseReferenceEquals(stringC, strB) = FalsestringC == strB = True
*/
在?System.Object?通用基類中實現(xiàn)?Equals
?也會執(zhí)行引用相等性檢查,但最好不要使用這種檢查,因為如果恰好某個類替代了此方法,結果可能會出乎意料。 以上情況同樣適用于?==
?和?!=
?運算符。 當它們作用于引用類型時,==
?和?!=
?的默認行為是執(zhí)行引用相等性檢查。 但是,派生類可重載運算符,執(zhí)行值相等性檢查。 為了盡量降低錯誤的可能性,當需要確定兩個對象是否具有引用相等性時,最好始終使用?ReferenceEquals。
運行時始終暫存同一程序集內的常量字符串。 也就是說,僅維護每個唯一文本字符串的一個實例。 但是,運行時不能保證會暫存在運行時創(chuàng)建的字符串,也不保證會暫存不同程序集中兩個相等的常量字符串。
07.04?C# 運算符和表達式
C# 提供了許多運算符。 其中許多都受到內置類型的支持,可用于對這些類型的值執(zhí)行基本操作。 這些運算符包括以下組:
- 算術運算符,將對數(shù)值操作數(shù)執(zhí)行算術運算
- 比較運算符,將比較數(shù)值操作數(shù)
- 布爾邏輯運算符,將對?bool?操作數(shù)執(zhí)行邏輯運算
- 位運算符和移位運算符,將對整數(shù)類型的操作數(shù)執(zhí)行位運算或移位運算
- 相等運算符,將檢查其操作數(shù)是否相等
通??梢灾剌d這些運算符,也就是說,可以為用戶定義類型的操作數(shù)指定運算符行為。
最簡單的 C# 表達式是文本(例如整數(shù)和實數(shù))和變量名稱。 可以使用運算符將它們組合成復雜的表達式。 運算符優(yōu)先級和結合性決定了表達式中操作的執(zhí)行順序。 可以使用括號更改由運算符優(yōu)先級和結合性決定的計算順序。
在下面的代碼中,表達式的示例位于賦值的右側:
int a, b, c;
a = 7;
b = a;
c = b++;
b = a + b * c;
c = a >= 100 ? b : c / 10;
a = (int)Math.Sqrt(b * b + c * c);string s = "String literal";
char l = s[s.Length - 1];var numbers = new List<int>(new[] { 1, 2, 3 });
b = numbers.FindLast(n => n > 1);
通常情況下,表達式會生成結果,并可包含在其他表達式中。?void?方法調用是不生成結果的表達式的示例。 它只能用作語句,如下面的示例所示:
Console.WriteLine("Hello, world!");
下面是 C# 提供的一些其他類型的表達式:
-
內插字符串表達式,提供創(chuàng)建格式化字符串的便利語法:
C#復制運行
var r = 2.3; var message = $"The area of a circle with radius {r} is {Math.PI * r * r:F3}."; Console.WriteLine(message); // Output: // The area of a circle with radius 2.3 is 16.619.
-
Lambda 表達式,可用于創(chuàng)建匿名函數(shù):
C#復制運行
int[] numbers = { 2, 3, 4, 5 }; var maximumSquare = numbers.Max(x => x * x); Console.WriteLine(maximumSquare); // Output: // 25
-
查詢表達式,可用于直接以 C# 使用查詢功能:
C#復制運行
var scores = new[] { 90, 97, 78, 68, 85 }; IEnumerable<int> highScoresQuery =from score in scoreswhere score > 80orderby score descendingselect score; Console.WriteLine(string.Join(" ", highScoresQuery)); // Output: // 97 90 85
可使用表達式主體定義為方法、構造函數(shù)、屬性、索引器或終結器提供簡潔的定義。
07.04.01 運算符優(yōu)先級
在包含多個運算符的表達式中,先按優(yōu)先級較高的運算符計算,再按優(yōu)先級較低的運算符計算。 在下面的示例中,首先執(zhí)行乘法,因為其優(yōu)先級高于加法:
C#復制運行
var a = 2 + 2 * 2;
Console.WriteLine(a); // output: 6
使用括號更改運算符優(yōu)先級所施加的計算順序:
C#復制運行
var a = (2 + 2) * 2;
Console.WriteLine(a); // output: 8
下表按最高優(yōu)先級到最低優(yōu)先級的順序列出 C# 運算符。 每行中運算符的優(yōu)先級相同。
運算符 | 類別或名稱 |
---|---|
x.y、f(x)、a[i]、x?.y、x?[y]、x++、x--、x!、new、typeof、checked、unchecked、default、nameof、delegate、sizeof、stackalloc、x->y | 主要 |
+x、-x、x、~x、++x、--x、^x、(T)x、await、&&x、*x、true 和 false | 一元 |
x..y | 范圍 |
switch、with | switch ?和?with ?表達式 |
x * y、x / y、x % y | 乘法 |
x + y、x – y | 加法 |
x << y、x >> y | Shift |
x < y、x > y、x <= y、x >= y、is、as | 關系和類型測試 |
x == y、x != y | 相等 |
x & y | 布爾邏輯 AND?或按位邏輯 AND |
x ^ y | 布爾邏輯 XOR?或按位邏輯 XOR |
x | y | 布爾邏輯 OR?或按位邏輯 OR |
x && y | 條件“與” |
x || y | 條件“或” |
x ?? y | Null 合并運算符 |
c ? t : f | 條件運算符 |
x = y、x += y、x -= y、x *= y、x /= y、x %= y、x &= y、x |= y、x ^= y、x <<= y、x >>= y、x ??= y、=> | 賦值和 lambda 聲明 |
07.04.02 運算符結合性
當運算符的優(yōu)先級相同,運算符的結合性決定了運算的執(zhí)行順序:
- 左結合運算符按從左到右的順序計算。 除賦值運算符和?null 合并運算符外,所有二元運算符都是左結合運算符。 例如,
a + b - c
?將計算為?(a + b) - c
。 - 右結合運算符按從右到左的順序計算。 賦值運算符、null 合并運算符、lambda 和條件運算符?:是右結合運算符。 例如,
x = y = z
?將計算為?x = (y = z)
。
使用括號更改運算符結合性所施加的計算順序:
C#復制運行
int a = 13 / 5 / 2;
int b = 13 / (5 / 2);
Console.WriteLine($"a = {a}, b = "); // output: a = 1, b = 6
07.04.03 操作數(shù)計算
與運算符的優(yōu)先級和結合性無關,從左到右計算表達式中的操作數(shù)。 以下示例展示了運算符和操作數(shù)的計算順序:
展開表
表達式 | 計算順序 |
---|---|
a + b | a, b, + |
a + b * c | a, b, c, *, + |
a / b + c * d | a, b, /, c, d, *, + |
a / (b + c) * d | a, b, c, +, /, d, * |
通常,會計算所有運算符操作數(shù)。 但是,某些運算符有條件地計算操作數(shù)。 也就是說,此類運算符的最左側操作數(shù)的值定義了是否應計算其他操作數(shù),或計算其他哪些操作數(shù)。 這些運算符有條件邏輯?AND (&&)?和?OR (||)?運算符、null 合并運算符????和???=、null 條件運算符??.?和??[]?以及條件運算符?:。 有關詳細信息,請參閱每個運算符的說明。
07.05 表達式
07.05.01?選擇語句 -?if
、if-else
?和?switch
if
、if-else
?和?switch
?語句根據(jù)表達式的值從多個可能的語句選擇要執(zhí)行的路徑。 僅當提供的布爾表達式的計算結果為?true
?時,if
,if?語句才執(zhí)行語句。?語句if-else允許你根據(jù)布爾表達式選擇要遵循的兩個代碼路徑中的哪一個。?switch?語句根據(jù)與表達式匹配的模式來選擇要執(zhí)行的語句列表。
07.05.01.01?if
?語句
if
?語句可采用以下兩種形式中的任一種:
-
包含?
else
?部分的?if
?語句根據(jù)布爾表達式的值選擇兩個語句中的一個來執(zhí)行,如以下示例所示:C#
DisplayWeatherReport(15.0); // Output: Cold. DisplayWeatherReport(24.0); // Output: Perfect!void DisplayWeatherReport(double tempInCelsius) {if (tempInCelsius < 20.0){Console.WriteLine("Cold.");}else{Console.WriteLine("Perfect!");} }
-
不包含?
else
?部分的?if
?語句僅在布爾表達式計算結果為?true
?時執(zhí)行其主體,如以下示例所示:C#
DisplayMeasurement(45); // Output: The measurement value is 45 DisplayMeasurement(-3); // Output: Warning: not acceptable value! The measurement value is -3void DisplayMeasurement(double value) {if (value < 0 || value > 100){Console.Write("Warning: not acceptable value! ");}Console.WriteLine($"The measurement value is {value}"); }
可嵌套?if
?語句來檢查多個條件,如以下示例所示:
C#
DisplayCharacter('f'); // Output: A lowercase letter: f
DisplayCharacter('R'); // Output: An uppercase letter: R
DisplayCharacter('8'); // Output: A digit: 8
DisplayCharacter(','); // Output: Not alphanumeric character: ,void DisplayCharacter(char ch)
{if (char.IsUpper(ch)){Console.WriteLine($"An uppercase letter: {ch}");}else if (char.IsLower(ch)){Console.WriteLine($"A lowercase letter: {ch}");}else if (char.IsDigit(ch)){Console.WriteLine($"A digit: {ch}");}else{Console.WriteLine($"Not alphanumeric character: {ch}");}
}
在表達式上下文中,可使用條件運算符??:?根據(jù)布爾表達式的值計算兩個表達式中的一個。
07.05.01.02?switch
?語句
switch
?語句根據(jù)與匹配表達式匹配的模式來選擇要執(zhí)行的語句列表,如以下示例所示:
C#復制
DisplayMeasurement(-4); // Output: Measured value is -4; too low.
DisplayMeasurement(5); // Output: Measured value is 5.
DisplayMeasurement(30); // Output: Measured value is 30; too high.
DisplayMeasurement(double.NaN); // Output: Failed measurement.void DisplayMeasurement(double measurement)
{switch (measurement){case < 0.0:Console.WriteLine($"Measured value is {measurement}; too low.");break;case > 15.0:Console.WriteLine($"Measured value is {measurement}; too high.");break;case double.NaN:Console.WriteLine("Failed measurement.");break;default:Console.WriteLine($"Measured value is {measurement}.");break;}
}
在上述示例中,switch
?語句使用以下模式:
- 關系模式:用于將表達式結果與常量進行比較。
- 常量模式:測試表達式結果是否等于常量。
?重要
有關?switch
?語句支持的模式的信息,請參閱模式。
上述示例還展示了?default
?case。?default
?case 指定匹配表達式與其他任何 case 模式都不匹配時要執(zhí)行的語句。 如果匹配表達式與任何 case 模式都不匹配,且沒有?default
?case,控制就會貫穿?switch
?語句。
switch
?語句執(zhí)行第一個 switch 部分中的語句列表,其 case 模式與匹配表達式匹配,并且它的?case guard(如果存在)求值為?true
?。?switch
?語句按文本順序從上到下對 case 模式求值。 編譯器在?switch
?語句包含無法訪問的 case 時會生成錯誤。 這種 case 已由大寫字母處理或其模式無法匹配。
?備注
default
?case 可以在?switch
?語句的任何位置出現(xiàn)。 無論其位置如何,僅當所有其他事例模式都不匹配或?goto default;
?語句在其中一個 switch 節(jié)中執(zhí)行時,default
?才會計算事例。
可以為?switch
?語句的一部分指定多個 case 模式,如以下示例所示:
C#復制
DisplayMeasurement(-4); // Output: Measured value is -4; out of an acceptable range.
DisplayMeasurement(50); // Output: Measured value is 50.
DisplayMeasurement(132); // Output: Measured value is 132; out of an acceptable range.void DisplayMeasurement(int measurement)
{switch (measurement){case < 0:case > 100:Console.WriteLine($"Measured value is {measurement}; out of an acceptable range.");break;default:Console.WriteLine($"Measured value is {measurement}.");break;}
}
在?switch
?語句中,控制不能從一個 switch 部分貫穿到下一個 switch 部分。 如本部分中的示例所示,通常使用每個 switch 部分末尾的?break
?語句將控制從?switch
?語句傳遞出去。 還可使用?return?和?throw?語句將控制從?switch
?語句傳遞出去。 若要模擬貫穿行為,將控制傳遞給其他 switch 部分,可使用?goto?語句。
在表達式上下文中,可使用?switch?表達式,根據(jù)與表達式匹配的模式,對候選表達式列表中的單個表達式進行求值。
07.05.01.03 Case guard
case 模式可能表達功能不夠,無法指定用于執(zhí)行 switch 部分的條件。 在這種情況下,可以使用 case guard。 這是一個附加條件,必須與匹配模式同時滿足。 case guard 必須是布爾表達式。 可以在模式后面的?when
?關鍵字之后指定一個 case guard,如以下示例所示:
C#復制
DisplayMeasurements(3, 4); // Output: First measurement is 3, second measurement is 4.
DisplayMeasurements(5, 5); // Output: Both measurements are valid and equal to 5.void DisplayMeasurements(int a, int b)
{switch ((a, b)){case (> 0, > 0) when a == b:Console.WriteLine($"Both measurements are valid and equal to {a}.");break;case (> 0, > 0):Console.WriteLine($"First measurement is {a}, second measurement is .");break;default:Console.WriteLine("One or both measurements are not valid.");break;}
}
上述示例使用帶有嵌套關系模式的位置模式。
07.05.02?迭代語句 -?for
、foreach
、do
?和?while
此迭代語句重復執(zhí)行語句或語句塊。?for?語句:在指定的布爾表達式的計算結果為?true
?時會執(zhí)行其主體。?foreach?語句:枚舉集合元素并對集合中的每個元素執(zhí)行其主體。?do?語句:有條件地執(zhí)行其主體一次或多次。?while?語句:有條件地執(zhí)行其主體零次或多次。
在迭代語句體中的任何點,都可以使用?break?語句跳出循環(huán)。 可以使用?continue?語句進入循環(huán)中的下一個迭代。
07.05.02.01?for
?語句
在指定的布爾表達式的計算結果為?true
?時,for
?語句會執(zhí)行一條語句或一個語句塊。 以下示例顯示了?for
?語句,該語句在整數(shù)計數(shù)器小于 3 時執(zhí)行其主體:
C#復制運行
for (int i = 0; i < 3; i++)
{Console.Write(i);
}
// Output:
// 012
上述示例展示了?for
?語句的元素:
-
“初始化表達式”部分僅在進入循環(huán)前執(zhí)行一次。 通常,在該部分中聲明并初始化局部循環(huán)變量。 不能從?
for
?語句外部訪問聲明的變量。上例中的“初始化表達式”部分聲明并初始化整數(shù)計數(shù)器變量:
C#復制
int i = 0
-
“條件”部分確定是否應執(zhí)行循環(huán)中的下一個迭代。 如果計算結果為?
true
?或不存在,則執(zhí)行下一個迭代;否則退出循環(huán)。 “條件”部分必須為布爾表達式。上例中的“條件”條件部分檢查計數(shù)器值是否小于 3:
C#復制
i < 3
-
“迭代器”部分定義循環(huán)主體的每次執(zhí)行后將執(zhí)行的操作。
上例中的“迭代器”部分增加計數(shù)器:
C#復制
i++
-
循環(huán)體,必須是一個語句或一個語句塊。
“迭代器”部分可包含用逗號分隔的零個或多個以下語句表達式:
- 為?increment?表達式添加前綴或后綴,如?
++i
?或?i++
- 為?decrement?表達式添加前綴或后綴,如?
--i
?或?i--
- assignment
- 方法的調用
- await表達式
- 通過使用?new?運算符來創(chuàng)建對象
如果未在“初始化表達式”部分中聲明循環(huán)變量,則還可以在“初始化表達式”部分中使用上述列表中的零個或多個表達式。 下面的示例顯示了幾種不太常見的“初始化表達式”和“迭代器”部分的使用情況:為“初始化表達式”部分中的外部變量賦值、同時在“初始化表達式”部分和“迭代器”部分中調用一種方法,以及更改“迭代器”部分中的兩個變量的值:
C#復制運行
int i;
int j = 3;
for (i = 0, Console.WriteLine($"Start: i={i}, j={j}"); i < j; i++, j--, Console.WriteLine($"Step: i={i}, j={j}"))
{//...
}
// Output:
// Start: i=0, j=3
// Step: i=1, j=2
// Step: i=2, j=1
for
?語句的所有部分都是可選的。 例如,以下代碼定義無限?for
?循環(huán):
C#復制
for ( ; ; )
{//...
}
07.05.02???????.02?foreach
?語句
foreach
?語句為類型實例中實現(xiàn)?System.Collections.IEnumerable?或?System.Collections.Generic.IEnumerable<T>?接口的每個元素執(zhí)行語句或語句塊,如以下示例所示:
C#復制運行
List<int> fibNumbers = new() { 0, 1, 1, 2, 3, 5, 8, 13 };
foreach (int element in fibNumbers)
{Console.Write($"{element} ");
}
// Output:
// 0 1 1 2 3 5 8 13
foreach
?語句并不限于這些類型。 可以將其與滿足以下條件的任何類型的實例一起使用:
- 類型具有公共無參數(shù)?
GetEnumerator
?方法。?GetEnumerator
?方法可以是類型的擴展方法。 GetEnumerator
?方法的返回類型具有公共?Current
?屬性和公共無參數(shù)?MoveNext
?方法(其返回類型為?bool
)。
下面的示例使用?foreach
?語句,其中包含?System.Span<T>?類型的實例,該實例不實現(xiàn)任何接口:
C#復制
Span<int> numbers = [3, 14, 15, 92, 6];
foreach (int number in numbers)
{Console.Write($"{number} ");
}
// Output:
// 3 14 15 92 6
如果枚舉器的?Current
?屬性返回引用返回值(ref T
,其中?T
?為集合元素類型),就可以使用?ref
?或?ref readonly
?修飾符來聲明迭代變量,如下面的示例所示:
C#復制
Span<int> storage = stackalloc int[10];
int num = 0;
foreach (ref int item in storage)
{item = num++;
}
foreach (ref readonly var item in storage)
{Console.Write($"{item} ");
}
// Output:
// 0 1 2 3 4 5 6 7 8 9
如果?foreach
?語句的源集合為空,則?foreach
?語句的正文不會被執(zhí)行,而是被跳過。 如果?foreach
?語句應用為?null
,則會引發(fā)?NullReferenceException。
07.05.02???????.03 await foreach
可以使用?await foreach
?語句來使用異步數(shù)據(jù)流,即實現(xiàn)?IAsyncEnumerable<T>?接口的集合類型。 異步檢索下一個元素時,可能會掛起循環(huán)的每次迭代。 下面的示例演示如何使用?await foreach
?語句:
C#復制
await foreach (var item in GenerateSequenceAsync())
{Console.WriteLine(item);
}
還可以將?await foreach
?語句與滿足以下條件的任何類型的實例一起使用:
- 類型具有公共無參數(shù)?
GetAsyncEnumerator
?方法。 該方法可以是類型的擴展方法。 GetAsyncEnumerator
?方法的返回類型具有公共?Current
?屬性和公共無參數(shù)?MoveNextAsync
?方法(其返回類型為?Task<bool>、ValueTask<bool>?或任何其他可等待類型,其 awaiter 的?GetResult
?方法返回?bool
?值)。
默認情況下,在捕獲的上下文中處理流元素。 如果要禁用上下文捕獲,請使用?TaskAsyncEnumerableExtensions.ConfigureAwait?擴展方法。 有關同步上下文并捕獲當前上下文的詳細信息,請參閱使用基于任務的異步模式。 有關異步流的詳細信息,請參閱異步流教程。
07.05.02???????.04 迭代變量的類型
可以使用?var?關鍵字讓編譯器推斷?foreach
?語句中迭代變量的類型,如以下代碼所示:
C#復制
foreach (var item in collection) { }
?備注
譯器可以將?var
?的類型推斷為可為空的引用類型,具體取決于是否啟用可為空的感知上下文以及初始化表達式的類型是否為引用類型。 有關詳細信息,請參閱隱式類型本地變量。
還可以顯式指定迭代變量的類型,如以下代碼所示:
C#復制
IEnumerable<T> collection = new T[5];
foreach (V item in collection) { }
在上述窗體中,集合元素的類型?T
?必須可隱式或顯式地轉換為迭代變量的類型?V
。 如果從?T
?到?V
?的顯式轉換在運行時失敗,foreach
?語句將引發(fā)?InvalidCastException。 例如,如果?T
?是非密封類類型,則?V
?可以是任何接口類型,甚至可以是?T
?未實現(xiàn)的接口類型。 在運行時,集合元素的類型可以是從?T
?派生并且實際實現(xiàn)?V
?的類型。 如果不是這樣,則會引發(fā)?InvalidCastException。
07.05.02???????.05?do
?語句
在指定的布爾表達式的計算結果為?true
?時,do
?語句會執(zhí)行一條語句或一個語句塊。 由于在每次執(zhí)行循環(huán)之后都會計算此表達式,所以?do
?循環(huán)會執(zhí)行一次或多次。?do
?循環(huán)不同于?while?循環(huán)(該循環(huán)執(zhí)行零次或多次)。
下面的示例演示?do
?語句的用法:
C#復制運行
int n = 0;
do
{Console.Write(n);n++;
} while (n < 5);
// Output:
// 01234
07.05.02???????.06?while
?語句
在指定的布爾表達式的計算結果為?true
?時,while
?語句會執(zhí)行一條語句或一個語句塊。 由于在每次執(zhí)行循環(huán)之前都會計算此表達式,所以?while
?循環(huán)會執(zhí)行零次或多次。?while
?循環(huán)不同于?do?循環(huán)(該循環(huán)執(zhí)行 1 次或多次)。
下面的示例演示?while
?語句的用法:
C#復制運行
int n = 0;
while (n < 5)
{Console.Write(n);n++;
}
// Output:
// 01234
07.05.03?跳轉語句 -?break
、continue
、return
?和?goto
jump 語句無條件轉移控制。?break?語句將終止最接近的封閉迭代語句或?switch?語句。?continue?語句啟動最接近的封閉迭代語句的新迭代。?return?語句終止它所在的函數(shù)的執(zhí)行,并將控制權返回給調用方。?goto?語句將控制權轉交給帶有標簽的語句。
有關引發(fā)異常并無條件轉移控制權的?throw
?語句的信息,請參閱異常處理語句一文的throw?語句部分。
07.05.03.01?break
?語句
break
?語句:將終止最接近的封閉迭代語句(即?for
、foreach
、while
?或?do
?循環(huán))或?switch?語句。?break
?語句將控制權轉交給已終止語句后面的語句(若有)。
C#復制運行
int[] numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
foreach (int number in numbers)
{if (number == 3){break;}Console.Write($"{number} ");
}
Console.WriteLine();
Console.WriteLine("End of the example.");
// Output:
// 0 1 2
// End of the example.
在嵌套循環(huán)中,break
?語句僅終止包含它的最內部循環(huán),如以下示例所示:
C#復制運行
for (int outer = 0; outer < 5; outer++)
{for (int inner = 0; inner < 5; inner++){if (inner > outer){break;}Console.Write($"{inner} ");}Console.WriteLine();
}
// Output:
// 0
// 0 1
// 0 1 2
// 0 1 2 3
// 0 1 2 3 4
在循環(huán)內使用?switch
?語句時,switch 節(jié)末尾的?break
?語句僅從?switch
?語句中轉移控制權。 包含?switch
?語句的循環(huán)不受影響,如以下示例所示:
C#復制
double[] measurements = [-4, 5, 30, double.NaN];
foreach (double measurement in measurements)
{switch (measurement){case < 0.0:Console.WriteLine($"Measured value is {measurement}; too low.");break;case > 15.0:Console.WriteLine($"Measured value is {measurement}; too high.");break;case double.NaN:Console.WriteLine("Failed measurement.");break;default:Console.WriteLine($"Measured value is {measurement}.");break;}
}
// Output:
// Measured value is -4; too low.
// Measured value is 5.
// Measured value is 30; too high.
// Failed measurement.
07.05.03.02?continue
?語句
continue
?語句啟動最接近的封閉迭代語句(即?for
、foreach
、while
?或?do
?循環(huán))的新迭代,如以下示例所示:
C#復制運行
for (int i = 0; i < 5; i++)
{Console.Write($"Iteration {i}: ");if (i < 3){Console.WriteLine("skip");continue;}Console.WriteLine("done");
}
// Output:
// Iteration 0: skip
// Iteration 1: skip
// Iteration 2: skip
// Iteration 3: done
// Iteration 4: done
07.05.03.03?return
?語句
return
?語句終止它所在的函數(shù)的執(zhí)行,并將控制權和函數(shù)結果(若有)返回給調用方。
如果函數(shù)成員不計算值,則使用不帶表達式的?return
?語句,如以下示例所示:
C#復制運行
Console.WriteLine("First call:");
DisplayIfNecessary(6);Console.WriteLine("Second call:");
DisplayIfNecessary(5);void DisplayIfNecessary(int number)
{if (number % 2 == 0){return;}Console.WriteLine(number);
}
// Output:
// First call:
// Second call:
// 5
如前面的示例所示,通常使用不帶表達式的?return
?語句提前終止函數(shù)成員。 如果函數(shù)成員不包含?return
?語句,則在執(zhí)行其最后一個語句后終止。
如果函數(shù)成員不計算值,則使用帶表達式的?return
?語句,如以下示例所示:
C#復制運行
double surfaceArea = CalculateCylinderSurfaceArea(1, 1);
Console.WriteLine($"{surfaceArea:F2}"); // output: 12.57double CalculateCylinderSurfaceArea(double baseRadius, double height)
{double baseArea = Math.PI * baseRadius * baseRadius;double sideArea = 2 * Math.PI * baseRadius * height;return 2 * baseArea + sideArea;
}
如果?return
?語句具有表達式,該表達式必須可隱式轉換為函數(shù)成員的返回類型,除非它是異步的。 從?async
?函數(shù)返回的表達式必須隱式轉換為?Task<TResult>?或?ValueTask<TResult>?類型參數(shù),以函數(shù)的返回類型為準。 如果?async
?函數(shù)的返回類型為?Task?或?ValueTask,則使用不帶表達式的?return
?語句。
07.05.03.04 引用返回
默認情況下,return
?語句返回表達式的值。 可以返回對變量的引用。 引用返回值(或 ref 返回值)是由方法按引用向調用方返回的值。 即是說,調用方可以修改方法所返回的值,此更改反映在所調用方法中的對象的狀態(tài)中。 為此,請使用帶?ref
?關鍵字的?return
?語句,如以下示例所示:
C#復制運行
int[] xs = new int [] {10, 20, 30, 40 };
ref int found = ref FindFirst(xs, s => s == 30);
found = 0;
Console.WriteLine(string.Join(" ", xs)); // output: 10 20 0 40ref int FindFirst(int[] numbers, Func<int, bool> predicate)
{for (int i = 0; i < numbers.Length; i++){if (predicate(numbers[i])){return ref numbers[i];}}throw new InvalidOperationException("No element satisfies the given condition.");
}
借助引用返回值,方法可以將對變量的引用(而不是值)返回給調用方。 然后,調用方可以選擇將返回的變量視為按值返回或按引用返回。 調用方可以新建稱為引用本地的變量,其本身就是對返回值的引用。 引用返回值是指,方法返回對某變量的引用(或別名)。 相應變量的作用域必須包括方法。 相應變量的生存期必須超過方法的返回值。 調用方對方法的返回值進行的修改應用于方法返回的變量。
如果聲明方法返回引用返回值,表明方法返回變量別名。 設計意圖通常是讓調用代碼通過別名訪問此變量(包括修改它)。 方法的引用返回值不得包含返回類型?void
。
為方便調用方修改對象的狀態(tài),引用返回值必須存儲在被顯式定義為?reference 變量的變量中。
ref
?返回值是被調用方法范圍中另一個變量的別名。 可以將引用返回值的所有使用都解釋為,使用它取別名的變量:
- 分配值時,就是將值分配到它取別名的變量。
- 讀取值時,就是讀取它取別名的變量的值。
- 如果以引用方式返回它,就是返回對相同變量所取的別名。
- 如果以引用方式將它傳遞到另一個方法,就是傳遞對它取別名的變量的引用。
- 如果返回引用本地別名,就是返回相同變量的新別名。
引用返回必須是調用方法的?ref-safe-context。 也就是說:
- 返回值的生存期必須長于方法執(zhí)行時間。 換言之,它不能是返回自身的方法中的本地變量。 它可以是實例或類的靜態(tài)字段,也可是傳遞給方法的參數(shù)。 嘗試返回局部變量將生成編譯器錯誤 CS8168:“無法按引用返回局部 "obj",因為它不是 ref 局部變量”。
- 返回值不得為文本?
null
。 使用引用返回值的方法可以返回值當前為?null
(未實例化)或可為空的值類型的變量別名。 - 返回值不得為常量、枚舉成員、通過屬性的按值返回值或?
class
/struct
?方法。
此外,禁止對異步方法使用引用返回值。 異步方法可能會在執(zhí)行尚未完成時就返回值,盡管返回值仍未知。
返回引用返回值的方法必須:
- 在返回類型前面有?ref?關鍵字。
- 方法主體中的每個?return?語句都在返回實例的名稱前面有?ref?關鍵字。
下面的示例方法滿足這些條件,且返回對名為?p
?的?Person
?對象的引用:
C#復制
public ref Person GetContactInformation(string fname, string lname)
{// ...method implementation...return ref p;
}
下面是一個更完整的 ref 返回示例,同時顯示方法簽名和方法主體。
C#復制
public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{for (int i = 0; i < matrix.GetLength(0); i++)for (int j = 0; j < matrix.GetLength(1); j++)if (predicate(matrix[i, j]))return ref matrix[i, j];throw new InvalidOperationException("Not found");
}
所調用方法還可能會將返回值聲明為?ref readonly
?以按引用返回值,并堅持調用代碼無法修改返回的值。 調用方法可以通過將返回值存儲在局部?ref readonly
?reference 變量中來避免復制該值。
下列示例定義一個具有兩個?String?字段(Title
?和?Author
)的?Book
?類。 還定義包含?Book
?對象的專用數(shù)組的?BookCollection
?類。 通過調用?GetBookByTitle
?方法,可按引用返回個別 book 對象。
C#復制
public class Book
{public string Author;public string Title;
}public class BookCollection
{private Book[] books = { new Book { Title = "Call of the Wild, The", Author = "Jack London" },new Book { Title = "Tale of Two Cities, A", Author = "Charles Dickens" }};private Book nobook = null;public ref Book GetBookByTitle(string title){for (int ctr = 0; ctr < books.Length; ctr++){if (title == books[ctr].Title)return ref books[ctr];}return ref nobook;}public void ListBooks(){foreach (var book in books){Console.WriteLine($"{book.Title}, by {book.Author}");}Console.WriteLine();}
}
調用方將?GetBookByTitle
?方法所返回的值存儲為 ref 局部變量時,調用方對返回值所做的更改將反映在?BookCollection
?對象中,如下例所示。
C#復制
var bc = new BookCollection();
bc.ListBooks();ref var book = ref bc.GetBookByTitle("Call of the Wild, The");
if (book != null)book = new Book { Title = "Republic, The", Author = "Plato" };
bc.ListBooks();
// The example displays the following output:
// Call of the Wild, The, by Jack London
// Tale of Two Cities, A, by Charles Dickens
//
// Republic, The, by Plato
// Tale of Two Cities, A, by Charles Dickens
07.05.03.05?goto
?語句
goto
?語句將控制權轉交給帶有標簽的語句,如以下示例所示:
C#復制
var matrices = new Dictionary<string, int[][]>
{["A"] =[[1, 2, 3, 4],[4, 3, 2, 1]],["B"] =[[5, 6, 7, 8],[8, 7, 6, 5]],
};CheckMatrices(matrices, 4);void CheckMatrices(Dictionary<string, int[][]> matrixLookup, int target)
{foreach (var (key, matrix) in matrixLookup){for (int row = 0; row < matrix.Length; row++){for (int col = 0; col < matrix[row].Length; col++){if (matrix[row][col] == target){goto Found;}}}Console.WriteLine($"Not found {target} in matrix {key}.");continue;Found:Console.WriteLine($"Found {target} in matrix {key}.");}
}
// Output:
// Found 4 in matrix A.
// Not found 4 in matrix B.
如前面的示例所示,可以使用?goto
?語句退出嵌套循環(huán)。
?提示
使用嵌套循環(huán)時,請考慮將單獨的循環(huán)重構為單獨的方法。 這可能會導致沒有?goto
?語句的更簡單、更具可讀性的代碼。
還可使用?switch?語句中的?goto
?語句將控制權移交到具有常量大小寫標簽的 switch 節(jié),如以下示例所示:
C#復制運行
using System;public enum CoffeeChoice
{Plain,WithMilk,WithIceCream,
}public class GotoInSwitchExample
{public static void Main(){Console.WriteLine(CalculatePrice(CoffeeChoice.Plain)); // output: 10.0Console.WriteLine(CalculatePrice(CoffeeChoice.WithMilk)); // output: 15.0Console.WriteLine(CalculatePrice(CoffeeChoice.WithIceCream)); // output: 17.0}private static decimal CalculatePrice(CoffeeChoice choice){decimal price = 0;switch (choice){case CoffeeChoice.Plain:price += 10.0m;break;case CoffeeChoice.WithMilk:price += 5.0m;goto case CoffeeChoice.Plain;case CoffeeChoice.WithIceCream:price += 7.0m;goto case CoffeeChoice.Plain;}return price;}
}
在?switch
?語句中,還可使用語句?goto default;
?將控制權轉交給帶?default
?標簽的 switch 節(jié)。
如果當前函數(shù)成員中不存在具有給定名稱的標簽,或者?goto
?語句不在標簽范圍內,則會出現(xiàn)編譯時錯誤。 也就是說,你不能使用?goto
?語句將控制權從當前函數(shù)成員轉移到任何嵌套范圍。
07.05.04?異常處理語句 -?throw
、try-catch
、try-finally
?和?try-catch-finally
使用?throw
?和?try
?語句來處理異常。 使用?throw?語句引發(fā)異常。 使用?try?語句捕獲和處理在執(zhí)行代碼塊期間可能發(fā)生的異常。
07.05.04.01?throw
?語句
throw
?語句引發(fā)異常:
C#復制
if (shapeAmount <= 0)
{throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
}
在?throw e;
?語句中,表達式?e
?的結果必須隱式轉換為?System.Exception。
可以使用內置異常類,例如?ArgumentOutOfRangeException?或?InvalidOperationException。 .NET 還提供了以下在某些情況下引發(fā)異常的幫助程序方法:ArgumentNullException.ThrowIfNull?和?ArgumentException.ThrowIfNullOrEmpty。 還可以定義自己的派生自?System.Exception?的異常類。 有關詳細信息,請參閱創(chuàng)建和引發(fā)異常。
在?catch?塊內,可以使用?throw;
?語句重新引發(fā)由?catch
?塊處理的異常:
C#復制
try
{ProcessShapes(shapeAmount);
}
catch (Exception e)
{LogError(e, "Shape processing failed.");throw;
}
?備注
throw;
?保留異常的原始堆棧跟蹤,該跟蹤存儲在?Exception.StackTrace?屬性中。 與此相反,throw e;
?更新?e
?的?StackTrace?屬性。
引發(fā)異常時,公共語言運行時 (CLR) 將查找可以處理此異常的?catch?塊。 如果當前執(zhí)行的方法不包含此類?catch
?塊,則 CLR 查看調用了當前方法的方法,并以此類推遍歷調用堆棧。 如果未找到?catch
?塊,CLR 將終止正在執(zhí)行的線程。 有關詳細信息,請參閱?C# 語言規(guī)范的如何處理異常部分。
07.05.04.02?throw
?表達式
還可以將?throw
?用作表達式。 這在很多情況下可能很方便,包括:
-
條件運算符。 以下示例使用?
throw
?表達式在傳遞的數(shù)組?args
?為空時引發(fā)?ArgumentException:C#復制
string first = args.Length >= 1 ? args[0]: throw new ArgumentException("Please supply at least one argument.");
-
null 合并運算符。 以下示例使用?
throw
?表達式在要分配給屬性的字符串為?null
?時引發(fā)?ArgumentNullException:C#復制
public string Name {get => name;set => name = value ??throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null"); }
-
expression-bodied?lambda?或方法。 以下示例使用?
throw
?表達式引發(fā)?InvalidCastException,以指示不支持轉換為?DateTime?值:C#復制
DateTime ToDateTime(IFormatProvider provider) =>throw new InvalidCastException("Conversion to a DateTime is not supported.");
07.05.04.03?try
?語句
可以通過以下任何形式使用?try
?語句:try-catch?- 處理在?try
?塊內執(zhí)行代碼期間可能發(fā)生的異常,try-finally?- 指定在控件離開?try
?塊時執(zhí)行的代碼,以及?try-catch-finally?- 作為上述兩種形式的組合。
07.05.04.04?try-catch
?語句
使用?try-catch
?語句處理在執(zhí)行代碼塊期間可能發(fā)生的異常。 將代碼置于?try
?塊中可能發(fā)生異常的位置。 使用?catch 子句指定要在相應的?catch
?塊中處理的異常的基類型:
C#復制
try
{var result = Process(-3, 4);Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{Console.WriteLine($"Processing failed: {e.Message}");
}
可以提供多個 catch 子句:
C#復制
try
{var result = await ProcessAsync(-3, 4, cancellationToken);Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{Console.WriteLine($"Processing failed: {e.Message}");
}
catch (OperationCanceledException)
{Console.WriteLine("Processing is cancelled.");
}
發(fā)生異常時,將從上到下按指定順序檢查 catch 子句。 對于任何引發(fā)的異常,最多只執(zhí)行一個?catch
?塊。 如前面的示例所示,可以省略異常變量的聲明,并在 catch 子句中僅指定異常類型。 沒有任何指定異常類型的 catch 子句與任何異常匹配,如果存在,則必須是最后一個 catch 子句。
如果要重新引發(fā)捕獲的異常,請使用?throw?語句,如以下示例所示:
C#復制
try
{var result = Process(-3, 4);Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e)
{LogError(e, "Processing failed.");throw;
}
?備注
throw;
?保留異常的原始堆棧跟蹤,該跟蹤存儲在?Exception.StackTrace?屬性中。 與此相反,throw e;
?更新?e
?的?StackTrace?屬性。
07.05.04.05?when
?異常篩選器
除了異常類型之外,還可以指定異常篩選器,該篩選器進一步檢查異常并確定相應的?catch
?塊是否處理該異常。 異常篩選器是遵循?when
?關鍵字的布爾表達式,如以下示例所示:
C#復制
try
{var result = Process(-3, 4);Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
{Console.WriteLine($"Processing failed: {e.Message}");
}
前面的示例使用異常篩選器提供單個?catch
?塊來處理兩個指定類型的異常。
可以為相同異常類型提供若干?catch
?子句,如果它們通過異常篩選器區(qū)分。 其中一個子句可能沒有異常篩選器。 如果存在此類子句,則它必須是指定該異常類型的最后一個子句。
如果?catch
?子句具有異常篩選器,則可以指定與?catch
?子句之后出現(xiàn)的異常類型相同或小于派生的異常類型。 例如,如果存在異常篩選器,則?catch (Exception e)
?子句不需要是最后一個子句。
07.05.04.06 異步和迭代器方法中的異常
如果異步函數(shù)中發(fā)生異常,則等待函數(shù)的結果時,它會傳播到函數(shù)的調用方,如以下示例所示:
C#復制
public static async Task Run()
{try{Task<int> processing = ProcessAsync(-1);Console.WriteLine("Launched processing.");int result = await processing;Console.WriteLine($"Result: {result}.");}catch (ArgumentException e){Console.WriteLine($"Processing failed: {e.Message}");}// Output:// Launched processing.// Processing failed: Input must be non-negative. (Parameter 'input')
}private static async Task<int> ProcessAsync(int input)
{if (input < 0){throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");}await Task.Delay(500);return input;
}
如果迭代器方法中發(fā)生異常,則僅當?shù)髑斑M到下一個元素時,它才會傳播到調用方。
07.05.04.07?try-finally
?語句
在?try-finally
?語句中,當控件離開?try
?塊時,將執(zhí)行?finally
?塊。 控件可能會離開?try
?塊,因為
- 正常執(zhí)行,
- 執(zhí)行?jump 語句(即?
return
、break
、continue
?或?goto
),或 - 從?
try
?塊中傳播異常。
以下示例使用?finally
?塊在控件離開方法之前重置對象的狀態(tài):
C#復制
public async Task HandleRequest(int itemId, CancellationToken ct)
{Busy = true;try{await ProcessAsync(itemId, ct);}finally{Busy = false;}
}
還可以使用?finally
?塊來清理?try
?塊中使用的已分配資源。
?備注
當資源類型實現(xiàn)?IDisposable?或?IAsyncDisposable?接口時,請考慮?using?語句。?using
?語句可確保在控件離開?using
?語句時釋放獲取的資源。 編譯器將?using
?語句轉換為?try-finally
?語句。
finally
?塊的執(zhí)行取決于操作系統(tǒng)是否選擇觸發(fā)異常解除操作。 未執(zhí)行?finally
?塊的唯一情況涉及立即終止程序。 例如,由于?Environment.FailFast?調用或?OverflowException?或?InvalidProgramException?異常,可能會發(fā)生此類終止。 大多數(shù)操作系統(tǒng)在停止和卸載進程的過程中執(zhí)行合理的資源清理。
07.05.04.08?try-catch-finally
?語句
使用?try-catch-finally
?語句來處理在執(zhí)行?try
?塊期間可能發(fā)生的異常,并指定在控件離開?try
?語句時必須執(zhí)行的代碼:
C#復制
public async Task ProcessRequest(int itemId, CancellationToken ct)
{Busy = true;try{await ProcessAsync(itemId, ct);}catch (Exception e) when (e is not OperationCanceledException){LogError(e, $"Failed to process request for item ID {itemId}.");throw;}finally{Busy = false;}}
當?catch
?塊處理異常時,finally
?塊在執(zhí)行該?catch
?塊后執(zhí)行(即使執(zhí)行?catch
?塊期間發(fā)生另一個異常)。 有關?catch
?和?finally
?塊的信息,請分別參閱?try-catch?語句和?try-finally?語句?部分。
07.05.06?checked 和 unchecked 語句
07.05.06.01 checked unchecked
checked
?和?unchecked
?語句指定整型類型算術運算和轉換的溢出檢查上下文。 當發(fā)生整數(shù)算術溢出時,溢出檢查上下文將定義發(fā)生的情況。 在已檢查的上下文中,引發(fā)?System.OverflowException;如果在常數(shù)表達式中發(fā)生溢出,則會發(fā)生編譯時錯誤。 在未檢查的上下文中,會通過丟棄任何不適應目標類型的高序位來將操作結果截斷。 例如,在加法示例中,它將從最大值包裝到最小值。 以下示例顯示了已檢查和未檢查上下文中的相同操作:
C#復制運行
uint a = uint.MaxValue;unchecked
{Console.WriteLine(a + 3); // output: 2
}try
{checked{Console.WriteLine(a + 3);}
}
catch (OverflowException e)
{Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
?備注
用戶定義的運算符和溢出情況下的轉換行為可能與上一段中描述的不同。 特別是,用戶定義的 checked 運算符可能不會在已檢查的上下文中引發(fā)異常。
有關詳細信息,請參閱算術運算符一文的算術溢出和被零除以及用戶定義的 checked 運算符部分。
若要為表達式指定溢出檢查上下文,還可以使用?checked
?和?unchecked
?運算符,如以下示例所示:
C#復制運行
double a = double.MaxValue;int b = unchecked((int)a);
Console.WriteLine(b); // output: -2147483648try
{b = checked((int)a);
}
catch (OverflowException e)
{Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
checked
?和?unchecked
?語句和運算符僅影響以文本形式存在于語句塊或運算符括號內的操作的溢出檢查上下文,如以下示例所示:
C#復制運行
int Multiply(int a, int b) => a * b;int factor = 2;try
{checked{Console.WriteLine(Multiply(factor, int.MaxValue)); // output: -2}
}
catch (OverflowException e)
{Console.WriteLine(e.Message);
}try
{checked{Console.WriteLine(Multiply(factor, factor * int.MaxValue));}
}
catch (OverflowException e)
{Console.WriteLine(e.Message); // output: Arithmetic operation resulted in an overflow.
}
在前面的示例中,第一次調用?Multiply
?本地函數(shù)表明,checked
?語句不會影響?Multiply
?函數(shù)中的溢出檢查上下文,因為不會引發(fā)任何異常。 在第二次調用?Multiply
?函數(shù)時,計算函數(shù)第二個參數(shù)的表達式將在已檢查的上下文中計算,并導致異常,因為它以文本形式存在于?checked
?語句的塊內。
07.05.06.02 受溢出檢查上下文影響的操作
溢出檢查上下文會影響以下操作:
-
以下內置算術運算符:一元?
++
、--
、-
?和二元?+
、-
、*
?和?/
?運算符,當它們的操作數(shù)為整型類型(即整數(shù)或字符類型)或枚舉類型時。 -
整型類型之間或從?float?或?double?到整型類型的顯式數(shù)字轉換。
?備注
在將?
decimal
?值轉換為整型類型并且結果超出目標類型的范圍時,不管溢出檢查上下文如何,都始終會引發(fā)?OverflowException。 -
從 C# 11 開始,用戶定義的 checked 運算符和轉換。 有關詳細信息,請參閱算術運算符一文的用戶定義的 checked 運算符部分。
07.05.06.03 默認溢出檢查上下文
如果未指定溢出檢查上下文,則?CheckForOverflowUnderflow?編譯器選項的值將定義非常數(shù)表達式的默認上下文。 默認情況下,該選項的值未設置,并且整型算術運算和轉換在未檢查的上下文中執(zhí)行。
默認情況下,常數(shù)表達式在已檢查的上下文中計算,如果發(fā)生溢出,則會發(fā)生編譯時錯誤。 可以使用?unchecked
?語句或運算符為常數(shù)表達式顯式指定未檢查的上下文。
07.05.07?fixed 語句 - 固定用于指針操作的變量
fixed
?語句可防止垃圾回收器重新定位可移動變量,并聲明指向該變量的指針。 固定變量的地址在語句的持續(xù)時間內不會更改。 只能在相應的?fixed
?語句中使用聲明的指針。 聲明的指針是只讀的,無法修改:
C#復制
unsafe
{byte[] bytes = [1, 2, 3];fixed (byte* pointerToFirst = bytes){Console.WriteLine($"The address of the first array element: {(long)pointerToFirst:X}.");Console.WriteLine($"The value of the first array element: {*pointerToFirst}.");}
}
// Output is similar to:
// The address of the first array element: 2173F80B5C8.
// The value of the first array element: 1.
?備注
只能在不安全的上下文中使用?fixed
?語句。 必須使用?AllowUnsafeBlocks?編譯器選項來編譯包含不安全塊的代碼。
可以按如下所示初始化聲明的指針:
-
使用數(shù)組,如本文開頭的示例所示。 初始化的指針包含第一個數(shù)組元素的地址。
-
使用變量的地址。 使用?address-of?&?運算符,如以下示例所示:
C#復制
unsafe {int[] numbers = [10, 20, 30];fixed (int* toFirst = &numbers[0], toLast = &numbers[^1]){Console.WriteLine(toLast - toFirst); // output: 2} }
對象字段是可以固定的可移動變量的另一個示例。
當初始化的指針包含對象字段或數(shù)組元素的地址時,
fixed
?語句保證垃圾回收器在語句主體執(zhí)行期間不會重新定位或釋放包含對象實例。 -
使用實現(xiàn)名為?
GetPinnableReference
?的方法的類型的實例。 該方法必須返回非托管類型的?ref
?變量。 .NET 類型?System.Span<T>?和?System.ReadOnlySpan<T>?使用此模式。 可以固定跨度實例,如以下示例所示:C#復制
unsafe {int[] numbers = [10, 20, 30, 40, 50];Span<int> interior = numbers.AsSpan()[1..^1];fixed (int* p = interior){for (int i = 0; i < interior.Length; i++){Console.Write(p[i]); }// output: 203040} }
有關詳細信息,請參閱?Span<T>.GetPinnableReference()?API 參考。
-
使用字符串,如以下示例所示:
C#復制
unsafe {var message = "Hello!";fixed (char* p = message){Console.WriteLine(*p); // output: H} }
-
使用固定大小的緩沖區(qū)。
可以在堆棧上分配內存,在這種情況下,內存不受垃圾回收的約束,因此不需要固定。 為此,請使用?stackalloc?表達式。
還可以使用?fixed
?關鍵字聲明固定大小的緩沖區(qū)。
07.05.08?lock 語句 - 確保對共享資源的獨占訪問權限
lock
?語句獲取給定對象的互斥 lock,執(zhí)行語句塊,然后釋放 lock。 持有 lock 時,持有 lock 的線程可以再次獲取并釋放 lock。 阻止任何其他線程獲取 lock 并等待釋放 lock。?lock
?語句可確保在任何時候最多只有一個線程執(zhí)行其主體。
07.05.08.01?lock
?語句
C#復制
lock (x)
{// Your code...
}
變量?x
?是?System.Threading.Lock?類型或引用類型的表達式。 當?x
?在編譯時已知屬于類型?System.Threading.Lock?時,它完全等效于:
C#復制
using (x.EnterScope())
{// Your code...
}
Lock.EnterScope()?返回的對象是包括一個?Dispose()
?方法的?ref struct。 生成的?using?語句可確保即使?lock
?語句正文引發(fā)異常,也會釋放范圍。
否則,該?lock
?語句完全等效于:
C#復制
object __lockObj = x;
bool __lockWasTaken = false;
try
{System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);// Your code...
}
finally
{if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
由于該代碼使用?try-finally?語句,因此即使在?lock
?語句的正文中引發(fā)異常,也會釋放 lock。
在?lock
?語句的正文中不能使用?await?表達式。
07.05.08.02 準則
從 .NET 9 和 C# 13 開始,鎖定?System.Threading.Lock?類型的專用對象實例以獲取最佳性能。 此外,如果已知的?Lock
?對象被強制轉換為另一種類型并鎖定,編譯器會發(fā)出警告。 如果使用舊版的 .NET 和 C#,請鎖定不用于其他用途的專用對象實例。 避免對不同的共享資源使用相同的 lock 對象實例,因為這可能導致死鎖或鎖爭用。 具體而言,請避免將以下實例用作 lock 對象:
this
,因為調用方也可能鎖定?this
。- Type?實例(可以通過?typeof?運算符或反射獲取)。
- 字符串實例,包括字符串字面量,(這些可能是暫存的)。
盡可能縮短持有鎖的時間,以減少鎖爭用。
07.05.08.03 示例
以下示例定義了一個?Account
?類,該類通過鎖定專用的?balanceLock
?實例來同步對其專用?balance
?字段的訪問。 使用同一實例進行鎖定可確保兩個不同的線程不能同時調用?Debit
?或?Credit
?方法更新?balance
?字段。 此示例使用 C# 13 和新?Lock
?對象。 如果使用較舊版本的 C# 或較舊的 .NET 庫,請鎖定?object
?實例。
C#復制
using System;
using System.Threading.Tasks;public class Account
{// Use `object` in versions earlier than C# 13private readonly System.Threading.Lock _balanceLock = new();private decimal _balance;public Account(decimal initialBalance) => _balance = initialBalance;public decimal Debit(decimal amount){if (amount < 0){throw new ArgumentOutOfRangeException(nameof(amount), "The debit amount cannot be negative.");}decimal appliedAmount = 0;lock (_balanceLock){if (_balance >= amount){_balance -= amount;appliedAmount = amount;}}return appliedAmount;}public void Credit(decimal amount){if (amount < 0){throw new ArgumentOutOfRangeException(nameof(amount), "The credit amount cannot be negative.");}lock (_balanceLock){_balance += amount;}}public decimal GetBalance(){lock (_balanceLock){return _balance;}}
}class AccountTest
{static async Task Main(){var account = new Account(1000);var tasks = new Task[100];for (int i = 0; i < tasks.Length; i++){tasks[i] = Task.Run(() => Update(account));}await Task.WhenAll(tasks);Console.WriteLine($"Account's balance is {account.GetBalance()}");// Output:// Account's balance is 2000}static void Update(Account account){decimal[] amounts = [0, 2, -3, 6, -2, -1, 8, -5, 11, -6];foreach (var amount in amounts){if (amount >= 0){account.Credit(amount);}else{account.Debit(Math.Abs(amount));}}}
}
07.05.09 yield 語句 - 提供下一個元素
在迭代器中使用?yield
?語句提供下一個值或表示迭代結束。?yield
?語句有以下兩種形式:
-
yield return
:在迭代中提供下一個值,如以下示例所示:C#復制運行
foreach (int i in ProduceEvenNumbers(9)) {Console.Write(i);Console.Write(" "); } // Output: 0 2 4 6 8IEnumerable<int> ProduceEvenNumbers(int upto) {for (int i = 0; i <= upto; i += 2){yield return i;} }
-
yield break
:顯式示迭代結束,如以下示例所示:C#復制運行
Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {2, 3, 4, 5, -1, 3, 4}))); // Output: 2 3 4 5Console.WriteLine(string.Join(" ", TakeWhilePositive(new int[] {9, 8, 7}))); // Output: 9 8 7IEnumerable<int> TakeWhilePositive(IEnumerable<int> numbers) {foreach (int n in numbers){if (n > 0){yield return n;}else{yield break;}} }
當控件到達迭代器的末尾時,迭代也結束。
在前面的示例中,迭代器的返回類型為?IEnumerable<T>(在非泛型情況下,使用?IEnumerable?作為迭代器的返回類型)。 還可以使用?IAsyncEnumerable<T>?作為迭代器的返回類型。 這使得迭代器異步。 使用?await foreach?語句對迭代器的結果進行迭代,如以下示例所示:
C#復制
await foreach (int n in GenerateNumbersAsync(5))
{Console.Write(n);Console.Write(" ");
}
// Output: 0 2 4 6 8async IAsyncEnumerable<int> GenerateNumbersAsync(int count)
{for (int i = 0; i < count; i++){yield return await ProduceNumberAsync(i);}
}async Task<int> ProduceNumberAsync(int seed)
{await Task.Delay(1000);return 2 * seed;
}
迭代器的返回類型可以是?IEnumerator<T>?或?IEnumerator。 在以下方案中實現(xiàn)?GetEnumerator
?方法時,請使用這些返回類型:
-
設計實現(xiàn)?IEnumerable<T>?或?IEnumerable?接口的類型。
-
添加實例或擴展?
GetEnumerator
?方法來使用?foreach?語句對類型的實例啟用迭代,如以下示例所示:C#復制
public static void Example() {var point = new Point(1, 2, 3);foreach (int coordinate in point){Console.Write(coordinate);Console.Write(" ");}// Output: 1 2 3 }public readonly record struct Point(int X, int Y, int Z) {public IEnumerator<int> GetEnumerator(){yield return X;yield return Y;yield return Z;} }
不能在下列情況中使用?yield
?語句:
- 帶有?in、ref?或?out?參數(shù)的方法
- Lambda 表達式和匿名方法
- 不安全塊。 在 C# 13 之前,
yield
?在具有?unsafe
?塊的任何方法中都無效。 從 C# 13 開始,可以在包含?unsafe
?塊的方法中使用?yield
,但不能在?unsafe
?塊中使用。 yield return
?和?yield break
?不能在?try、catch?和?finally?塊中使用。
07.05.09.01 迭代器的執(zhí)行
迭代器的調用不會立即執(zhí)行,如以下示例所示:
C#復制運行
var numbers = ProduceEvenNumbers(5);
Console.WriteLine("Caller: about to iterate.");
foreach (int i in numbers)
{Console.WriteLine($"Caller: {i}");
}IEnumerable<int> ProduceEvenNumbers(int upto)
{Console.WriteLine("Iterator: start.");for (int i = 0; i <= upto; i += 2){Console.WriteLine($"Iterator: about to yield {i}");yield return i;Console.WriteLine($"Iterator: yielded {i}");}Console.WriteLine("Iterator: end.");
}
// Output:
// Caller: about to iterate.
// Iterator: start.
// Iterator: about to yield 0
// Caller: 0
// Iterator: yielded 0
// Iterator: about to yield 2
// Caller: 2
// Iterator: yielded 2
// Iterator: about to yield 4
// Caller: 4
// Iterator: yielded 4
// Iterator: end.
如前面的示例所示,當開始對迭代器的結果進行迭代時,迭代器會一直執(zhí)行,直到到達第一個?yield return
?語句為止。 然后,迭代器的執(zhí)行會暫停,調用方會獲得第一個迭代值并處理該值。 在后續(xù)的每次迭代中,迭代器的執(zhí)行都會在導致上一次掛起的?yield return
?語句之后恢復,并繼續(xù)執(zhí)行,直到到達下一個?yield return
?語句為止。 當控件到達迭代器或?yield break
?語句的末尾時,迭代完成。
07.06 字符串和字符串字面量
字符串是值為文本的?String?類型對象。 文本在內部存儲為?Char?對象的依序只讀集合。 在 C# 字符串末尾沒有 null 終止字符;因此,一個 C# 字符串可以包含任何數(shù)量的嵌入的 null 字符 ('\0')。 字符串的?Length?屬性表示其包含的?Char
?對象數(shù)量,而非 Unicode 字符數(shù)。 若要訪問字符串中的各個 Unicode 碼位,請使用?StringInfo?對象。
07.06.01 string 與System.String
在 C# 中,string
?關鍵字是?String?的別名。 因此,String
?和?string
?是等效的(雖然建議使用提供的別名?string
),因為即使不使用?using System;
,它也能正常工作。?String
?類提供了安全創(chuàng)建、操作和比較字符串的多種方法。 此外,C# 語言重載了部分運算符,以簡化常見字符串操作。 有關關鍵字的詳細信息,請參閱?string。 有關類型及其方法的詳細信息,請參閱?String。
07.06.02 聲明和初始化字符串
可以使用各種方法聲明和初始化字符串,如以下示例中所示:
C#復制
// Declare without initializing.
string message1;// Initialize to null.
string message2 = null;// Initialize as an empty string.
// Use the Empty constant instead of the literal "".
string message3 = System.String.Empty;// Initialize with a regular string literal.
string oldPath = "c:\\Program Files\\Microsoft Visual Studio 8.0";// Initialize with a verbatim string literal.
string newPath = @"c:\Program Files\Microsoft Visual Studio 9.0";// Use System.String if you prefer.
System.String greeting = "Hello World!";// In local variables (i.e. within a method body)
// you can use implicit typing.
var temp = "I'm still a strongly-typed System.String!";// Use a const string to prevent 'message4' from
// being used to store another string value.
const string message4 = "You can't get rid of me!";// Use the String constructor only when creating
// a string from a char*, char[], or sbyte*. See
// System.String documentation for details.
char[] letters = { 'A', 'B', 'C' };
string alphabet = new string(letters);
不要使用?new?運算符創(chuàng)建字符串對象,除非使用字符數(shù)組初始化字符串。
使用?Empty?常量值初始化字符串,以新建字符串長度為零的?String?對象。 長度為零的字符串文本表示法是“”。 通過使用?Empty?值(而不是?null)初始化字符串,可以減少?NullReferenceException?發(fā)生的可能性。 嘗試訪問字符串前,先使用靜態(tài)?IsNullOrEmpty(String)?方法驗證字符串的值。
07.06.03 字符串的不可變性
字符串對象是“不可變的”:它們在創(chuàng)建后無法更改。 看起來是在修改字符串的所有?String?方法和 C# 運算符實際上都是在新的字符串對象中返回結果。 在下面的示例中,當?s1
?和?s2
?的內容被串聯(lián)在一起以形成單個字符串時,兩個原始字符串沒有被修改。?+=
?運算符創(chuàng)建一個新的字符串,其中包含組合的內容。 這個新對象被分配給變量?s1
,而分配給?s1
?的原始對象被釋放,以供垃圾回收,因為沒有任何其他變量包含對它的引用。
C#復制
string s1 = "A string is more ";
string s2 = "than the sum of its chars.";// Concatenate s1 and s2. This actually creates a new
// string object and stores it in s1, releasing the
// reference to the original object.
s1 += s2;System.Console.WriteLine(s1);
// Output: A string is more than the sum of its chars.
由于字符串“modification”實際上是一個新創(chuàng)建的字符串,因此,必須在創(chuàng)建對字符串的引用時使用警告。 如果創(chuàng)建了字符串的引用,然后“修改”了原始字符串,則該引用將繼續(xù)指向原始對象,而非指向修改字符串時所創(chuàng)建的新對象。 以下代碼闡釋了此行為:
C#復制
string str1 = "Hello ";
string str2 = str1;
str1 += "World";System.Console.WriteLine(str2);
//Output: Hello
有關如何創(chuàng)建基于修改的新字符串的詳細信息,例如原始字符串上的搜索和替換操作,請參閱如何修改字符串內容。
07.06.04 帶引號的字符串字面量
帶引號的字符串字面量在同一行上以單個雙引號字符 ("
) 開頭和結尾。 帶引號的字符串字面量最適合匹配單個行且不包含任何轉義序列的字符串。 帶引號的字符串字面量必須嵌入轉義字符,如以下示例所示:
C#復制
string columns = "Column 1\tColumn 2\tColumn 3";
//Output: Column 1 Column 2 Column 3string rows = "Row 1\r\nRow 2\r\nRow 3";
/* Output:Row 1Row 2Row 3
*/string title = "\"The \u00C6olean Harp\", by Samuel Taylor Coleridge";
//Output: "The ?olean Harp", by Samuel Taylor Coleridge
07.06.05 逐字字符串文本
對于多行字符串、包含反斜杠字符或嵌入雙引號的字符串,逐字字符串字面量更方便。 逐字字符串將新的行字符作為字符串文本的一部分保留。 使用雙引號在逐字字符串內部嵌入引號。 下面的示例演示逐字字符串的一些常見用法:
C#復制
string filePath = @"C:\Users\scoleridge\Documents\";
//Output: C:\Users\scoleridge\Documents\string text = @"My pensive SARA ! thy soft cheek reclinedThus on mine arm, most soothing sweet it isTo sit beside our Cot,...";
/* Output:
My pensive SARA ! thy soft cheek reclinedThus on mine arm, most soothing sweet it isTo sit beside our Cot,...
*/string quote = @"Her name was ""Sara.""";
//Output: Her name was "Sara."
07.06.06 原始字符串文本
從 C# 11 開始,可以使用原始字符串字面量更輕松地創(chuàng)建多行字符串,或使用需要轉義序列的任何字符。 原始字符串字面量無需使用轉義序列。 你可以編寫字符串,包括空格格式,以及你希望在輸出中顯示該字符串的方式。 原始字符串字面量:
- 以至少三個雙引號字符序列 (
"""
) 開頭和結尾。 可以使用三個以上的連續(xù)字符開始和結束序列,以支持包含三個(或更多)重復引號字符的字符串字面量。 - 單行原始字符串字面量需要左引號和右引號字符位于同一行上。
- 多行原始字符串字面量需要左引號和右引號字符位于各自的行上。
- 在多行原始字符串字面量中,會刪除右引號左側的任何空格。
以下示例演示了這些規(guī)則:
C#復制
string singleLine = """Friends say "hello" as they pass by.""";
string multiLine = """"Hello World!" is typically the first program someone writes.""";
string embeddedXML = """<element attr = "content"><body style="normal">Here is the main text</body><footer>Excerpts from "An amazing story"</footer></element >""";
// The line "<element attr = "content">" starts in the first column.
// All whitespace left of that column is removed from the string.string rawStringLiteralDelimiter = """"Raw string literals are delimited by a string of at least three double quotes,like this: """"""";
以下示例演示了基于這些規(guī)則報告的編譯器錯誤:
C#復制
// CS8997: Unterminated raw string literal.
var multiLineStart = """Thisis the beginning of a string """;// CS9000: Raw string literal delimiter must be on its own line.
var multiLineEnd = """This is the beginning of a string """;// CS8999: Line does not start with the same whitespace as the closing line
// of the raw string literal
var noOutdenting = """A line of text.
Trying to outdent the second line.""";
前兩個示例無效,因為多行原始字符串字面量需要讓左引號和右引號序列在其自己的行上。 第三個示例無效,因為文本已從右引號序列中縮進。
使用帶引號的字符串字面量或逐字字符串字面量時,如果生成的文本包括需要轉義序列的字符,應考慮原始字符串字面量。 原始字符串字面量將更易于你和其他人閱讀,因為它更類似于輸出文本。 例如,請考慮包含格式化 JSON 字符串的以下代碼:
C#復制
string jsonString = """
{"Date": "2019-08-01T00:00:00-07:00","TemperatureCelsius": 25,"Summary": "Hot","DatesAvailable": ["2019-08-01T00:00:00-07:00","2019-08-02T00:00:00-07:00"],"TemperatureRanges": {"Cold": {"High": 20,"Low": -10},"Hot": {"High": 60,"Low": 20}},"SummaryWords": ["Cool","Windy","Humid"]
}
""";
將該文本與?JSON 序列化示例中的等效文本(沒有使用此新功能)進行比較。
07.06.07 字符串轉義序列
展開表
轉義序列 | 字符名稱 | Unicode 編碼 |
---|---|---|
\' | 單引號 | 0x0027 |
\" | 雙引號 | 0x0022 |
\ | 反斜杠 | 0x005C |
\0 | null | 0x0000 |
\a | 警報 | 0x0007 |
\b | Backspace | 0x0008 |
\f | 換頁 | 0x000C |
\n | 換行 | 0x000A |
\r | 回車 | 0x000D |
\t | 水平制表符 | 0x0009 |
\v | 垂直制表符 | 0x000B |
\u | Unicode 轉義序列 (UTF-16) | \uHHHH (范圍:0000 - FFFF;示例:\u00E7 ?=“?”) |
\U | Unicode 轉義序列 (UTF-32) | \U00HHHHHH (范圍:000000 - 10FFFF;示例:\U0001F47D ?= "👽") |
\x | 除長度可變外,Unicode 轉義序列與“\u”類似 | \xH[H][H][H] (范圍:0 - FFFF;示例:\x00E7 、\x0E7 ?或?\xE7 ?=“?”) |
?警告
使用?\x
?轉義序列且指定的位數(shù)小于 4 個十六進制數(shù)字時,如果緊跟在轉義序列后面的字符是有效的十六進制數(shù)字(即 0-9、A-F 和 a-f),則這些字符將被解釋為轉義序列的一部分。 例如,\xA1
?會生成“?”,即碼位 U+00A1。 但是,如果下一個字符是“A”或“a”,則轉義序列將轉而被解釋為?\xA1A
?并生成“?”(即碼位 U+0A1A)。 在此類情況下,如果指定全部 4 個十六進制數(shù)字(例如?\x00A1
),則可能導致解釋出錯。
?備注
在編譯時,逐字字符串被轉換為普通字符串,并具有所有相同的轉義序列。 因此,如果在調試器監(jiān)視窗口中查看逐字字符串,將看到由編譯器添加的轉義字符,而不是來自你的源代碼的逐字字符串版本。 例如,原義字符串?@"C:\files.txt"
?在監(jiān)視窗口中顯示為“C:\files.txt”。
07.06.08 格式字符串
格式字符串是在運行時以動態(tài)方式確定其內容的字符串。 格式字符串是通過將內插表達式或占位符嵌入字符串大括號內創(chuàng)建的。 大括號 ({...}
) 中的所有內容都將解析為一個值,并在運行時以格式化字符串的形式輸出。 有兩種方法創(chuàng)建格式字符串:字符串內插和復合格式。
07.06.09 字符串內插
在 C# 6.0 及更高版本中提供,內插字符串由?$
?特殊字符標識,并在大括號中包含內插表達式。 如果不熟悉字符串內插,請參閱字符串內插 - C# 交互式教程快速概覽。
使用字符串內插來改善代碼的可讀性和可維護性。 字符串內插可實現(xiàn)與?String.Format
?方法相同的結果,但提高了易用性和內聯(lián)清晰度。
C#復制
var jh = (firstName: "Jupiter", lastName: "Hammon", born: 1711, published: 1761);
Console.WriteLine($"{jh.firstName} {jh.lastName} was an African American poet born in {jh.born}.");
Console.WriteLine($"He was first published in {jh.published} at the age of {jh.published - jh.born}.");
Console.WriteLine($"He'd be over {Math.Round((2018d - jh.born) / 100d) * 100d} years old today.");// Output:
// Jupiter Hammon was an African American poet born in 1711.
// He was first published in 1761 at the age of 50.
// He'd be over 300 years old today.
從 C# 10 開始,當用于占位符的所有表達式也是常量字符串時,可以使用字符串內插來初始化常量字符串。
從 C# 11 開始,可以將原始字符串字面量與字符串內插結合使用。 使用三個或更多個連續(xù)雙引號開始和結束格式字符串。 如果輸出字符串應包含?{
?或?}
?字符,則可以使用額外的?$
?字符來指定開始和結束內插的?{
?和?}
?字符數(shù)。 輸出中包含任何更少的?{
?或?}
?字符序列。 以下示例演示了如何使用該功能來顯示點與原點的距離,以及如何將點置于大括號中:
C#復制
int X = 2;
int Y = 3;var pointMessage = $$"""The point {{{X}}, {{Y}}} is {{Math.Sqrt(X * X + Y * Y)}} from the origin.""";Console.WriteLine(pointMessage);
// Output:
// The point {2, 3} is 3.605551275463989 from the origin.
07.06.10 復合格式設置
String.Format?利用大括號中的占位符創(chuàng)建格式字符串。 此示例生成與上面使用的字符串內插方法類似的輸出。
C#復制
var pw = (firstName: "Phillis", lastName: "Wheatley", born: 1753, published: 1773);
Console.WriteLine("{0} {1} was an African American poet born in {2}.", pw.firstName, pw.lastName, pw.born);
Console.WriteLine("She was first published in {0} at the age of {1}.", pw.published, pw.published - pw.born);
Console.WriteLine("She'd be over {0} years old today.", Math.Round((2018d - pw.born) / 100d) * 100d);// Output:
// Phillis Wheatley was an African American poet born in 1753.
// She was first published in 1773 at the age of 20.
// She'd be over 300 years old today.
有關設置 .NET 類型格式的詳細信息,請參閱?.NET 中的格式設置類型。
07.06.11 子字符串
子字符串是包含在字符串中的任何字符序列。 使用?Substring?方法可以通過原始字符串的一部分新建字符串。 可以使用?IndexOf?方法搜索一次或多次出現(xiàn)的子字符串。 使用?Replace?方法可以將出現(xiàn)的所有指定子字符串替換為新字符串。 與?Substring?方法一樣,Replace?實際返回的是新字符串,且不修改原始字符串。 有關詳細信息,請參閱如何搜索字符串和如何修改字符串內容。
C#復制
string s3 = "Visual C# Express";
System.Console.WriteLine(s3.Substring(7, 2));
// Output: "C#"System.Console.WriteLine(s3.Replace("C#", "Basic"));
// Output: "Visual Basic Express"// Index values are zero-based
int index = s3.IndexOf("C");
// index = 7
07.06.12 訪問單個字符
可以使用包含索引值的數(shù)組表示法來獲取對單個字符的只讀訪問權限,如下面的示例中所示:
C#復制
string s5 = "Printing backwards";for (int i = 0; i < s5.Length; i++)
{System.Console.Write(s5[s5.Length - i - 1]);
}
// Output: "sdrawkcab gnitnirP"
如果?String?方法不提供修改字符串中的各個字符所需的功能,可以使用?StringBuilder?對象“就地”修改各個字符,再新建字符串來使用?StringBuilder?方法存儲結果。 在下面的示例中,假定必須以特定方式修改原始字符串,然后存儲結果以供未來使用:
C#復制
string question = "hOW DOES mICROSOFT wORD DEAL WITH THE cAPS lOCK KEY?";
System.Text.StringBuilder sb = new System.Text.StringBuilder(question);for (int j = 0; j < sb.Length; j++)
{if (System.Char.IsLower(sb[j]) == true)sb[j] = System.Char.ToUpper(sb[j]);else if (System.Char.IsUpper(sb[j]) == true)sb[j] = System.Char.ToLower(sb[j]);
}
// Store the new string.
string corrected = sb.ToString();
System.Console.WriteLine(corrected);
// Output: How does Microsoft Word deal with the Caps Lock key?
07.06.13 Null 字符串和空字符串
空字符串是包含零個字符的?System.String?對象實例。 空字符串常用在各種編程方案中,表示空文本字段。 可以對空字符串調用方法,因為它們是有效的?System.String?對象。 對空字符串進行了初始化,如下所示:
C#復制
string s = String.Empty;
相比較而言,null 字符串并不指?System.String?對象實例,只要嘗試對 null 字符串調用方法,都會引發(fā)?NullReferenceException。 但是,可以在串聯(lián)和與其他字符串的比較操作中使用 null 字符串。 以下示例說明了對 null 字符串的引用會引發(fā)和不會引發(fā)意外的某些情況:
C#復制
string str = "hello";
string nullStr = null;
string emptyStr = String.Empty;string tempStr = str + nullStr;
// Output of the following line: hello
Console.WriteLine(tempStr);bool b = (emptyStr == nullStr);
// Output of the following line: False
Console.WriteLine(b);// The following line creates a new empty string.
string newStr = emptyStr + nullStr;// Null strings and empty strings behave differently. The following
// two lines display 0.
Console.WriteLine(emptyStr.Length);
Console.WriteLine(newStr.Length);
// The following line raises a NullReferenceException.
//Console.WriteLine(nullStr.Length);// The null character can be displayed and counted, like other chars.
string s1 = "\x0" + "abc";
string s2 = "abc" + "\x0";
// Output of the following line: * abc*
Console.WriteLine("*" + s1 + "*");
// Output of the following line: *abc *
Console.WriteLine("*" + s2 + "*");
// Output of the following line: 4
Console.WriteLine(s2.Length);
07.06.14 使用 StringBuilder 快速創(chuàng)建字符串
.NET 中的字符串操作進行了高度的優(yōu)化,在大多數(shù)情況下不會顯著影響性能。 但是,在某些情況下(例如,執(zhí)行數(shù)百次或數(shù)千次的緊密循環(huán)),字符串操作可能影響性能。?StringBuilder?類創(chuàng)建字符串緩沖區(qū),用于在程序執(zhí)行多個字符串操控時提升性能。 使用?StringBuilder?字符串,還可以重新分配各個字符,而內置字符串數(shù)據(jù)類型則不支持這樣做。 例如,此代碼更改字符串的內容,而無需創(chuàng)建新的字符串:
C#復制
System.Text.StringBuilder sb = new System.Text.StringBuilder("Rat: the ideal pet");
sb[0] = 'C';
System.Console.WriteLine(sb.ToString());
//Outputs Cat: the ideal pet
在以下示例中,StringBuilder?對象用于通過一組數(shù)字類型創(chuàng)建字符串:
C#復制
var sb = new StringBuilder();// Create a string composed of numbers 0 - 9
for (int i = 0; i < 10; i++)
{sb.Append(i.ToString());
}
Console.WriteLine(sb); // displays 0123456789// Copy one character of the string (not possible with a System.String)
sb[0] = sb[9];Console.WriteLine(sb); // displays 9123456789
07.06.15 字符串、擴展方法和 LINQ
由于?String?類型實現(xiàn)?IEnumerable<T>,因此可以對字符串使用?Enumerable?類中定義的擴展方法。 為了避免視覺干擾,這些方法已從?String?類型的 IntelliSense 中排除,但它們仍然可用。 還可以使用字符串上的 LINQ 查詢表達式。 有關詳細信息,請參閱?LINQ 和字符串。
07.06.16?如何確定字符串是否表示數(shù)值
若要確定字符串是否是指定數(shù)值類型的有效表示形式,請使用由所有基元數(shù)值類型以及如?DateTime?和?IPAddress?等類型實現(xiàn)的靜態(tài)?TryParse
?方法。 以下示例演示如何確定“108”是否為有效的?int。
C#復制
int i = 0;
string s = "108";
bool result = int.TryParse(s, out i); //i now = 108
如果該字符串包含非數(shù)字字符,或者數(shù)值對于指定的特定類型而言太大或太小,則?TryParse
?將返回 false 并將 out 參數(shù)設置為零。 否則,它將返回 true 并將 out 參數(shù)設置為字符串的數(shù)值。
?備注
字符串可能僅包含數(shù)字字符,但對于你使用的?TryParse
?方法的類型仍然無效。 例如,“256”不是?byte
?的有效值,但對?int
?有效。 “98.6”不是?int
?的有效值,但它是有效的?decimal
。
以下示例演示如何對?long
、byte
?和?decimal
?值的字符串表示形式使用?TryParse
。
C#復制
string numString = "1287543"; //"1287543.0" will return false for a long
long number1 = 0;
bool canConvert = long.TryParse(numString, out number1);
if (canConvert == true)
Console.WriteLine("number1 now = {0}", number1);
else
Console.WriteLine("numString is not a valid long");byte number2 = 0;
numString = "255"; // A value of 256 will return false
canConvert = byte.TryParse(numString, out number2);
if (canConvert == true)
Console.WriteLine("number2 now = {0}", number2);
else
Console.WriteLine("numString is not a valid byte");decimal number3 = 0;
numString = "27.3"; //"27" is also a valid decimal
canConvert = decimal.TryParse(numString, out number3);
if (canConvert == true)
Console.WriteLine("number3 now = {0}", number3);
else
Console.WriteLine("number3 is not a valid decimal");
08 類型轉換專題
08.01?強制轉換和類型轉換
由于 C# 是在編譯時靜態(tài)類型化的,因此變量在聲明后就無法再次聲明,或無法分配另一種類型的值,除非該類型可以隱式轉換為變量的類型。 例如,string
?無法隱式轉換為?int
。 因此,在將?i
?聲明為?int
?后,無法將字符串“Hello”分配給它,如以下代碼所示:
C#
int i;// error CS0029: can't implicitly convert type 'string' to 'int'
i = "Hello";
但有時可能需要將值復制到其他類型的變量或方法參數(shù)中。 例如,可能需要將一個整數(shù)變量傳遞給參數(shù)類型化為?double
?的方法。 或者可能需要將類變量分配給接口類型的變量。 這些類型的操作稱為類型轉換。 在 C# 中,可以執(zhí)行以下幾種類型的轉換:
-
隱式轉換:不需要特殊語法,因為轉換始終會成功,并且不會丟失任何數(shù)據(jù)。 示例包括從較小整數(shù)類型到較大整數(shù)類型的轉換以及從派生類到基類的轉換。
-
顯式轉換(強制轉換)?:必須使用強制轉換表達式,才能執(zhí)行顯式轉換。 在轉換中可能丟失信息時或在出于其他原因轉換可能不成功時,必須進行強制轉換。 典型的示例包括從數(shù)值到精度較低或范圍較小的類型的轉換和從基類實例到派生類的轉換。
-
用戶定義的轉換:用戶定義的轉換使用你可以定義的特殊方法,以支持在不具有基類和派生類關系的自定義類型之間實現(xiàn)顯式和隱式轉換。 有關詳細信息,請參閱用戶定義轉換運算符。
-
使用幫助程序類進行轉換:若要在非兼容類型(如整數(shù)和?System.DateTime?對象,或十六進制字符串和字節(jié)數(shù)組)之間轉換,可使用?System.BitConverter?類、System.Convert?類和內置數(shù)值類型的?
Parse
?方法(如?Int32.Parse)。 有關詳細信息,請參見如何將字節(jié)數(shù)組轉換為 int、如何將字符串轉換為數(shù)字和如何在十六進制字符串與數(shù)值類型之間轉換。
08.01.01 隱式轉換
對于內置數(shù)值類型,如果要存儲的值無需截斷或四舍五入即可適應變量,則可以進行隱式轉換。 對于整型類型,這意味著源類型的范圍是目標類型范圍的正確子集。 例如,long?類型的變量(64 位整數(shù))能夠存儲?int(32 位整數(shù))可存儲的任何值。 在下面的示例中,編譯器先將右側的?num
?值隱式轉換為?long
?類型,再將它賦給?bigNum
。
C#
// Implicit conversion. A long can
// hold any value an int can hold, and more!
int num = 2147483647;
long bigNum = num;
有關所有隱式數(shù)值轉換的完整列表,請參閱內置數(shù)值轉換一文的隱式數(shù)值轉換表部分。
對于引用類型,隱式轉換始終存在于從一個類轉換為該類的任何一個直接或間接的基類或接口的情況。 由于派生類始終包含基類的所有成員,因此不必使用任何特殊語法。
C#
Derived d = new Derived();// Always OK.
Base b = d;
08.01.02 顯式轉換
但是,如果進行轉換可能會導致信息丟失,則編譯器會要求執(zhí)行顯式轉換,顯式轉換也稱為強制轉換。 強制轉換是顯式告知編譯器以下信息的一種方式:你打算進行轉換且你知道可能會發(fā)生數(shù)據(jù)丟失,或者你知道強制轉換有可能在運行時失敗。 若要執(zhí)行強制轉換,請在要轉換的值或變量前面的括號中指定要強制轉換到的類型。 下面的程序將?double?強制轉換為?int。如不強制轉換,程序將無法編譯。
C#
class Test
{static void Main(){double x = 1234.7;int a;// Cast double to int.a = (int)x;System.Console.WriteLine(a);}
}
// Output: 1234
有關支持的顯式數(shù)值轉換的完整列表,請參閱內置數(shù)值轉換一文的顯式數(shù)值轉換部分。
對于引用類型,如果需要從基類型轉換為派生類型,則必須進行顯式強制轉換:
C#
// Create a new derived type.
Giraffe g = new Giraffe();// Implicit conversion to base type is safe.
Animal a = g;// Explicit conversion is required to cast back
// to derived type. Note: This will compile but will
// throw an exception at run time if the right-side
// object is not in fact a Giraffe.
Giraffe g2 = (Giraffe)a;
引用類型之間的強制轉換操作不會更改基礎對象的運行時類型;它只更改用作對該對象引用的值的類型。 有關詳細信息,請參閱多態(tài)性。
08.01.03 運行時的類型轉換異常
在某些引用類型轉換中,編譯器無法確定強制轉換是否會有效。 正確進行編譯的強制轉換操作有可能在運行時失敗。 如下面的示例所示,類型轉換在運行時失敗將導致引發(fā)?InvalidCastException。
C#
class Animal
{public void Eat() => System.Console.WriteLine("Eating.");public override string ToString() => "I am an animal.";
}class Reptile : Animal { }
class Mammal : Animal { }class UnSafeCast
{static void Main(){Test(new Mammal());// Keep the console window open in debug mode.System.Console.WriteLine("Press any key to exit.");System.Console.ReadKey();}static void Test(Animal a){// System.InvalidCastException at run time// Unable to cast object of type 'Mammal' to type 'Reptile'Reptile r = (Reptile)a;}
}
Test
?方法有一個?Animal
?形式參數(shù),因此,將實際參數(shù)?a
?顯式強制轉換為?Reptile
?會造成危險的假設。 更安全的做法是不要做出假設,而是檢查類型。 C# 提供?is?運算符,使你可以在實際執(zhí)行強制轉換之前測試兼容性。 有關詳細信息,請參閱如何使用模式匹配以及 as 和 is 運算符安全地進行強制轉換。
08.02?裝箱和取消裝箱
裝箱是將值類型轉換為?object
?類型或由此值類型實現(xiàn)的任何接口類型的過程。 常見語言運行時 (CLR) 對值類型進行裝箱時,會將值包裝在?System.Object?實例中并將其存儲在托管堆中。 取消裝箱將從對象中提取值類型。 裝箱是隱式的;取消裝箱是顯式的。 裝箱和取消裝箱的概念是類型系統(tǒng) C# 統(tǒng)一視圖的基礎,其中任一類型的值都被視為一個對象。
下例將整型變量?i
?進行了裝箱并分配給對象?o
。
C#
int i = 123;
// The following line boxes i.
object o = i;
然后,可以將對象?o
?取消裝箱并分配給整型變量?i
:
C#
o = 123;
i = (int)o; // unboxing
以下示例演示如何在 C# 中使用裝箱。
C#
// String.Concat example.
// String.Concat has many versions. Rest the mouse pointer on
// Concat in the following statement to verify that the version
// that is used here takes three object arguments. Both 42 and
// true must be boxed.
Console.WriteLine(String.Concat("Answer", 42, true));// List example.
// Create a list of objects to hold a heterogeneous collection
// of elements.
List<object> mixedList = new List<object>();// Add a string element to the list.
mixedList.Add("First Group:");// Add some integers to the list.
for (int j = 1; j < 5; j++)
{// Rest the mouse pointer over j to verify that you are adding// an int to a list of objects. Each element j is boxed when// you add j to mixedList.mixedList.Add(j);
}// Add another string and more integers.
mixedList.Add("Second Group:");
for (int j = 5; j < 10; j++)
{mixedList.Add(j);
}// Display the elements in the list. Declare the loop variable by
// using var, so that the compiler assigns its type.
foreach (var item in mixedList)
{// Rest the mouse pointer over item to verify that the elements// of mixedList are objects.Console.WriteLine(item);
}// The following loop sums the squares of the first group of boxed
// integers in mixedList. The list elements are objects, and cannot
// be multiplied or added to the sum until they are unboxed. The
// unboxing must be done explicitly.
var sum = 0;
for (var j = 1; j < 5; j++)
{// The following statement causes a compiler error: Operator// '*' cannot be applied to operands of type 'object' and// 'object'.//sum += mixedList[j] * mixedList[j];// After the list elements are unboxed, the computation does// not cause a compiler error.sum += (int)mixedList[j] * (int)mixedList[j];
}// The sum displayed is 30, the sum of 1 + 4 + 9 + 16.
Console.WriteLine("Sum: " + sum);// Output:
// Answer42True
// First Group:
// 1
// 2
// 3
// 4
// Second Group:
// 5
// 6
// 7
// 8
// 9
// Sum: 30
08.02.01 性能
相對于簡單的賦值而言,裝箱和取消裝箱過程需要進行大量的計算。 對值類型進行裝箱時,必須分配并構造一個新對象。 取消裝箱所需的強制轉換也需要進行大量的計算,只是程度較輕。 有關更多信息,請參閱性能。
08.02.02 裝箱
裝箱用于在垃圾回收堆中存儲值類型。 裝箱是值類型到?object
?類型或到此值類型所實現(xiàn)的任何接口類型的隱式轉換。 對值類型裝箱會在堆中分配一個對象實例,并將該值復制到新的對象中。
請看以下值類型變量的聲明:
C#
int i = 123;
以下語句對變量?i
?隱式應用了裝箱操作:
C#
// Boxing copies the value of i into object o.
object o = i;
此語句的結果是在堆棧上創(chuàng)建對象引用?o
,而在堆上則引用?int
?類型的值。 該值是賦給變量?i
?的值類型值的一個副本。 以下裝箱轉換圖說明了?i
?和?o
?這兩個變量之間的差異:
還可以像下面的示例一樣執(zhí)行顯式裝箱,但顯式裝箱從來不是必需的:
C#
int i = 123;
object o = (object)i; // explicit boxing
08.02.03 示例
此示例使用裝箱將整型變量?i
?轉換為對象?o
。 這樣一來,存儲在變量?i
?中的值就從?123
?更改為?456
。 該示例表明原始值類型和裝箱的對象使用不同的內存位置,因此能夠存儲不同的值。
C#
class TestBoxing
{static void Main(){int i = 123;// Boxing copies the value of i into object o.object o = i;// Change the value of i.i = 456;// The change in i doesn't affect the value stored in o.System.Console.WriteLine("The value-type value = {0}", i);System.Console.WriteLine("The object-type value = {0}", o);}
}
/* Output:The value-type value = 456The object-type value = 123
*/
08.02.04 取消裝箱
取消裝箱是從?object
?類型到值類型或從接口類型到實現(xiàn)該接口的值類型的顯式轉換。 取消裝箱操作包括:
-
檢查對象實例,以確保它是給定值類型的裝箱值。
-
將該值從實例復制到值類型變量中。
下面的語句演示裝箱和取消裝箱兩種操作:
C#
int i = 123; // a value type
object o = i; // boxing
int j = (int)o; // unboxing
下圖演示了上述語句的結果:
要在運行時成功取消裝箱值類型,被取消裝箱的項必須是對一個對象的引用,該對象是先前通過裝箱該值類型的實例創(chuàng)建的。 嘗試取消裝箱?null
?會導致?NullReferenceException。 嘗試取消裝箱對不兼容值類型的引用會導致?InvalidCastException。
08.02.05 示例
下面的示例演示無效的取消裝箱及引發(fā)的?InvalidCastException
。 使用?try
?和?catch
,在發(fā)生錯誤時顯示錯誤信息。
C#
class TestUnboxing
{static void Main(){int i = 123;object o = i; // implicit boxingtry{int j = (short)o; // attempt to unboxSystem.Console.WriteLine("Unboxing OK.");}catch (System.InvalidCastException e){System.Console.WriteLine("{0} Error: Incorrect unboxing.", e.Message);}}
}
此程序輸出:
Specified cast is not valid. Error: Incorrect unboxing.
如果將下列語句:
C#
int j = (short)o;
更改為:
C#
int j = (int)o;
將執(zhí)行轉換,并將得到以下輸出:
Unboxing OK.
08.03 數(shù)值轉換方法
08.03.01?字節(jié)數(shù)組轉換為 int
此示例演示如何使用?BitConverter?類將字節(jié)數(shù)組轉換為?int?然后又轉換回字節(jié)數(shù)組。 例如,在從網(wǎng)絡讀取字節(jié)之后,可能需要將字節(jié)轉換為內置數(shù)據(jù)類型。 除了示例中的?ToInt32(Byte[], Int32)?方法之外,下表還列出了?BitConverter?類中將字節(jié)(來自字節(jié)數(shù)組)轉換為其他內置類型的方法。
返回類型 | 方法 |
---|---|
bool | ToBoolean(Byte[], Int32) |
char | ToChar(Byte[], Int32) |
double | ToDouble(Byte[], Int32) |
short | ToInt16(Byte[], Int32) |
int | ToInt32(Byte[], Int32) |
long | ToInt64(Byte[], Int32) |
float | ToSingle(Byte[], Int32) |
ushort | ToUInt16(Byte[], Int32) |
uint | ToUInt32(Byte[], Int32) |
ulong | ToUInt64(Byte[], Int32) |
08.03.02 示例
此示例初始化字節(jié)數(shù)組,并在計算機體系結構為 little-endian(即首先存儲最低有效字節(jié))的情況下反轉數(shù)組,然后調用?ToInt32(Byte[], Int32)?方法以將數(shù)組中的四個字節(jié)轉換為?int
。?ToInt32(Byte[], Int32)?的第二個參數(shù)指定字節(jié)數(shù)組的起始索引。
?備注
輸出可能會根據(jù)計算機體系結構的字節(jié)順序而不同。
C#
byte[] bytes = [0, 0, 0, 25];// If the system architecture is little-endian (that is, little end first),
// reverse the byte array.
if (BitConverter.IsLittleEndian)Array.Reverse(bytes);int i = BitConverter.ToInt32(bytes, 0);
Console.WriteLine("int: {0}", i);
// Output: int: 25
在本示例中,將調用?BitConverter?類的?GetBytes(Int32)?方法,將?int
?轉換為字節(jié)數(shù)組。
?備注
輸出可能會根據(jù)計算機體系結構的字節(jié)順序而不同。
C#
byte[] bytes = BitConverter.GetBytes(201805978);
Console.WriteLine("byte array: " + BitConverter.ToString(bytes));
// Output: byte array: 9A-50-07-0C
08.03.02?字符串轉換為數(shù)字
你可以調用數(shù)值類型(int
、long
、double
?等)中找到的?Parse
?或?TryParse
?方法或使用?System.Convert?類中的方法將?string
?轉換為數(shù)字。
調用?TryParse
?方法(例如,int.TryParse("11", out number))或?Parse
?方法(例如,var number = int.Parse("11"))會稍微高效和簡單一些。 使用?Convert?方法對于實現(xiàn)?IConvertible?的常規(guī)對象更有用。
對預期字符串會包含的數(shù)值類型(如?System.Int32?類型)使用?Parse
?或?TryParse
?方法。?Convert.ToInt32?方法在內部使用?Parse。?Parse
?方法返回轉換后的數(shù)字;TryParse
?方法返回布爾值,該值指示轉換是否成功,并以?out
?參數(shù)形式返回轉換后的數(shù)字。 如果字符串的格式無效,則?Parse
?會引發(fā)異常,但?TryParse
?會返回?false
。 調用?Parse
?方法時,應始終使用異常處理來捕獲分析操作失敗時的?FormatException。
08.03.03 調用 Parse 或 TryParse 方法
Parse
?和?TryParse
?方法會忽略字符串開頭和末尾的空格,但所有其他字符都必須是組成合適數(shù)值類型(int
、long
、ulong
、float
、decimal
?等)的字符。 如果組成數(shù)字的字符串中有任何空格,都會導致錯誤。 例如,可以使用?decimal.TryParse
?分析“10”、“10.3”或“ 10 ”,但不能使用此方法分析從“10X”、“1 0”(注意嵌入的空格)、“10 .3”(注意嵌入的空格)、“10e1”(float.TryParse
?在此處適用)等中分析出 10。 無法成功分析值為?null
?或?String.Empty?的字符串。 在嘗試通過調用?String.IsNullOrEmpty?方法分析字符串之前,可以檢查字符串是否為 Null 或為空。
下面的示例演示了對?Parse
?和?TryParse
?的成功調用和不成功的調用。
C#
using System;public static class StringConversion
{public static void Main(){string input = String.Empty;try{int result = Int32.Parse(input);Console.WriteLine(result);}catch (FormatException){Console.WriteLine($"Unable to parse '{input}'");}// Output: Unable to parse ''try{int numVal = Int32.Parse("-105");Console.WriteLine(numVal);}catch (FormatException e){Console.WriteLine(e.Message);}// Output: -105if (Int32.TryParse("-105", out int j)){Console.WriteLine(j);}else{Console.WriteLine("String could not be parsed.");}// Output: -105try{int m = Int32.Parse("abc");}catch (FormatException e){Console.WriteLine(e.Message);}// Output: Input string was not in a correct format.const string inputString = "abc";if (Int32.TryParse(inputString, out int numValue)){Console.WriteLine(numValue);}else{Console.WriteLine($"Int32.TryParse could not parse '{inputString}' to an int.");}// Output: Int32.TryParse could not parse 'abc' to an int.}
}
下面的示例演示了一種分析字符串的方法,該字符串應包含前導數(shù)字字符(包括十六進制字符)和尾隨的非數(shù)字字符。 在調用?TryParse?方法之前,它從字符串的開頭向新字符串分配有效字符。 因為要分析的字符串包含少量字符,所以本示例調用?String.Concat?方法將有效字符分配給新字符串。 對于較大的字符串,可以改用?StringBuilder?類。
C#
using System;public static class StringConversion
{public static void Main(){var str = " 10FFxxx";string numericString = string.Empty;foreach (var c in str){// Check for numeric characters (hex in this case) or leading or trailing spaces.if ((c >= '0' && c <= '9') || (char.ToUpperInvariant(c) >= 'A' && char.ToUpperInvariant(c) <= 'F') || c == ' '){numericString = string.Concat(numericString, c.ToString());}else{break;}}if (int.TryParse(numericString, System.Globalization.NumberStyles.HexNumber, null, out int i)){Console.WriteLine($"'{str}' --> '{numericString}' --> {i}");}// Output: ' 10FFxxx' --> ' 10FF' --> 4351str = " -10FFXXX";numericString = "";foreach (char c in str){// Check for numeric characters (0-9), a negative sign, or leading or trailing spaces.if ((c >= '0' && c <= '9') || c == ' ' || c == '-'){numericString = string.Concat(numericString, c);}else{break;}}if (int.TryParse(numericString, out int j)){Console.WriteLine($"'{str}' --> '{numericString}' --> {j}");}// Output: ' -10FFXXX' --> ' -10' --> -10}
}
08.03.04 調用 Convert 方法
下表列出了?Convert?類中可用于將字符串轉換為數(shù)字的一些方法。
數(shù)值類型 | 方法 |
---|---|
decimal | ToDecimal(String) |
float | ToSingle(String) |
double | ToDouble(String) |
short | ToInt16(String) |
int | ToInt32(String) |
long | ToInt64(String) |
ushort | ToUInt16(String) |
uint | ToUInt32(String) |
ulong | ToUInt64(String) |
下面的示例調用?Convert.ToInt32(String)?方法將輸入字符串轉換為?int。該示例將捕獲由此方法引發(fā)的兩個最常見異常:FormatException?和?OverflowException。 如果生成的數(shù)字可以在不超過?Int32.MaxValue?的情況下遞增,則示例將向結果添加 1 并顯示輸出。
C#
using System;public class ConvertStringExample1
{static void Main(string[] args){int numVal = -1;bool repeat = true;while (repeat){Console.Write("Enter a number between ?2,147,483,648 and +2,147,483,647 (inclusive): ");string? input = Console.ReadLine();// ToInt32 can throw FormatException or OverflowException.try{numVal = Convert.ToInt32(input);if (numVal < Int32.MaxValue){Console.WriteLine("The new value is {0}", ++numVal);}else{Console.WriteLine("numVal cannot be incremented beyond its current value");}}catch (FormatException){Console.WriteLine("Input string is not a sequence of digits.");}catch (OverflowException){Console.WriteLine("The number cannot fit in an Int32.");}Console.Write("Go again? Y/N: ");string? go = Console.ReadLine();if (go?.ToUpper() != "Y"){repeat = false;}}}
}
// Sample Output:
// Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): 473
// The new value is 474
// Go again? Y/N: y
// Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): 2147483647
// numVal cannot be incremented beyond its current value
// Go again? Y/N: y
// Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): -1000
// The new value is -999
// Go again? Y/N: n
08.03.05 十六進制字符串與數(shù)值類型之間轉換
以下示例演示如何執(zhí)行下列任務:
-
獲取字符串中每個字符的十六進制值。
-
獲取與十六進制字符串中的每個值對應的?char。
-
將十六進制?
string
?轉換為?int。 -
將十六進制?
string
?轉換為?float。 -
將字節(jié)數(shù)組轉換為十六進制?
string
。
此示例輸出?string
?中每個字符的十六進制值。 首先,將?string
?分析為字符數(shù)組。 然后,對每個字符調用?ToInt32(Char)獲取相應的數(shù)值。 最后,在?string
?中將數(shù)字的格式設置為十六進制表示形式。
C#
string input = "Hello World!";
char[] values = input.ToCharArray();
foreach (char letter in values)
{// Get the integral value of the character.int value = Convert.ToInt32(letter);// Convert the integer value to a hexadecimal value in string form.Console.WriteLine($"Hexadecimal value of {letter} is {value:X}");
}
/* Output:Hexadecimal value of H is 48Hexadecimal value of e is 65Hexadecimal value of l is 6CHexadecimal value of l is 6CHexadecimal value of o is 6FHexadecimal value of is 20Hexadecimal value of W is 57Hexadecimal value of o is 6FHexadecimal value of r is 72Hexadecimal value of l is 6CHexadecimal value of d is 64Hexadecimal value of ! is 21*/
此示例分析十六進制值的?string
?并輸出對應于每個十六進制值的字符。 首先,調用?Split(Char[])?方法以獲取每個十六進制值作為數(shù)組中的單個?string
。 然后,調用?ToInt32(String, Int32)將十六進制值轉換為表示為?int?的十進制值。示例中演示了 2 種不同方法,用于獲取對應于該字符代碼的字符。 第 1 種方法是使用?ConvertFromUtf32(Int32),它將對應于整型參數(shù)的字符作為?string
?返回。 第 2 種方法是將?int
?顯式轉換為?char。
C#
string hexValues = "48 65 6C 6C 6F 20 57 6F 72 6C 64 21";
string[] hexValuesSplit = hexValues.Split(' ');
foreach (string hex in hexValuesSplit)
{// Convert the number expressed in base-16 to an integer.int value = Convert.ToInt32(hex, 16);// Get the character corresponding to the integral value.string stringValue = Char.ConvertFromUtf32(value);char charValue = (char)value;Console.WriteLine("hexadecimal value = {0}, int value = {1}, char value = {2} or {3}",hex, value, stringValue, charValue);
}
/* Output:hexadecimal value = 48, int value = 72, char value = H or Hhexadecimal value = 65, int value = 101, char value = e or ehexadecimal value = 6C, int value = 108, char value = l or lhexadecimal value = 6C, int value = 108, char value = l or lhexadecimal value = 6F, int value = 111, char value = o or ohexadecimal value = 20, int value = 32, char value = orhexadecimal value = 57, int value = 87, char value = W or Whexadecimal value = 6F, int value = 111, char value = o or ohexadecimal value = 72, int value = 114, char value = r or rhexadecimal value = 6C, int value = 108, char value = l or lhexadecimal value = 64, int value = 100, char value = d or dhexadecimal value = 21, int value = 33, char value = ! or !
*/
此示例演示了將十六進制?string
?轉換為整數(shù)的另一種方法,即調用?Parse(String, NumberStyles)?方法。
C#
string hexString = "8E2";
int num = Int32.Parse(hexString, System.Globalization.NumberStyles.HexNumber);
Console.WriteLine(num);
//Output: 2274
下面的示例演示了如何使用?System.BitConverter?類和?UInt32.Parse?方法將十六進制?string
?轉換為?float。
C#
string hexString = "43480170";
uint num = uint.Parse(hexString, System.Globalization.NumberStyles.AllowHexSpecifier);byte[] floatVals = BitConverter.GetBytes(num);
float f = BitConverter.ToSingle(floatVals, 0);
Console.WriteLine("float convert = {0}", f);// Output: 200.0056
下面的示例演示了如何使用?System.BitConverter?類將字節(jié)數(shù)組轉換為十六進制字符串。
C#
byte[] vals = [0x01, 0xAA, 0xB1, 0xDC, 0x10, 0xDD];string str = BitConverter.ToString(vals);
Console.WriteLine(str);str = BitConverter.ToString(vals).Replace("-", "");
Console.WriteLine(str);/*Output:01-AA-B1-DC-10-DD01AAB1DC10DD*/
下面的示例演示如何通過調用 .NET 5.0 中引入的?Convert.ToHexString?方法,將字節(jié)數(shù)組轉換為十六進制字符串。
C#
byte[] array = [0x64, 0x6f, 0x74, 0x63, 0x65, 0x74];string hexValue = Convert.ToHexString(array);
Console.WriteLine(hexValue);/*Output:646F74636574*/
09 實用技術
09.01 數(shù)據(jù)初始化,對象和集合初始值設定項
使用 C# 可以在單條語句中實例化對象或集合并執(zhí)行成員分配。
09.01.01 對象初始值設定項
使用對象初始值設定項,你可以在創(chuàng)建對象時向對象的任何可訪問字段或屬性分配值,而無需調用后跟賦值語句行的構造函數(shù)。 利用對象初始值設定項語法,你可為構造函數(shù)指定參數(shù)或忽略參數(shù)(以及括號語法)。 以下示例演示如何使用具有命名類型?Cat
?的對象初始值設定項以及如何調用無參數(shù)構造函數(shù)。 注意自動實現(xiàn)的屬性在?Cat
?類中的用法。 有關詳細信息,請參閱?自動實現(xiàn)的屬性。
C#復制
public class Cat
{// Automatically implemented properties.public int Age { get; set; }public string? Name { get; set; }public Cat(){}public Cat(string name){this.Name = name;}
}
C#復制
Cat cat = new Cat { Age = 10, Name = "Fluffy" };
Cat sameCat = new Cat("Fluffy"){ Age = 10 };
對象初始值設定項語法允許你創(chuàng)建一個實例,然后將具有其分配屬性的新建對象指定給賦值中的變量。
除了分配字段和屬性外,對象初始值設定項還可以設置索引器。 請思考這個基本的?Matrix
?類:
C#復制
public class Matrix
{private double[,] storage = new double[3, 3];public double this[int row, int column]{// The embedded array will throw out of range exceptions as appropriate.get { return storage[row, column]; }set { storage[row, column] = value; }}
}
可以使用以下代碼初始化標識矩陣:
C#復制
var identity = new Matrix
{[0, 0] = 1.0,[0, 1] = 0.0,[0, 2] = 0.0,[1, 0] = 0.0,[1, 1] = 1.0,[1, 2] = 0.0,[2, 0] = 0.0,[2, 1] = 0.0,[2, 2] = 1.0,
};
包含可訪問資源庫的任何可訪問索引器都可以用作對象初始值設定項中的表達式之一,這與參數(shù)的數(shù)量或類型無關。 索引參數(shù)構成左側賦值,而表達式右側是值。 例如,如果?IndexersExample
?具有適當?shù)乃饕?#xff0c;則以下初始值設定項都是有效的:
C#復制
var thing = new IndexersExample
{name = "object one",[1] = '1',[2] = '4',[3] = '9',Size = Math.PI,['C',4] = "Middle C"
}
對于要進行編譯的前面的代碼,IndexersExample
?類型必須具有以下成員:
C#復制
public string name;
public double Size { set { ... }; }
public char this[int i] { set { ... }; }
public string this[char c, int i] { set { ... }; }
09.01.02 具有匿名類型的對象初始值設定項
盡管對象初始值設定項可用于任何上下文中,但它們在 LINQ 查詢表達式中特別有用。 查詢表達式常使用只能通過使用對象初始值設定項進行初始化的匿名類型,如下面的聲明所示。
C#復制
var pet = new { Age = 10, Name = "Fluffy" };
利用匿名類型,LINQ 查詢表達式中的?select
?子句可以將原始序列的對象轉換為其值和形狀可能不同于原始序列的對象。 建議只存儲某個序列中每個對象的部分信息。 在下面的示例中,假定產(chǎn)品對象 (p
) 包含很多字段和方法,而你只想創(chuàng)建包含產(chǎn)品名和單價的對象序列。
C#復制
var productInfos =from p in productsselect new { p.ProductName, p.UnitPrice };
執(zhí)行此查詢時,productInfos
?變量包含一系列對象,這些對象可以在?foreach
?語句中進行訪問,如下面的示例所示:
C#復制
foreach(var p in productInfos){...}
新的匿名類型中的每個對象都具有兩個公共屬性,這兩個屬性接收與原始對象中的屬性或字段相同的名稱。 你還可在創(chuàng)建匿名類型時重命名字段;下面的示例將?UnitPrice
?字段重命名為?Price
。
C#復制
select new {p.ProductName, Price = p.UnitPrice};
09.01.03 帶?required
?修飾符的對象初始值設定項
可以使用?required
?關鍵字強制調用方使用對象初始值設定項設置屬性或字段的值。 不需要將所需屬性設置為構造函數(shù)參數(shù)。 編譯器可確保所有調用方初始化這些值。
C#復制
public class Pet
{public required int Age;public string Name;
}// `Age` field is necessary to be initialized.
// You don't need to initialize `Name` property
var pet = new Pet() { Age = 10};// Compiler error:
// Error CS9035 Required member 'Pet.Age' must be set in the object initializer or attribute constructor.
// var pet = new Pet();
通常的做法是保證對象正確初始化,尤其是在要管理多個字段或屬性,并且不希望將它們全部包含在構造函數(shù)中時。
09.01.04 帶?init
?訪問器的對象初始值設定項
確保無人更改設計,并且可以使用訪問器來限制?init
?對象。 它有助于限制屬性值的設置。
C#復制
public class Person
{public string FirstName { get; set; }public string LastName { get; init; }
}// The `LastName` property can be set only during initialization. It CAN'T be modified afterwards.
// The `FirstName` property can be modified after initialization.
var pet = new Person() { FirstName = "Joe", LastName = "Doe"};// You can assign the FirstName property to a different value.
pet.FirstName = "Jane";// Compiler error:
// Error CS8852 Init - only property or indexer 'Person.LastName' can only be assigned in an object initializer,
// or on 'this' or 'base' in an instance constructor or an 'init' accessor.
// pet.LastName = "Kowalski";
必需的僅限 init 的屬性支持不可變結構,同時允許該類型用戶使用自然語法。
09.01.05 具有類類型屬性的對象初始值設定項
初始化對象時,考慮類類型屬性的含義至關重要:
C#復制
public class HowToClassTypedInitializer
{public class EmbeddedClassTypeA{public int I { get; set; }public bool B { get; set; }public string S { get; set; }public EmbeddedClassTypeB ClassB { get; set; }public override string ToString() => $"{I}|{B}|{S}|||{ClassB}";public EmbeddedClassTypeA(){Console.WriteLine($"Entering EmbeddedClassTypeA constructor. Values are: {this}");I = 3;B = true;S = "abc";ClassB = new() { BB = true, BI = 43 };Console.WriteLine($"Exiting EmbeddedClassTypeA constructor. Values are: {this})");}}public class EmbeddedClassTypeB{public int BI { get; set; }public bool BB { get; set; }public string BS { get; set; }public override string ToString() => $"{BI}|{BB}|{BS}";public EmbeddedClassTypeB(){Console.WriteLine($"Entering EmbeddedClassTypeB constructor. Values are: {this}");BI = 23;BB = false;BS = "BBBabc";Console.WriteLine($"Exiting EmbeddedClassTypeB constructor. Values are: {this})");}}public static void Main(){var a = new EmbeddedClassTypeA{I = 103,B = false,ClassB = { BI = 100003 }};Console.WriteLine($"After initializing EmbeddedClassTypeA: {a}");var a2 = new EmbeddedClassTypeA{I = 103,B = false,ClassB = new() { BI = 100003 } //New instance};Console.WriteLine($"After initializing EmbeddedClassTypeA a2: {a2}");}// Output://Entering EmbeddedClassTypeA constructor Values are: 0|False||||//Entering EmbeddedClassTypeB constructor Values are: 0|False|//Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)//Exiting EmbeddedClassTypeA constructor Values are: 3|True|abc|||43|True|BBBabc)//After initializing EmbeddedClassTypeA: 103|False|abc|||100003|True|BBBabc//Entering EmbeddedClassTypeA constructor Values are: 0|False||||//Entering EmbeddedClassTypeB constructor Values are: 0|False|//Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)//Exiting EmbeddedClassTypeA constructor Values are: 3|True|abc|||43|True|BBBabc)//Entering EmbeddedClassTypeB constructor Values are: 0|False|//Exiting EmbeddedClassTypeB constructor Values are: 23|False|BBBabc)//After initializing EmbeddedClassTypeA a2: 103|False|abc|||100003|False|BBBabc
}
以下示例演示了對于 ClassB,初始化過程如何涉及到更新特定的值,同時保留原始實例中的其他值。 初始值設定項重用當前實例:ClassB 的值為:100003
(此處分配的新值)、true
(在 EmbeddedClassTypeA 的初始化中保留的值)、BBBabc
(EmbeddedClassTypeB 中未更改的默認值)。
09.01.06 集合初始值設定項
在初始化實現(xiàn)?IEnumerable?的集合類型和初始化使用適當?shù)暮灻鳛閷嵗椒ɑ驍U展方法的?Add
?時,集合初始值設定項允許指定一個或多個元素初始值設定項。 元素初始值設定項可以是值、表達式或對象初始值設定項。 通過使用集合初始值設定項,無需指定多個調用;編譯器將自動添加這些調用。
下面的示例演示了兩個簡單的集合初始值設定項:
C#復制
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
List<int> digits2 = new List<int> { 0 + 1, 12 % 3, MakeInt() };
下面的集合初始值設定項使用對象初始值設定項來初始化上一個示例中定義的?Cat
?類的對象。 各個對象初始值設定項括在大括號中且用逗號隔開。
C#復制
List<Cat> cats = new List<Cat>
{new Cat{ Name = "Sylvester", Age=8 },new Cat{ Name = "Whiskers", Age=2 },new Cat{ Name = "Sasha", Age=14 }
};
如果集合的?Add
?方法允許,則可以將?null?指定為集合初始值設定項中的一個元素。
C#復制
List<Cat?> moreCats = new List<Cat?>
{new Cat{ Name = "Furrytail", Age=5 },new Cat{ Name = "Peaches", Age=4 },null
};
如果集合支持讀取/寫入索引,可以指定索引元素。
C#復制
var numbers = new Dictionary<int, string>
{[7] = "seven",[9] = "nine",[13] = "thirteen"
};
前面的示例生成調用?Item[TKey]?以設置值的代碼。 還可使用以下語法初始化字典和其他關聯(lián)容器。 請注意,它使用具有多個值的對象,而不是帶括號和賦值的索引器語法:
C#復制
var moreNumbers = new Dictionary<int, string>
{{19, "nineteen" },{23, "twenty-three" },{42, "forty-two" }
};
此初始值設定項示例調用?Add(TKey, TValue),將這三個項添加到字典中。 由于編譯器生成的方法調用不同,這兩種初始化關聯(lián)集合的不同方法的行為略有不同。 這兩種變量都適用于?Dictionary
?類。 其他類型根據(jù)它們的公共 API 可能只支持兩者中的一種。
09.01.07 具有集合只讀屬性初始化的對象初始值設定項
某些類可能具有屬性為只讀的集合屬性,如以下示例中?CatOwner
?的?Cats
?屬性:
C#復制
public class CatOwner
{public IList<Cat> Cats { get; } = new List<Cat>();
}
由于無法為屬性分配新列表,因此你無法使用迄今為止討論的集合初始值設定項語法:
C#復制
CatOwner owner = new CatOwner
{Cats = new List<Cat>{new Cat{ Name = "Sylvester", Age=8 },new Cat{ Name = "Whiskers", Age=2 },new Cat{ Name = "Sasha", Age=14 }}
};
但是,可以通過省略列表創(chuàng)建 (new List<Cat>
),使用初始化語法將新條目添加到?Cats
,如下所示:
C#復制
CatOwner owner = new CatOwner
{Cats ={new Cat{ Name = "Sylvester", Age=8 },new Cat{ Name = "Whiskers", Age=2 },new Cat{ Name = "Sasha", Age=14 }}
};
要添加的條目集顯示在大括號中。 上述代碼與編寫代碼相同:
C#復制
CatOwner owner = new ();
owner.Cats.Add(new Cat{ Name = "Sylvester", Age=8 });
owner.Cats.Add(new Cat{ Name = "Whiskers", Age=2 });
owner.Cats.Add(new Cat{ Name = "Sasha", Age=14 });
09.01.08 數(shù)據(jù)初始化示例
下例結合了對象和集合初始值設定項的概念。
C#復制
public class InitializationSample
{public class Cat{// Automatically implemented properties.public int Age { get; set; }public string? Name { get; set; }public Cat() { }public Cat(string name){Name = name;}}public static void Main(){Cat cat = new Cat { Age = 10, Name = "Fluffy" };Cat sameCat = new Cat("Fluffy"){ Age = 10 };List<Cat> cats = new List<Cat>{new Cat { Name = "Sylvester", Age = 8 },new Cat { Name = "Whiskers", Age = 2 },new Cat { Name = "Sasha", Age = 14 }};List<Cat?> moreCats = new List<Cat?>{new Cat { Name = "Furrytail", Age = 5 },new Cat { Name = "Peaches", Age = 4 },null};// Display results.System.Console.WriteLine(cat.Name);foreach (Cat c in cats){System.Console.WriteLine(c.Name);}foreach (Cat? c in moreCats){if (c != null){System.Console.WriteLine(c.Name);}else{System.Console.WriteLine("List element has null value.");}}}// Output://Fluffy//Sylvester//Whiskers//Sasha//Furrytail//Peaches//List element has null value.
}
以下示例演示了一個對象,該對象實現(xiàn)?IEnumerable?并包含具有多個參數(shù)的?Add
?方法。 它使用一個集合初始值設定項,其中列表中的每個項都有多個元素,這些元素對應于?Add
?方法的簽名。
C#復制
public class FullExample
{class FormattedAddresses : IEnumerable<string>{private List<string> internalList = new List<string>();public IEnumerator<string> GetEnumerator() => internalList.GetEnumerator();System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => internalList.GetEnumerator();public void Add(string firstname, string lastname,string street, string city,string state, string zipcode) => internalList.Add($"""{firstname} {lastname}{street}{city}, {state} {zipcode}""");}public static void Main(){FormattedAddresses addresses = new FormattedAddresses(){{"John", "Doe", "123 Street", "Topeka", "KS", "00000" },{"Jane", "Smith", "456 Street", "Topeka", "KS", "00000" }};Console.WriteLine("Address Entries:");foreach (string addressEntry in addresses){Console.WriteLine("\r\n" + addressEntry);}}/** Prints:Address Entries:John Doe123 StreetTopeka, KS 00000Jane Smith456 StreetTopeka, KS 00000*/
}
Add
?方法可使用?params
?關鍵字來獲取可變數(shù)量的自變量,如下例中所示。 此示例還演示了索引器的自定義實現(xiàn),以使用索引初始化集合。 從 C# 13 開始,params
?參數(shù)不再局限于數(shù)組。 它可以是集合類型或接口。
C#復制
public class DictionaryExample
{class RudimentaryMultiValuedDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, List<TValue>>> where TKey : notnull{private Dictionary<TKey, List<TValue>> internalDictionary = new Dictionary<TKey, List<TValue>>();public IEnumerator<KeyValuePair<TKey, List<TValue>>> GetEnumerator() => internalDictionary.GetEnumerator();System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => internalDictionary.GetEnumerator();public List<TValue> this[TKey key]{get => internalDictionary[key];set => Add(key, value);}public void Add(TKey key, params TValue[] values) => Add(key, (IEnumerable<TValue>)values);public void Add(TKey key, IEnumerable<TValue> values){if (!internalDictionary.TryGetValue(key, out List<TValue>? storedValues)){internalDictionary.Add(key, storedValues = new List<TValue>());}storedValues.AddRange(values);}}public static void Main(){RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary1= new RudimentaryMultiValuedDictionary<string, string>(){{"Group1", "Bob", "John", "Mary" },{"Group2", "Eric", "Emily", "Debbie", "Jesse" }};RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary2= new RudimentaryMultiValuedDictionary<string, string>(){["Group1"] = new List<string>() { "Bob", "John", "Mary" },["Group2"] = new List<string>() { "Eric", "Emily", "Debbie", "Jesse" }};RudimentaryMultiValuedDictionary<string, string> rudimentaryMultiValuedDictionary3= new RudimentaryMultiValuedDictionary<string, string>(){{"Group1", new string []{ "Bob", "John", "Mary" } },{ "Group2", new string[]{ "Eric", "Emily", "Debbie", "Jesse" } }};Console.WriteLine("Using first multi-valued dictionary created with a collection initializer:");foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary1){Console.WriteLine($"\r\nMembers of group {group.Key}: ");foreach (string member in group.Value){Console.WriteLine(member);}}Console.WriteLine("\r\nUsing second multi-valued dictionary created with a collection initializer using indexing:");foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary2){Console.WriteLine($"\r\nMembers of group {group.Key}: ");foreach (string member in group.Value){Console.WriteLine(member);}}Console.WriteLine("\r\nUsing third multi-valued dictionary created with a collection initializer using indexing:");foreach (KeyValuePair<string, List<string>> group in rudimentaryMultiValuedDictionary3){Console.WriteLine($"\r\nMembers of group {group.Key}: ");foreach (string member in group.Value){Console.WriteLine(member);}}}/** Prints:Using first multi-valued dictionary created with a collection initializer:Members of group Group1:BobJohnMaryMembers of group Group2:EricEmilyDebbieJesseUsing second multi-valued dictionary created with a collection initializer using indexing:Members of group Group1:BobJohnMaryMembers of group Group2:EricEmilyDebbieJesseUsing third multi-valued dictionary created with a collection initializer using indexing:Members of group Group1:BobJohnMaryMembers of group Group2:EricEmilyDebbieJesse*/
}
09.01.09?如何使用對象初始值設定項初始化對象
可以使用對象初始值設定項以聲明方式初始化類型對象,而無需顯式調用類型的構造函數(shù)。
以下示例演示如何將對象初始值設定項用于命名對象。 編譯器通過首先訪問無參數(shù)實例構造函數(shù),然后處理成員初始化來處理對象初始值設定項。 因此,如果無參數(shù)構造函數(shù)在類中聲明為?private
,則需要公共訪問的對象初始值設定項將失敗。
如果要定義匿名類型,則必須使用對象初始值設定項。 有關詳細信息,請參閱如何在查詢中返回元素屬性的子集。
下面的示例演示如何使用對象初始值設定項初始化新的?StudentName
?類型。 此示例在?StudentName
?類型中設置屬性:
C#復制
public class HowToObjectInitializers
{public static void Main(){// Declare a StudentName by using the constructor that has two parameters.StudentName student1 = new StudentName("Craig", "Playstead");// Make the same declaration by using an object initializer and sending// arguments for the first and last names. The parameterless constructor is// invoked in processing this declaration, not the constructor that has// two parameters.StudentName student2 = new StudentName{FirstName = "Craig",LastName = "Playstead"};// Declare a StudentName by using an object initializer and sending// an argument for only the ID property. No corresponding constructor is// necessary. Only the parameterless constructor is used to process object// initializers.StudentName student3 = new StudentName{ID = 183};// Declare a StudentName by using an object initializer and sending// arguments for all three properties. No corresponding constructor is// defined in the class.StudentName student4 = new StudentName{FirstName = "Craig",LastName = "Playstead",ID = 116};Console.WriteLine(student1.ToString());Console.WriteLine(student2.ToString());Console.WriteLine(student3.ToString());Console.WriteLine(student4.ToString());}// Output:// Craig 0// Craig 0// 183// Craig 116public class StudentName{// This constructor has no parameters. The parameterless constructor// is invoked in the processing of object initializers.// You can test this by changing the access modifier from public to// private. The declarations in Main that use object initializers will// fail.public StudentName() { }// The following constructor has parameters for two of the three// properties.public StudentName(string first, string last){FirstName = first;LastName = last;}// Properties.public string? FirstName { get; set; }public string? LastName { get; set; }public int ID { get; set; }public override string ToString() => FirstName + " " + ID;}
}
對象初始值設定項可用于在對象中設置索引器。 下面的示例定義了一個?BaseballTeam
?類,該類使用索引器獲取和設置不同位置的球員。 初始值設定項可以根據(jù)位置的縮寫或每個位置的棒球記分卡的編號來分配球員:
C#復制
public class HowToIndexInitializer
{public class BaseballTeam{private string[] players = new string[9];private readonly List<string> positionAbbreviations = new List<string>{"P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"};public string this[int position]{// Baseball positions are 1 - 9.get { return players[position-1]; }set { players[position-1] = value; }}public string this[string position]{get { return players[positionAbbreviations.IndexOf(position)]; }set { players[positionAbbreviations.IndexOf(position)] = value; }}}public static void Main(){var team = new BaseballTeam{["RF"] = "Mookie Betts",[4] = "Jose Altuve",["CF"] = "Mike Trout"};Console.WriteLine(team["2B"]);}
}
下一個示例演示使用帶參數(shù)和不帶參數(shù)的構造函數(shù)執(zhí)行構造函數(shù)和成員初始化的順序:
C#復制
public class ObjectInitializersExecutionOrder
{public static void Main(){new Person { FirstName = "Paisley", LastName = "Smith", City = "Dallas" };new Dog(2) { Name = "Mike" };}public class Dog{private int age;private string name;public Dog(int age){Console.WriteLine("Hello from Dog's non-parameterless constructor");this.age = age;}public required string Name{get { return name; }set{Console.WriteLine("Hello from setter of Dog's required property 'Name'");name = value;}}}public class Person{private string firstName;private string lastName;private string city;public Person(){Console.WriteLine("Hello from Person's parameterless constructor");}public required string FirstName{get { return firstName; }set{Console.WriteLine("Hello from setter of Person's required property 'FirstName'");firstName = value;}}public string LastName{get { return lastName; }init{Console.WriteLine("Hello from setter of Person's init property 'LastName'");lastName = value;}}public string City{get { return city; }set{Console.WriteLine("Hello from setter of Person's property 'City'");city = value;}}}// Output:// Hello from Person's parameterless constructor// Hello from setter of Person's required property 'FirstName'// Hello from setter of Person's init property 'LastName'// Hello from setter of Person's property 'City'// Hello from Dog's non-parameterless constructor// Hello from setter of Dog's required property 'Name'
}
09.01.10?如何使用集合初始值設定項初始化字典
Dictionary<TKey,TValue>?包含鍵/值對集合。 其?Add?方法采用兩個參數(shù),一個用于鍵,一個用于值。 若要初始化?Dictionary<TKey,TValue>?或其?Add
?方法采用多個參數(shù)的任何集合,一種方法是將每組參數(shù)括在大括號中,如下面的示例中所示。 另一種方法是使用索引初始值設定項,如下面的示例所示。
?備注
我們以具有重復鍵的情況來舉例說明初始化集合的這兩種方法之間的主要區(qū)別:
C#復制
{ 111, new StudentName { FirstName="Sachin", LastName="Karnik", ID=211 } },
{ 111, new StudentName { FirstName="Dina", LastName="Salimzianova", ID=317 } },
Add?方法將引發(fā)?ArgumentException:'An item with the same key has already been added. Key: 111'
,而示例的第二部分(公共讀/寫索引器方法)將使用相同的鍵靜默覆蓋已存在的條目。
在下面的代碼示例中,使用類型?StudentName
?的實例初始化?Dictionary<TKey,TValue>。 第一個初始化使用具有兩個參數(shù)的?Add
?方法。 編譯器為每對?int
?鍵和?StudentName
?值生成對?Add
?的調用。 第二個初始化使用?Dictionary
?類的公共讀取/寫入索引器方法:
C#復制
public class HowToDictionaryInitializer
{class StudentName{public string? FirstName { get; set; }public string? LastName { get; set; }public int ID { get; set; }}public static void Main(){var students = new Dictionary<int, StudentName>(){{ 111, new StudentName { FirstName="Sachin", LastName="Karnik", ID=211 } },{ 112, new StudentName { FirstName="Dina", LastName="Salimzianova", ID=317 } },{ 113, new StudentName { FirstName="Andy", LastName="Ruth", ID=198 } }};foreach(var index in Enumerable.Range(111, 3)){Console.WriteLine($"Student {index} is {students[index].FirstName} {students[index].LastName}");}Console.WriteLine(); var students2 = new Dictionary<int, StudentName>(){[111] = new StudentName { FirstName="Sachin", LastName="Karnik", ID=211 },[112] = new StudentName { FirstName="Dina", LastName="Salimzianova", ID=317 } ,[113] = new StudentName { FirstName="Andy", LastName="Ruth", ID=198 }};foreach (var index in Enumerable.Range(111, 3)){Console.WriteLine($"Student {index} is {students2[index].FirstName} {students2[index].LastName}");}}
}
請注意,在第一個聲明中,集合中的每個元素有兩對大括號。 最內層的大括號中括住了?StudentName
?的對象初始值設定項,最外層的大括號則括住了要添加到?students
Dictionary<TKey,TValue>?的鍵/值對的初始值設定項。 最后,字典的整個集合初始值設定項被括在大括號中。 在第二個初始化中,左側賦值是鍵,右側是將對象初始值設定項用于?StudentName
?的值。
09.02?嵌套類型
在類、構造或接口中定義的類型稱為嵌套類型。 例如
C#復制
public class Container
{class Nested{Nested() { }}
}
不論外部類型是類、接口還是構造,嵌套類型均默認為?private;僅可從其包含類型中進行訪問。 在上一個示例中,Nested
?類無法訪問外部類型。
還可指定訪問修飾符來定義嵌套類型的可訪問性,如下所示:
-
“類”的嵌套類型可以是?public、protected、internal、protected internal、private?或?private protected。
但是,在密封類中定義?
protected
、protected internal
?或?private protected
?嵌套類將產(chǎn)生編譯器警告?CS0628“封閉類匯中聲明了新的受保護成員”。另請注意,使嵌套類型在外部可見違反了代碼質量規(guī)則?CA1034“嵌套類型不應是可見的”。
-
構造的嵌套類型可以是?public、internal?或?private。
以下示例使?Nested
?類為 public:
C#復制
public class Container
{public class Nested{Nested() { }}
}
嵌套類型(或內部類型)可訪問包含類型(或外部類型)。 若要訪問包含類型,請將其作為參數(shù)傳遞給嵌套類型的構造函數(shù)。 例如:
C#復制
public class Container
{public class Nested{private Container? parent;public Nested(){}public Nested(Container parent){this.parent = parent;}}
}
嵌套類型可以訪問其包含類型可以訪問的所有成員。 它可以訪問包含類型的私有成員和受保護成員(包括所有繼承的受保護成員)。
在前面的聲明中,類?Nested
?的完整名稱為?Container.Nested
。 這是用來創(chuàng)建嵌套類新實例的名稱,如下所示:
C#復制
Container.Nested nest = new Container.Nested();
09.03?分部類和方法
拆分一個類、一個結構、一個接口或一個方法的定義到兩個或更多的文件中是可能的。 每個源文件包含類型或方法定義的一部分,編譯應用程序時將把所有部分組合起來。
09.03.01 分部類 partial class?
在以下幾種情況下需要拆分類定義:
- 通過單獨的文件聲明某個類可以讓多位程序員同時對該類進行處理。
- 你可以向該類中添加代碼,而不必重新創(chuàng)建包括自動生成的源代碼的源文件。 Visual Studio 在創(chuàng)建Windows 窗體、Web 服務包裝器代碼等時會使用這種方法。 你可以創(chuàng)建使用這些類的代碼,這樣就不需要修改由Visual Studio生成的文件。
- 源代碼生成器可以在類中生成額外的功能。
若要拆分類定義,請使用?partial?關鍵字修飾符,如下所示:
C#復制
public partial class Employee
{public void DoWork(){}
}public partial class Employee
{public void GoToLunch(){}
}
partial
?關鍵字指示可在命名空間中定義該類、結構或接口的其他部分。 所有部分都必須使用?partial
?關鍵字。 在編譯時,各個部分都必須可用來形成最終的類型。 各個部分必須具有相同的可訪問性,如?public
、private
?等。
如果將任意部分聲明為抽象的,則整個類型都被視為抽象的。 如果將任意部分聲明為密封的,則整個類型都被視為密封的。 如果任意部分聲明基類型,則整個類型都將繼承該類。
指定基類的所有部分必須一致,但忽略基類的部分仍繼承該基類型。 各個部分可以指定不同的基接口,最終類型將實現(xiàn)所有分部聲明所列出的全部接口。 在某一分部定義中聲明的任何類、結構或接口成員可供所有其他部分使用。 最終類型是所有部分在編譯時的組合。
?備注
partial
?修飾符不可用于委托或枚舉聲明中。
下面的示例演示嵌套類型可以是分部的,即使它們所嵌套于的類型本身并不是分部的也如此。
C#復制
class Container
{partial class Nested{void Test() { }}partial class Nested{void Test2() { }}
}
編譯時會對分部類型定義的屬性進行合并。 以下面的聲明為例:
C#復制
[SerializableAttribute]
partial class Moon { }[ObsoleteAttribute]
partial class Moon { }
它們等效于以下聲明:
C#復制
[SerializableAttribute]
[ObsoleteAttribute]
class Moon { }
將從所有分部類型定義中對以下內容進行合并:
- XML 注釋。 但是,如果分部成員的兩個聲明都包含注釋,則僅包括實現(xiàn)成員的注釋。
- interfaces
- 泛型類型參數(shù)屬性
- class 特性
- 成員
以下面的聲明為例:
C#復制
partial class Earth : Planet, IRotate { }
partial class Earth : IRevolve { }
它們等效于以下聲明:
C#復制
class Earth : Planet, IRotate, IRevolve { }
09.03.02 限制
處理分部類定義時需遵循下面的幾個規(guī)則:
- 要作為同一類型的各個部分的所有分部類型定義都必須使用?
partial
?進行修飾。 例如,下面的類聲明會生成錯誤:C#復制
public partial class A { } //public class A { } // Error, must also be marked partial
partial
?修飾符只能出現(xiàn)在緊靠關鍵字?class
、struct
?或?interface
?前面的位置。- 分部類型定義中允許使用嵌套的分部類型,如下面的示例中所示:
C#復制
partial class ClassWithNestedClass {partial class NestedClass { } }partial class ClassWithNestedClass {partial class NestedClass { } }
- 要成為同一類型的各個部分的所有分部類型定義都必須在同一程序集和同一模塊(.exe 或 .dll 文件)中進行定義。 分部定義不能跨越多個模塊。
- 類名和泛型類型參數(shù)在所有的分部類型定義中都必須匹配。 泛型類型可以是分部的。 每個分部聲明都必須以相同的順序使用相同的參數(shù)名。
- 下面用于分部類型定義中的關鍵字是可選的,但是如果某關鍵字出現(xiàn)在一個分部類型定義中,則必須在相同類型的其他分部定義中指定相同的關鍵字:
- 公共
- private
- 受保護
- internal
- abstract
- sealed
- 基類
- new?修飾符(嵌套部分)
- 泛型約束
有關詳細信息,請參閱類型參數(shù)的約束。
下面的示例在一個分部類定義中聲明?Coords
?類的字段和構造函數(shù),在另一個分部類定義中聲明成員?PrintCoords
。
C#復制
public partial class Coords
{private int x;private int y;public Coords(int x, int y){this.x = x;this.y = y;}
}public partial class Coords
{public void PrintCoords(){Console.WriteLine("Coords: {0},{1}", x, y);}
}class TestCoords
{static void Main(){Coords myCoords = new Coords(10, 15);myCoords.PrintCoords();// Keep the console window open in debug mode.Console.WriteLine("Press any key to exit.");Console.ReadKey();}
}
// Output: Coords: 10,15
從下面的示例可以看出,你也可以開發(fā)分部結構和接口。
C#復制
partial interface ITest
{void Interface_Test();
}partial interface ITest
{void Interface_Test2();
}partial struct S1
{void Struct_Test() { }
}partial struct S1
{void Struct_Test2() { }
}
09.03.03 分部成員
分部類或結構可以包含分部成員。 類的一個部分包含成員的簽名。 可以在同一部分或另一部分中定義實現(xiàn)。
當簽名遵循以下規(guī)則時,分部方法不需要實現(xiàn):
- 聲明未包含任何訪問修飾符。 默認情況下,該方法具有?private?訪問權限。
- 返回類型為?void。
- 沒有任何參數(shù)具有?out?修飾符。
- 方法聲明不能包括以下任何修飾符:
- virtual
- override
- sealed
- new
- extern
當未提供實現(xiàn)時,在編譯時會移除該方法以及對該方法的所有調用。
任何不符合所有這些限制的方法(包括屬性和索引器)都必須提供實現(xiàn)。 此實現(xiàn)可以由源生成器提供。?不能使用自動實現(xiàn)的屬性實現(xiàn)部分屬性?。 編譯器無法區(qū)分自動實現(xiàn)的屬性和分部屬性的聲明聲明。
分部方法允許類的某個部分的實現(xiàn)者聲明成員。 類的另一部分的實現(xiàn)者可以定義該成員。 在以下兩個情形中,此分離很有用:生成樣板代碼的模板和源生成器。
- 模板代碼:模板保留方法名稱和簽名,以使生成的代碼可以調用方法。 這些方法遵循允許開發(fā)人員決定是否實現(xiàn)方法的限制。 如果未實現(xiàn)該方法,編譯器會移除方法簽名以及對該方法的所有調用。 調用該方法(包括調用中的任何參數(shù)計算結果)在運行時沒有任何影響。 因此,分部類中的任何代碼都可以隨意地使用分部方法,即使未提供實現(xiàn)也是如此。 未實現(xiàn)該方法時調用該方法不會導致編譯時錯誤或運行時錯誤。
- 源生成器:源生成器提供成員的實現(xiàn)。 開發(fā)人員可以添加成員聲明(通常由源生成器讀取屬性)。 開發(fā)人員可以編寫調用這些成員的代碼。 源生成器在編譯過程中運行并提供實現(xiàn)。 在這種情況下,不會遵循不經(jīng)常實現(xiàn)的分部成員的限制。
C#復制
// Definition in file1.cs
partial void OnNameChanged();// Implementation in file2.cs
partial void OnNameChanged()
{// method body
}
- 分部成員聲明必須以上下文關鍵字?partial?開頭。
- 分部類型的兩個部分中的分部成員簽名必須匹配。
- 分部成員可以有?static?和?unsafe?修飾符。
- 分部成員可能是泛型成員。 約束在定義和實現(xiàn)方法聲明時必須相同。 參數(shù)和類型參數(shù)名稱在定義和實現(xiàn)方法聲明時不必相同。
- 你可以為已定義并實現(xiàn)的分部方法生成委托,但不能為沒有實現(xiàn)的分部方法生成委托。
09.04?如何聲明、實例化和使用委托
09.04.01?聲明委托
可以使用以下任一方法聲明委托:
- 使用匹配簽名聲明委托類型并聲明方法:
C#復制
// Declare a delegate.
delegate void NotifyCallback(string str);// Declare a method with the same signature as the delegate.
static void Notify(string name)
{Console.WriteLine($"Notification received for: {name}");
}
C#復制
// Create an instance of the delegate.
NotifyCallback del1 = new NotifyCallback(Notify);
- 將方法組分配給委托類型:
C#復制
// C# 2.0 provides a simpler way to declare an instance of NotifyCallback.
NotifyCallback del2 = Notify;
- 聲明匿名方法:
C#復制
// Instantiate NotifyCallback by using an anonymous method.
NotifyCallback del3 = delegate(string name){ Console.WriteLine($"Notification received for: {name}"); };
- 使用 lambda 表達式:
C#復制
// Instantiate NotifyCallback by using a lambda expression.
NotifyCallback del4 = name => { Console.WriteLine($"Notification received for: {name}"); };
有關詳細信息,請參閱?Lambda 表達式。
下面的示例演示如何聲明、實例化和使用委托。?BookDB
?類封裝用來維護書籍數(shù)據(jù)庫的書店數(shù)據(jù)庫。 它公開一個方法?ProcessPaperbackBooks
,用于在數(shù)據(jù)庫中查找所有平裝書并為每本書調用委托。 使用的?delegate
?類型名為?ProcessBookCallback
。?Test
?類使用此類打印平裝書的書名和平均價格。
使用委托提升書店數(shù)據(jù)庫和客戶端代碼之間的良好分隔功能。 客戶端代碼程序不知道如何存儲書籍或書店代碼如何查找平裝書。 書店代碼不知道它在找到平裝書之后對其執(zhí)行什么處理。
C#復制
// A set of classes for handling a bookstore:
namespace Bookstore
{using System.Collections;// Describes a book in the book list:public struct Book{public string Title; // Title of the book.public string Author; // Author of the book.public decimal Price; // Price of the book.public bool Paperback; // Is it paperback?public Book(string title, string author, decimal price, bool paperBack){Title = title;Author = author;Price = price;Paperback = paperBack;}}// Declare a delegate type for processing a book:public delegate void ProcessBookCallback(Book book);// Maintains a book database.public class BookDB{// List of all books in the database:ArrayList list = new ArrayList();// Add a book to the database:public void AddBook(string title, string author, decimal price, bool paperBack){list.Add(new Book(title, author, price, paperBack));}// Call a passed-in delegate on each paperback book to process it:public void ProcessPaperbackBooks(ProcessBookCallback processBook){foreach (Book b in list){if (b.Paperback)// Calling the delegate:processBook(b);}}}
}// Using the Bookstore classes:
namespace BookTestClient
{using Bookstore;// Class to total and average prices of books:class PriceTotaller{int countBooks = 0;decimal priceBooks = 0.0m;internal void AddBookToTotal(Book book){countBooks += 1;priceBooks += book.Price;}internal decimal AveragePrice(){return priceBooks / countBooks;}}// Class to test the book database:class Test{// Print the title of the book.static void PrintTitle(Book b){Console.WriteLine($" {b.Title}");}// Execution starts here.static void Main(){BookDB bookDB = new BookDB();// Initialize the database with some books:AddBooks(bookDB);// Print all the titles of paperbacks:Console.WriteLine("Paperback Book Titles:");// Create a new delegate object associated with the static// method Test.PrintTitle:bookDB.ProcessPaperbackBooks(PrintTitle);// Get the average price of a paperback by using// a PriceTotaller object:PriceTotaller totaller = new PriceTotaller();// Create a new delegate object associated with the nonstatic// method AddBookToTotal on the object totaller:bookDB.ProcessPaperbackBooks(totaller.AddBookToTotal);Console.WriteLine("Average Paperback Book Price: ${0:#.##}",totaller.AveragePrice());}// Initialize the book database with some test books:static void AddBooks(BookDB bookDB){bookDB.AddBook("The C Programming Language", "Brian W. Kernighan and Dennis M. Ritchie", 19.95m, true);bookDB.AddBook("The Unicode Standard 2.0", "The Unicode Consortium", 39.95m, true);bookDB.AddBook("The MS-DOS Encyclopedia", "Ray Duncan", 129.95m, false);bookDB.AddBook("Dogbert's Clues for the Clueless", "Scott Adams", 12.00m, true);}}
}
/* Output:
Paperback Book Titles:The C Programming LanguageThe Unicode Standard 2.0Dogbert's Clues for the Clueless
Average Paperback Book Price: $23.97
*/
09.04.02 可靠編程
-
聲明委托。
以下語句聲明新的委托類型。
C#復制
public delegate void ProcessBookCallback(Book book);
每個委托類型描述自變量的數(shù)量和類型,以及它可以封裝的方法的返回值類型。 每當需要一組新的自變量類型或返回值類型,則必須聲明一個新的委托類型。
-
實例化委托。
聲明委托類型后,則必須創(chuàng)建委托對象并將其與特定的方法相關聯(lián)。 在上例中,你通過將?
PrintTitle
?方法傳遞給?ProcessPaperbackBooks
?方法執(zhí)行此操作,如下面的示例所示:C#復制
bookDB.ProcessPaperbackBooks(PrintTitle);
這將創(chuàng)建一個新的與靜態(tài)方法?
Test.PrintTitle
?關聯(lián)的委托對象。 同樣,如下面的示例所示,傳遞對象?totaller
?中的非靜態(tài)方法?AddBookToTotal
:C#復制
bookDB.ProcessPaperbackBooks(totaller.AddBookToTotal);
在這兩種情況下,都將新的委托對象傳遞給?
ProcessPaperbackBooks
?方法。創(chuàng)建委托后,它與之關聯(lián)的方法就永遠不會更改;委托對象是不可變的。
-
調用委托。
創(chuàng)建委托對象后,通常會將委托對象傳遞給將調用該委托的其他代碼。 委托對象是通過使用委托對象的名稱調用的,后跟用圓括號括起來的將傳遞給委托的自變量。 下面是一個委托調用示例:
C#復制
processBook(b);
委托可以同步調用(如在本例中)或通過使用?
BeginInvoke
?和?EndInvoke
?方法異步調用。
09.05?索引器
索引器允許類或結構的實例就像數(shù)組一樣進行索引。 無需顯式指定類型或實例成員,即可設置或檢索索引值。 索引器類似于屬性,不同之處在于它們的訪問器需要使用參數(shù)。
以下示例定義了一個泛型類,其中包含用于賦值和檢索值的簡單?get?和?set?訪問器方法。?Program
?類創(chuàng)建了此類的一個實例,用于存儲字符串。
C#復制
using System;class SampleCollection<T>
{// Declare an array to store the data elements.private T[] arr = new T[100];// Define the indexer to allow client code to use [] notation.public T this[int i]{get { return arr[i]; }set { arr[i] = value; }}
}class Program
{static void Main(){var stringCollection = new SampleCollection<string>();stringCollection[0] = "Hello, World";Console.WriteLine(stringCollection[0]);}
}
// The example displays the following output:
// Hello, World.
?備注
有關更多示例,請參閱相關部分。
09.05.01 表達式主體定義
索引器的 get 或 set 訪問器包含一個用于返回或設置值的語句很常見。 為了支持這種情況,表達式主體成員提供了一種經(jīng)過簡化的語法。 自 C# 6 起,可以表達式主體成員的形式實現(xiàn)只讀索引器,如以下示例所示。
C#復制
using System;class SampleCollection<T>
{// Declare an array to store the data elements.private T[] arr = new T[100];int nextIndex = 0;// Define the indexer to allow client code to use [] notation.public T this[int i] => arr[i];public void Add(T value){if (nextIndex >= arr.Length)throw new IndexOutOfRangeException($"The collection can hold only {arr.Length} elements.");arr[nextIndex++] = value;}
}class Program
{static void Main(){var stringCollection = new SampleCollection<string>();stringCollection.Add("Hello, World");System.Console.WriteLine(stringCollection[0]);}
}
// The example displays the following output:
// Hello, World.
請注意,=>
?引入了表達式主體,并未使用?get
?關鍵字。
自 C# 7.0 起,get 和 set 訪問器均可作為表達式主體成員實現(xiàn)。 在這種情況下,必須使用?get
?和?set
?關鍵字。 例如:
C#復制
using System;class SampleCollection<T>
{// Declare an array to store the data elements.private T[] arr = new T[100];// Define the indexer to allow client code to use [] notation.public T this[int i]{get => arr[i];set => arr[i] = value;}
}class Program
{static void Main(){var stringCollection = new SampleCollection<string>();stringCollection[0] = "Hello, World.";Console.WriteLine(stringCollection[0]);}
}
// The example displays the following output:
// Hello, World.
09.05.02 索引器概述
-
使用索引器可以用類似于數(shù)組的方式為對象建立索引。
-
get
?取值函數(shù)返回值。?set
?取值函數(shù)分配值。 -
this?關鍵字用于定義索引器。
-
value?關鍵字用于定義由?
set
?訪問器分配的值。 -
索引器不必根據(jù)整數(shù)值進行索引;由你決定如何定義特定的查找機制。
-
索引器可被重載。
-
索引器可以有多個形參,例如當訪問二維數(shù)組時。
索引器使你可從語法上方便地創(chuàng)建類、結構或接口,以便客戶端應用程序可以像訪問數(shù)組一樣訪問它們。 編譯器會生成一個?Item
?屬性(或者如果存在?IndexerNameAttribute,也可以生成一個命名屬性)和適當?shù)脑L問器方法。 在主要目標是封裝內部集合或數(shù)組的類型中,常常要實現(xiàn)索引器。 例如,假設有一個類?TempRecord
,它表示 24 小時的周期內在 10 個不同時間點所記錄的溫度(單位為華氏度)。 此類包含一個?float[]
?類型的數(shù)組?temps
,用于存儲溫度值。 通過在此類中實現(xiàn)索引器,客戶端可采用?float temp = tempRecord[4]
?的形式(而非?float temp = tempRecord.temps[4]
)訪問?TempRecord
?實例中的溫度。 索引器表示法不但簡化了客戶端應用程序的語法;還使類及其目標更容易直觀地為其它開發(fā)者所理解。
若要在類或結構上聲明索引器,請使用?this?關鍵字,如以下示例所示:
C#復制
// Indexer declaration
public int this[int index]
{// get and set accessors
}
?重要
通過聲明索引器,可自動在對象上生成一個名為?Item
?的屬性。 無法從實例成員訪問表達式直接訪問?Item
?屬性。 此外,如果通過索引器向對象添加自己的?Item
?屬性,則將收到?CS0102 編譯器錯誤。 要避免此錯誤,請使用?IndexerNameAttribute?重命名本文后面詳述的索引器。
索引器及其參數(shù)的類型必須至少具有和索引器相同的可訪問性。 有關可訪問性級別的詳細信息,請參閱訪問修飾符。
有關如何在接口上使用索引器的詳細信息,請參閱接口索引器。
索引器的簽名由其形參的數(shù)目和類型所組成。 它不包含索引器類型或形參的名稱。 如果要在相同類中聲明多個索引器,則它們的簽名必須不同。
索引器未分類為變量;因此,索引器值不能按引用(作為?ref?或?out?參數(shù))傳遞,除非其值是引用(即按引用返回。)
若要使索引器的名稱可為其他語言所用,請使用?System.Runtime.CompilerServices.IndexerNameAttribute,如以下示例所示:
C#復制
// Indexer declaration
[System.Runtime.CompilerServices.IndexerName("TheItem")]
public int this[int index]
{// get and set accessors
}
此索引器被索引器名稱屬性重寫,因此其名稱為?TheItem
。 默認情況下,默認名稱為?Item
。
下列示例演示如何聲明專用數(shù)組字段?temps
?和索引器。 索引器可以實現(xiàn)對實例?tempRecord[i]
?的直接訪問。 若不使用索引器,則將數(shù)組聲明為公共成員,并直接訪問其成員?tempRecord.temps[i]
。
C#復制
public class TempRecord
{// Array of temperature valuesfloat[] temps =[56.2F, 56.7F, 56.5F, 56.9F, 58.8F,61.3F, 65.9F, 62.1F, 59.2F, 57.5F];// To enable client code to validate input// when accessing your indexer.public int Length => temps.Length;// Indexer declaration.// If index is out of range, the temps array will throw the exception.public float this[int index]{get => temps[index];set => temps[index] = value;}
}
請注意,當評估索引器訪問時(例如在?Console.Write
?語句中),將調用?get?訪問器。 因此,如果不存在?get
?訪問器,則會發(fā)生編譯時錯誤。
C#復制
var tempRecord = new TempRecord();// Use the indexer's set accessor
tempRecord[3] = 58.3F;
tempRecord[5] = 60.1F;// Use the indexer's get accessor
for (int i = 0; i < 10; i++)
{Console.WriteLine($"Element #{i} = {tempRecord[i]}");
}
09.05.03 使用其他值進行索引
C# 不將索引參數(shù)類型限制為整數(shù)。 例如,對索引器使用字符串可能有用。 通過搜索集合內的字符串并返回相應的值,可以實現(xiàn)此類索引器。 訪問器可被重載,因此字符串和整數(shù)版本可以共存。
下面的示例聲明了存儲星期幾的類。?get
?訪問器采用字符串(星期幾)并返回對應的整數(shù)。 例如,“Sunday”返回 0,“Monday”返回 1,依此類推。
C#復制
// Using a string as an indexer value
class DayCollection
{string[] days = ["Sun", "Mon", "Tues", "Wed", "Thurs", "Fri", "Sat"];// Indexer with only a get accessor with the expression-bodied definition:public int this[string day] => FindDayIndex(day);private int FindDayIndex(string day){for (int j = 0; j < days.Length; j++){if (days[j] == day){return j;}}throw new ArgumentOutOfRangeException(nameof(day),$"Day {day} is not supported.\nDay input must be in the form \"Sun\", \"Mon\", etc");}
}
C#復制
var week = new DayCollection();
Console.WriteLine(week["Fri"]);try
{Console.WriteLine(week["Made-up day"]);
}
catch (ArgumentOutOfRangeException e)
{Console.WriteLine($"Not supported input: {e.Message}");
}
下面的示例聲明了使用?System.DayOfWeek?存儲星期幾的類。?get
?訪問器采用?DayOfWeek
(表示星期幾的值)并返回對應的整數(shù)。 例如,DayOfWeek.Sunday
?返回 0,DayOfWeek.Monday
?返回 1,依此類推。
C#復制
using Day = System.DayOfWeek;class DayOfWeekCollection
{Day[] days =[Day.Sunday, Day.Monday, Day.Tuesday, Day.Wednesday,Day.Thursday, Day.Friday, Day.Saturday];// Indexer with only a get accessor with the expression-bodied definition:public int this[Day day] => FindDayIndex(day);private int FindDayIndex(Day day){for (int j = 0; j < days.Length; j++){if (days[j] == day){return j;}}throw new ArgumentOutOfRangeException(nameof(day),$"Day {day} is not supported.\nDay input must be a defined System.DayOfWeek value.");}
}
C#復制
var week = new DayOfWeekCollection();
Console.WriteLine(week[DayOfWeek.Friday]);try
{Console.WriteLine(week[(DayOfWeek)43]);
}
catch (ArgumentOutOfRangeException e)
{Console.WriteLine($"Not supported input: {e.Message}");
}
09.05.04 可靠編程
提高索引器的安全性和可靠性有兩種主要方法:
-
請確保結合某一類型的錯誤處理策略,以處理萬一客戶端代碼傳入無效索引值的情況。 在本文前面的第一個示例中,TempRecord 類提供了 Length 屬性,使客戶端代碼能在將輸入傳遞給索引器之前對其進行驗證。 也可將錯誤處理代碼放入索引器自身內部。 請確保為用戶記錄在索引器的訪問器中引發(fā)的任何異常。
-
在可接受的程度內,為?get?和?set?訪問器的可訪問性設置盡可能多的限制。 這一點對?
set
?訪問器尤為重要。 有關詳細信息,請參閱限制訪問器可訪問性。
09.05.05 接口中的索引器
可以在接口上聲明索引器。 接口索引器的訪問器與類索引器的訪問器有所不同,差異如下:
- 接口訪問器不使用修飾符。
- 接口訪問器通常沒有正文。
訪問器的用途是指示索引器為讀寫、只讀還是只寫。 可以為接口中定義的索引器提供實現(xiàn),但這種情況非常少。 索引器通常定義 API 來訪問數(shù)據(jù)字段,而數(shù)據(jù)字段無法在接口中定義。
下面是接口索引器訪問器的示例:
C#復制
public interface ISomeInterface
{//...// Indexer declaration:string this[int index]{get;set;}
}
索引器的簽名必須不同于同一接口中聲明的所有其他索引器的簽名。
下面的示例演示如何實現(xiàn)接口索引器。
C#復制
// Indexer on an interface:
public interface IIndexInterface
{// Indexer declaration:int this[int index]{get;set;}
}// Implementing the interface.
class IndexerClass : IIndexInterface
{private int[] arr = new int[100];public int this[int index] // indexer declaration{// The arr object will throw IndexOutOfRange exception.get => arr[index];set => arr[index] = value;}
}
C#復制
IndexerClass test = new IndexerClass();
System.Random rand = System.Random.Shared;
// Call the indexer to initialize its elements.
for (int i = 0; i < 10; i++)
{test[i] = rand.Next();
}
for (int i = 0; i < 10; i++)
{System.Console.WriteLine($"Element #{i} = {test[i]}");
}/* Sample output:Element #0 = 360877544Element #1 = 327058047Element #2 = 1913480832Element #3 = 1519039937Element #4 = 601472233Element #5 = 323352310Element #6 = 1422639981Element #7 = 1797892494Element #8 = 875761049Element #9 = 393083859
*/
在前面的示例中,可通過使用接口成員的完全限定名來使用顯示接口成員實現(xiàn)。 例如
C#復制
string IIndexInterface.this[int index]
{
}
但僅當類采用相同的索引簽名實現(xiàn)多個接口時,才需用到完全限定名稱以避免歧義。 例如,如果?Employee
?類正在實現(xiàn)接口?ICitizen
?和接口?IEmployee
,而這兩個接口具有相同的索引簽名,則需要用到顯式接口成員實現(xiàn)。 即是說以下索引器聲明:
C#復制
string IEmployee.this[int index]
{
}
在?IEmployee
?接口中實現(xiàn)索引器,而以下聲明:
C#復制
string ICitizen.this[int index]
{
}
在?ICitizen
?接口中實現(xiàn)索引器。
09.05.06 屬性和索引器之間的比較
索引器與屬性相似。 除下表所示的差別外,對屬性訪問器定義的所有規(guī)則也適用于索引器訪問器。
展開表
Property | 索引器 |
---|---|
允許以將方法視作公共數(shù)據(jù)成員的方式調用方法。 | 通過在對象自身上使用數(shù)組表示法,允許訪問對象內部集合的元素。 |
通過簡單名稱訪問。 | 通過索引訪問。 |
可為靜態(tài)成員或實例成員。 | 必須是實例成員。 |
屬性的?get?訪問器沒有任何參數(shù)。 | 索引器的?get ?訪問器具有與索引器相同的形參列表。 |
屬性的?set?訪問器包含隱式?value ?參數(shù)。 | 索引器的?set ?訪問器具有與索引器相同的形參列表,value?參數(shù)也是如此。 |
支持使用?自動實現(xiàn)的屬性縮短語法。 | 支持僅使用索引器的 expression-bodied 成員。 |
10?泛型
10.01?泛型類型參數(shù)
在泛型類型或方法定義中,類型參數(shù)是在其創(chuàng)建泛型類型的一個實例時,客戶端指定的特定類型的占位符。 泛型類(例如泛型介紹中列出的?GenericList<T>
)無法按原樣使用,因為它不是真正的類型;它更像是類型的藍圖。 若要使用?GenericList<T>
,客戶端代碼必須通過指定尖括號內的類型參數(shù)來聲明并實例化構造類型。 此特定類的類型參數(shù)可以是編譯器可識別的任何類型。 可創(chuàng)建任意數(shù)量的構造類型實例,其中每個使用不同的類型參數(shù),如下所示:
C#復制
GenericList<float> list1 = new GenericList<float>();
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();
在?GenericList<T>
?的每個實例中,類中出現(xiàn)的每個?T
?在運行時均會被替換為類型參數(shù)。 通過這種替換,我們已通過使用單個類定義創(chuàng)建了三個單獨的類型安全的有效對象。 有關 CLR 如何執(zhí)行此替換的詳細信息,請參閱運行時中的泛型。
可在有關命名約定的文章中了解泛型類型參數(shù)的命名約定。
10.02?類型參數(shù)的約束
約束告知編譯器類型參數(shù)必須具備的功能。 在沒有任何約束的情況下,類型參數(shù)可以是任何類型。 編譯器只能假定?System.Object?的成員,它是任何 .NET 類型的最終基類。 有關詳細信息,請參閱使用約束的原因。 如果客戶端代碼使用不滿足約束的類型,編譯器將發(fā)出錯誤。 通過使用?where
?上下文關鍵字指定約束。 下表列出了各種類型的約束:
展開表
約束 | 說明 |
---|---|
where T : struct | 類型參數(shù)必須是不可為 null 的值類型,其中包含?record struct ?類型。 有關可為 null 的值類型的信息,請參閱可為 null 的值類型。 由于所有值類型都具有可訪問的無參數(shù)構造函數(shù)(無論是聲明的還是隱式的),因此?struct ?約束表示?new() ?約束,并且不能與?new() ?約束結合使用。?struct ?約束也不能與?unmanaged ?約束結合使用。 |
where T : class | 類型參數(shù)必須是引用類型。 此約束還應用于任何類、接口、委托或數(shù)組類型。 在可為 null 的上下文中,T ?必須是不可為 null 的引用類型。 |
where T : class? | 類型參數(shù)必須是可為 null 或不可為 null 的引用類型。 此約束還應用于任何類、接口、委托或數(shù)組類型(包括記錄)。 |
where T : notnull | 類型參數(shù)必須是不可為 null 的類型。 參數(shù)可以是不可為 null 的引用類型,也可以是不可為 null 的值類型。 |
where T : unmanaged | 類型參數(shù)必須是不可為 null 的非托管類型。?unmanaged ?約束表示?struct ?約束,且不能與?struct ?約束或?new() ?約束結合使用。 |
where T : new() | 類型參數(shù)必須具有公共無參數(shù)構造函數(shù)。 與其他約束一起使用時,new() ?約束必須最后指定。?new() ?約束不能與?struct ?和?unmanaged ?約束結合使用。 |
where T : ?<基類名> | 類型參數(shù)必須是指定的基類或派生自指定的基類。 在可為 null 的上下文中,T ?必須是從指定基類派生的不可為 null 的引用類型。 |
where T : ?<基類名>? | 類型參數(shù)必須是指定的基類或派生自指定的基類。 在可為 null 的上下文中,T ?可以是從指定基類派生的可為 null 或不可為 null 的類型。 |
where T : ?<接口名> | 類型參數(shù)必須是指定的接口或實現(xiàn)指定的接口。 可指定多個接口約束。 約束接口也可以是泛型。 在的可為 null 的上下文中,T ?必須是實現(xiàn)指定接口的不可為 null 的類型。 |
where T : ?<接口名>? | 類型參數(shù)必須是指定的接口或實現(xiàn)指定的接口。 可指定多個接口約束。 約束接口也可以是泛型。 在可為 null 的上下文中,T ?可以是可為 null 的引用類型、不可為 null 的引用類型或值類型。?T ?不能是可為 null 的值類型。 |
where T : U | 為?T ?提供的類型參數(shù)必須是為?U ?提供的參數(shù)或派生自為?U ?提供的參數(shù)。 在可為 null 的上下文中,如果?U ?是不可為 null 的引用類型,T ?必須是不可為 null 的引用類型。 如果?U ?是可為 null 的引用類型,則?T ?可以是可為 null 的引用類型,也可以是不可為 null 的引用類型。 |
where T : default | 重寫方法或提供顯式接口實現(xiàn)時,如果需要指定不受約束的類型參數(shù),此約束可解決歧義。?default ?約束表示基方法,但不包含?class ?或?struct ?約束。 有關詳細信息,請參閱default約束規(guī)范建議。 |
where T : allows ref struct | 此反約束聲明?T ?的類型參數(shù)可以是?ref struct ?類型。 該泛型類型或方法必須遵循?T ?的任何實例的引用安全規(guī)則,因為它可能是?ref struct 。 |
某些約束是互斥的,而某些約束必須按指定順序排列:
- 最多可應用?
struct
、class
、class?
、notnull
?和?unmanaged
?約束中的一個。 如果提供這些約束中的任何一個,則它必須是為該類型參數(shù)指定的第一個約束。 - 基類約束(
where T : Base
?或?where T : Base?
)不能與?struct
、class
、class?
、notnull
?或?unmanaged
?約束中的任何一個結合使用。 - 無論哪種形式,都最多只能應用一個基類約束。 如果想要支持可為 null 的基類型,請使用?
Base?
。 - 不能將接口不可為 null 和可為 null 的形式命名為約束。
new()
?約束不能與?struct
?或?unmanaged
?約束結合使用。 如果指定?new()
?約束,則它必須是該類型參數(shù)的最后一個約束。 反約束(如果適用)可以遵循?new()
?約束。default
?約束只能應用于替代或顯式接口實現(xiàn)。 它不能與?struct
?或?class
?約束結合使用。allows ref struct
?反約束不能與?class
?或?class?
?約束結合使用。allows ref struct
?反約束必須遵循該類型參數(shù)的所有約束。
10.02.01 使用約束的原因
約束指定類型參數(shù)的功能和預期。 聲明這些約束意味著你可以使用約束類型的操作和方法調用。 如果泛型類或方法對泛型成員使用除簡單賦值之外的任何操作,包括調用?System.Object?不支持的任何方法,則對類型參數(shù)應用約束。 例如,基類約束告訴編譯器,只有此類型的對象或派生自此類型的對象可替換該類型參數(shù)。 編譯器有了此保證后,就能夠允許在泛型類中調用該類型的方法。 以下代碼示例演示可通過應用基類約束添加到(泛型介紹中的)GenericList<T>
?類的功能。
C#復制
public class Employee
{public Employee(string name, int id) => (Name, ID) = (name, id);public string Name { get; set; }public int ID { get; set; }
}public class GenericList<T> where T : Employee
{private class Node{public Node(T t) => (Next, Data) = (null, t);public Node? Next { get; set; }public T Data { get; set; }}private Node? head;public void AddHead(T t){Node n = new Node(t) { Next = head };head = n;}public IEnumerator<T> GetEnumerator(){Node? current = head;while (current != null){yield return current.Data;current = current.Next;}}public T? FindFirstOccurrence(string s){Node? current = head;T? t = null;while (current != null){//The constraint enables access to the Name property.if (current.Data.Name == s){t = current.Data;break;}else{current = current.Next;}}return t;}
}
約束使泛型類能夠使用?Employee.Name
?屬性。 約束指定類型?T
?的所有項都保證是?Employee
?對象或從?Employee
?繼承的對象。
可以對同一類型參數(shù)應用多個約束,并且約束自身可以是泛型類型,如下所示:
C#復制
class EmployeeList<T> where T : notnull, Employee, IComparable<T>, new()
{// ...public void AddDefault(){T t = new T();// ...}
}
在應用?where T : class
?約束時,請避免對類型參數(shù)使用?==
?和?!=
?運算符,因為這些運算符僅測試引用標識而不測試值相等性。 即使在用作參數(shù)的類型中重載這些運算符也會發(fā)生此行為。 下面的代碼說明了這一點;即使?String?類重載?==
?運算符,輸出也為 false。
C#復制
public static void OpEqualsTest<T>(T s, T t) where T : class
{System.Console.WriteLine(s == t);
}private static void TestStringEquality()
{string s1 = "target";System.Text.StringBuilder sb = new System.Text.StringBuilder("target");string s2 = sb.ToString();OpEqualsTest<string>(s1, s2);
}
編譯器只知道?T
?在編譯時是引用類型,并且必須使用對所有引用類型都有效的默認運算符。 如果必須測試值相等性,請應用?where T : IEquatable<T>
?或?where T : IComparable<T>
?約束,并在用于構造泛型類的任何類中實現(xiàn)該接口。
10.02.02 約束多個參數(shù)
可以對多個參數(shù)應用多個約束,對一個參數(shù)應用多個約束,如下例所示:
C#復制
class Base { }
class Test<T, U>where U : structwhere T : Base, new()
{ }
10.02.03 未綁定的類型參數(shù)
沒有約束的類型參數(shù)(如公共類?SampleClass<T>{}
?中的 T)稱為未綁定的類型參數(shù)。 未綁定的類型參數(shù)具有以下規(guī)則:
- 不能使用?
!=
?和?==
?運算符,因為無法保證具體的類型參數(shù)能支持這些運算符。 - 可以在它們與?
System.Object
?之間來回轉換,或將它們顯式轉換為任何接口類型。 - 可以將它們與?null?進行比較。 將未綁定的參數(shù)與?
null
?進行比較時,如果類型參數(shù)為值類型,則該比較始終返回 false。
10.02.04 類型參數(shù)作為約束
在具有自己類型參數(shù)的成員函數(shù)必須將該參數(shù)約束為包含類型的類型參數(shù)時,將泛型類型參數(shù)用作約束非常有用,如下例所示:
C#復制
public class List<T>
{public void Add<U>(List<U> items) where U : T {/*...*/}
}
在上述示例中,T
?在?Add
?方法的上下文中是一個類型約束,而在?List
?類的上下文中是一個未綁定的類型參數(shù)。
類型參數(shù)還可在泛型類定義中用作約束。 必須在尖括號中聲明該類型參數(shù)以及任何其他類型參數(shù):
C#復制
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
類型參數(shù)作為泛型類的約束的作用非常有限,因為編譯器除了假設類型參數(shù)派生自?System.Object
?以外,不會做其他任何假設。 如果要在兩個類型參數(shù)之間強制繼承關系,可以將類型參數(shù)用作泛型類的約束。
10.02.05?notnull
?約束
可以使用?notnull
?約束指定類型參數(shù)必須是不可為 null 的值類型或不可為 null 的引用類型。 與大多數(shù)其他約束不同,如果類型參數(shù)違反?notnull
?約束,編譯器會生成警告而不是錯誤。
notnull
?約束僅在可為 null 上下文中使用時才有效。 如果在過時的可為 null 上下文中添加?notnull
?約束,編譯器不會針對違反約束的情況生成任何警告或錯誤。
10.02.06?class
?約束
可為 null 的上下文中的?class
?約束指定類型參數(shù)必須是不可為 null 的引用類型。 在可為 null 上下文中,當類型參數(shù)是可為 null 的引用類型時,編譯器會生成警告。
10.02.07?default
?約束
添加可為空引用類型會使泛型類型或方法中的?T?
?使用復雜化。?T?
?可以與?struct
?或?class
?約束一起使用,但必須存在其中一項。 使用?class
?約束時,T?
?引用了?T
?的可為空引用類型。 可在這兩個約束均未應用時使用?T?
。 在這種情況下,對于值類型和引用類型,T?
?解讀為?T?
。 但是,如果?T
?是?Nullable<T>的實例,則?T?
?與?T
?相同。 換句話說,它不會成為?T??
。
由于現(xiàn)在可在沒有?class
?或?struct
?約束的情況下使用?T?
,因此在重寫或顯式接口實現(xiàn)中可能會出現(xiàn)歧義。 在這兩種情況下,重寫不包含約束,但從基類繼承。 當基類不應用?class
?或?struct
?約束時,派生類需要通過某種方式在不使用任一種約束的情況下指定應用于基方法的重寫。 派生方法應用?default
?約束。?default
?約束不闡明?class
?和?struct
?約束。
10.02.08 非托管約束
可使用?unmanaged
?約束來指定類型參數(shù)必須是不可為 null 的非托管類型。 通過?unmanaged
?約束,用戶能編寫可重用例程,從而使用可作為內存塊操作的類型,如以下示例所示:
C#復制
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{var size = sizeof(T);var result = new Byte[size];Byte* p = (byte*)&argument;for (var i = 0; i < size; i++)result[i] = *p++;return result;
}
以上方法必須在?unsafe
?上下文中編譯,因為它并不是在已知的內置類型上使用?sizeof
?運算符。 如果沒有?unmanaged
?約束,則?sizeof
?運算符不可用。
unmanaged
?約束表示?struct
?約束,且不能與其結合使用。 因為?struct
?約束表示?new()
?約束,且?unmanaged
?約束也不能與?new()
?約束結合使用。
10.02.09 委托約束
可以使用?System.Delegate?或?System.MulticastDelegate?作為基類約束。 CLR 始終允許此約束,但 C# 語言不允許。 使用?System.Delegate
?約束,用戶能夠以類型安全的方式編寫使用委托的代碼。 以下代碼定義了合并兩個同類型委托的擴展方法:
C#復制
public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)where TDelegate : System.Delegate=> Delegate.Combine(source, target) as TDelegate;
可使用上述方法來合并相同類型的委托:
C#復制
Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");var combined = first.TypeSafeCombine(second);
combined!();Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);
如果對最后一行取消注釋,它將不會編譯。?first
?和?test
?均為委托類型,但它們是不同的委托類型。
10.02.10 枚舉約束
還可指定?System.Enum?類型作為基類約束。 CLR 始終允許此約束,但 C# 語言不允許。 使用?System.Enum
?的泛型提供類型安全的編程,緩存使用?System.Enum
?中靜態(tài)方法的結果。 以下示例查找枚舉類型的所有有效的值,然后生成將這些值映射到其字符串表示形式的字典。
C#復制
public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{var result = new Dictionary<int, string>();var values = Enum.GetValues(typeof(T));foreach (int item in values)result.Add(item, Enum.GetName(typeof(T), item)!);return result;
}
Enum.GetValues
?和?Enum.GetName
?使用反射,這會對性能產(chǎn)生影響。 可調用?EnumNamedValues
?來生成可緩存和重用的集合,而不是重復執(zhí)行需要反射才能實施的調用。
如以下示例所示,可使用它來創(chuàng)建枚舉并生成其值和名稱的字典:
C#復制
enum Rainbow
{Red,Orange,Yellow,Green,Blue,Indigo,Violet
}
C#復制
var map = EnumNamedValues<Rainbow>();foreach (var pair in map)Console.WriteLine($"{pair.Key}:\t{pair.Value}");
10.02.11 類型參數(shù)實現(xiàn)聲明的接口
某些場景要求為類型參數(shù)提供的參數(shù)實現(xiàn)該接口。 例如:
C#復制
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{static abstract T operator +(T left, T right);static abstract T operator -(T left, T right);
}
此模式使 C# 編譯器能夠確定重載運算符或任何?static virtual
?或?static abstract
?方法的包含類型。 它提供的語法使得可以在包含類型上定義加法和減法運算符。 如果沒有此約束,需要將參數(shù)和自變量聲明為接口,而不是類型參數(shù):
C#復制
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{static abstract IAdditionSubtraction<T> operator +(IAdditionSubtraction<T> left,IAdditionSubtraction<T> right);static abstract IAdditionSubtraction<T> operator -(IAdditionSubtraction<T> left,IAdditionSubtraction<T> right);
}
上述語法要求實現(xiàn)者對這些方法使用顯式接口實現(xiàn)。 提供額外的約束使接口能夠根據(jù)類型參數(shù)來定義運算符。 實現(xiàn)接口的類型可以隱式實現(xiàn)接口方法。
10.02.12 Allows ref struct
allows ref struct
?反約束聲明相應的類型參數(shù)可以是?ref struct?類型。 該類型參數(shù)的實例必須遵循以下規(guī)則:
- 它不能被裝箱。
- 它參與引用安全規(guī)則。
- 不能在不允許?
ref struct
?類型的地方使用實例,例如?static
?字段。 - 實例可以使用?
scoped
?修飾符進行標記。
不會繼承?allows ref struct
?子句。 在以下代碼中:
C#復制
class SomeClass<T, S>where T : allows ref structwhere S : T
{// etc
}
S
?的參數(shù)不能是?ref struct
,因為?S
?沒有?allows ref struct
?子句。
具有?allows ref struct
?子句的類型參數(shù)不能用作類型參數(shù),除非相應的類型參數(shù)也具有?allows ref struct
?子句。 下面的示例說明了此規(guī)則:
C#復制
public class Allow<T> where T : allows ref struct
{}public class Disallow<T>
{
}public class Example<T> where T : allows ref struct
{private Allow<T> fieldOne; // Allowed. T is allowed to be a ref structprivate Disallow<T> fieldTwo; // Error. T is not allowed to be a ref struct
}
前面的示例表明,對于一個可能是?ref struct
?類型的類型參數(shù),不能將其替換為不能是?ref struct
?類型的類型參數(shù)。
10.03?泛型類
泛型類封裝不特定于特定數(shù)據(jù)類型的操作。 泛型類最常見用法是用于鏈接列表、哈希表、堆棧、隊列和樹等集合。 無論存儲數(shù)據(jù)的類型如何,添加項和從集合刪除項等操作的執(zhí)行方式基本相同。
對于大多數(shù)需要集合類的方案,推薦做法是使用 .NET 類庫中提供的集合類。 有關使用這些類的詳細信息,請參閱?.NET 中的泛型集合。
通常,創(chuàng)建泛型類是從現(xiàn)有具體類開始,然后每次逐個將類型更改為類型參數(shù),直到泛化和可用性達到最佳平衡。 創(chuàng)建自己的泛型類時,需要考慮以下重要注意事項:
-
要將哪些類型泛化為類型參數(shù)。
通常,可參數(shù)化的類型越多,代碼就越靈活、其可重用性就越高。 但過度泛化會造成其他開發(fā)人員難以閱讀或理解代碼。
-
要將何種約束(如有)應用到類型參數(shù)(請參閱類型參數(shù)的約束)。
其中一個有用的規(guī)則是,應用最大程度的約束,同時仍可處理必須處理的類型。 例如,如果知道泛型類僅用于引用類型,則請應用類約束。 這可防止將類意外用于值類型,并使你可在?
T
?上使用?as
?運算符和檢查 null 值。 -
是否將泛型行為分解為基類和子類。
因為泛型類可用作基類,所以非泛型類的相同設計注意事項在此也適用。 請參閱本主題后文有關從泛型基類繼承的規(guī)則。
-
實現(xiàn)一個泛型接口還是多個泛型接口。
例如,如果要設計用于在基于泛型的集合中創(chuàng)建項的類,則可能必須實現(xiàn)一個接口,例如?IComparable<T>,其中?
T
?為類的類型。
有關簡單泛型類的示例,請參閱泛型介紹。
類型參數(shù)和約束的規(guī)則對于泛型類行為具有多種含義,尤其是在繼承性和成員可訪問性方面。 應當了解一些術語,然后再繼續(xù)。 對于泛型類?Node<T>,
,客戶端代碼可通過指定類型參數(shù)來引用類,創(chuàng)建封閉式構造類型 (Node<int>
)?;蛘?#xff0c;可以不指定類型參數(shù)(例如指定泛型基類時),創(chuàng)建開放式構造類型 (Node<T>
)。 泛型類可繼承自具體的封閉式構造或開放式構造基類:
C#復制
class BaseNode { }
class BaseNodeGeneric<T> { }// concrete type
class NodeConcrete<T> : BaseNode { }//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }//open constructed type
class NodeOpen<T> : BaseNodeGeneric<T> { }
非泛型類(即,具體類)可繼承自封閉式構造基類,但不可繼承自開放式構造類或類型參數(shù),因為運行時客戶端代碼無法提供實例化基類所需的類型參數(shù)。
C#復制
//No error
class Node1 : BaseNodeGeneric<int> { }//Generates an error
//class Node2 : BaseNodeGeneric<T> {}//Generates an error
//class Node3 : T {}
繼承自開放式構造類型的泛型類必須對非此繼承類共享的任何基類類型參數(shù)提供類型參數(shù),如下方代碼所示:
C#復制
class BaseNodeMultiple<T, U> { }//No error
class Node4<T> : BaseNodeMultiple<T, int> { }//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {}
繼承自開放式構造類型的泛型類必須指定作為基類型上約束超集或表示這些約束的約束:
C#復制
class NodeItem<T> where T : System.IComparable<T>, new() { }
class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }
泛型類型可使用多個類型參數(shù)和約束,如下所示:
C#復制
class SuperKeyType<K, V, U>where U : System.IComparable<U>where V : new()
{ }
開放式構造和封閉式構造類型可用作方法參數(shù):
C#復制
void Swap<T>(List<T> list1, List<T> list2)
{//code to swap items
}void Swap(List<int> list1, List<int> list2)
{//code to swap items
}
如果一個泛型類實現(xiàn)一個接口,則該類的所有實例均可強制轉換為該接口。
泛型類是不變量。 換而言之,如果一個輸入?yún)?shù)指定?List<BaseClass>
,且你嘗試提供?List<DerivedClass>
,則會出現(xiàn)編譯時錯誤。
10.04?泛型接口
為泛型集合類或表示集合中的項的泛型類定義接口通常很有用處。 為避免對值類型執(zhí)行裝箱和取消裝箱操作,最好對泛型類使用泛型接口,例如?IComparable<T>。 .NET 類庫定義多個泛型接口,以便用于?System.Collections.Generic?命名空間中的集合類。 有關這些接口的詳細信息,請參閱泛型接口。
接口被指定為類型參數(shù)上的約束時,僅可使用實現(xiàn)接口的類型。 如下代碼示例演示一個派生自?GenericList<T>
?類的?SortedList<T>
?類。 有關詳細信息,請參閱泛型介紹。?SortedList<T>
?添加約束?where T : IComparable<T>
。 此約束可使?SortedList<T>
?中的?BubbleSort
?方法在列表元素上使用泛型?CompareTo?方法。 在此示例中,列表元素是一個實現(xiàn)?IComparable<Person>
?的簡單類?Person
。
C#復制
//Type parameter T in angle brackets.
public class GenericList<T> : System.Collections.Generic.IEnumerable<T>
{protected Node head;protected Node current = null;// Nested class is also generic on Tprotected class Node{public Node next;private T data; //T as private member datatypepublic Node(T t) //T used in non-generic constructor{next = null;data = t;}public Node Next{get { return next; }set { next = value; }}public T Data //T as return type of property{get { return data; }set { data = value; }}}public GenericList() //constructor{head = null;}public void AddHead(T t) //T as method parameter type{Node n = new Node(t);n.Next = head;head = n;}// Implementation of the iteratorpublic System.Collections.Generic.IEnumerator<T> GetEnumerator(){Node current = head;while (current != null){yield return current.Data;current = current.Next;}}// IEnumerable<T> inherits from IEnumerable, therefore this class// must implement both the generic and non-generic versions of// GetEnumerator. In most cases, the non-generic method can// simply call the generic method.System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator(){return GetEnumerator();}
}public class SortedList<T> : GenericList<T> where T : System.IComparable<T>
{// A simple, unoptimized sort algorithm that// orders list elements from lowest to highest:public void BubbleSort(){if (null == head || null == head.Next){return;}bool swapped;do{Node previous = null;Node current = head;swapped = false;while (current.next != null){// Because we need to call this method, the SortedList// class is constrained on IComparable<T>if (current.Data.CompareTo(current.next.Data) > 0){Node tmp = current.next;current.next = current.next.next;tmp.next = current;if (previous == null){head = tmp;}else{previous.next = tmp;}previous = tmp;swapped = true;}else{previous = current;current = current.next;}}} while (swapped);}
}// A simple class that implements IComparable<T> using itself as the
// type argument. This is a common design pattern in objects that
// are stored in generic lists.
public class Person : System.IComparable<Person>
{string name;int age;public Person(string s, int i){name = s;age = i;}// This will cause list elements to be sorted on age values.public int CompareTo(Person p){return age - p.age;}public override string ToString(){return name + ":" + age;}// Must implement Equals.public bool Equals(Person p){return (this.age == p.age);}
}public class Program
{public static void Main(){//Declare and instantiate a new generic SortedList class.//Person is the type argument.SortedList<Person> list = new SortedList<Person>();//Create name and age values to initialize Person objects.string[] names =["Franscoise","Bill","Li","Sandra","Gunnar","Alok","Hiroyuki","Maria","Alessandro","Raul"];int[] ages = [45, 19, 28, 23, 18, 9, 108, 72, 30, 35];//Populate the list.for (int x = 0; x < 10; x++){list.AddHead(new Person(names[x], ages[x]));}//Print out unsorted list.foreach (Person p in list){System.Console.WriteLine(p.ToString());}System.Console.WriteLine("Done with unsorted list");//Sort the list.list.BubbleSort();//Print out sorted list.foreach (Person p in list){System.Console.WriteLine(p.ToString());}System.Console.WriteLine("Done with sorted list");}
}
可將多個接口指定為單個類型上的約束,如下所示:
C#復制
class Stack<T> where T : System.IComparable<T>, IEnumerable<T>
{
}
一個接口可定義多個類型參數(shù),如下所示:
C#復制
interface IDictionary<K, V>
{
}
適用于類的繼承規(guī)則也適用于接口:
C#復制
interface IMonth<T> { }interface IJanuary : IMonth<int> { } //No error
interface IFebruary<T> : IMonth<int> { } //No error
interface IMarch<T> : IMonth<T> { } //No error//interface IApril<T> : IMonth<T, U> {} //Error
如果泛型接口是協(xié)變的(即,僅使用自身的類型參數(shù)作為返回值),那么這些接口可繼承自非泛型接口。 在 .NET 類庫中,IEnumerable<T>?繼承自?IEnumerable,因為?IEnumerable<T>?在?GetEnumerator?的返回值和?Current?屬性 Getter 中僅使用?T
。
具體類可實現(xiàn)封閉式構造接口,如下所示:
C#復制
interface IBaseInterface<T> { }class SampleClass : IBaseInterface<string> { }
只要類形參列表提供接口所需的所有實參,泛型類即可實現(xiàn)泛型接口或封閉式構造接口,如下所示:
C#復制
interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }class SampleClass1<T> : IBaseInterface1<T> { } //No error
class SampleClass2<T> : IBaseInterface2<T, string> { } //No error
控制方法重載的規(guī)則對泛型類、泛型結構或泛型接口內的方法一樣。 有關詳細信息,請參閱泛型方法。
從 C# 11 開始,接口可以聲明?static abstract
?或?static virtual
?成員。 聲明任一?static abstract
?或?static virtual
?成員的接口幾乎始終是泛型接口。 編譯器必須在編譯時解析對?static virtual
?和?static abstract
?方法的調用。 接口中聲明的?static virtual
?和?static abstract
?方法沒有類似于類中聲明的?virtual
?或?abstract
?方法的運行時調度機制。 相反,編譯器使用編譯時可用的類型信息。 這些成員通常是在泛型接口中聲明的。 此外,聲明?static virtual
?或?static abstract
?方法的大多數(shù)接口都聲明了其中一個類型參數(shù)必須實現(xiàn)已聲明的接口。 然后,編譯器使用提供的類型參數(shù)來解析聲明成員的類型。
10.05?泛型方法
泛型方法是通過類型參數(shù)聲明的方法,如下所示:
C#復制
static void Swap<T>(ref T lhs, ref T rhs)
{T temp;temp = lhs;lhs = rhs;rhs = temp;
}
如下示例演示使用類型參數(shù)的?int
?調用方法的一種方式:
C#復制
public static void TestSwap()
{int a = 1;int b = 2;Swap<int>(ref a, ref b);System.Console.WriteLine(a + " " + b);
}
還可省略類型參數(shù),編譯器將推斷類型參數(shù)。 如下?Swap
?調用等效于之前的調用:
C#復制
Swap(ref a, ref b);
類型推理的相同規(guī)則適用于靜態(tài)方法和實例方法。 編譯器可基于傳入的方法參數(shù)推斷類型參數(shù);而無法僅根據(jù)約束或返回值推斷類型參數(shù)。 因此,類型推理不適用于不具有參數(shù)的方法。 類型推理發(fā)生在編譯時,之后編譯器嘗試解析重載的方法簽名。 編譯器將類型推理邏輯應用于共用同一名稱的所有泛型方法。 在重載解決方案步驟中,編譯器僅包含在其上類型推理成功的泛型方法。
在泛型類中,非泛型方法可訪問類級別類型參數(shù),如下所示:
C#復制
class SampleClass<T>
{void Swap(ref T lhs, ref T rhs) { }
}
如果定義一個具有與包含類相同的類型參數(shù)的泛型方法,則編譯器會生成警告?CS0693,因為在該方法范圍內,向內?T
?提供的參數(shù)會隱藏向外?T
?提供的參數(shù)。 如果需要使用類型參數(shù)(而不是類實例化時提供的參數(shù))調用泛型類方法所具備的靈活性,請考慮為此方法的類型參數(shù)提供另一標識符,如下方示例中?GenericList2<T>
?所示。
C#復制
class GenericList<T>
{// CS0693.void SampleMethod<T>() { }
}class GenericList2<T>
{// No warning.void SampleMethod<U>() { }
}
使用約束在方法中的類型參數(shù)上實現(xiàn)更多專用操作。 此版?Swap<T>
?現(xiàn)名為?SwapIfGreater<T>
,僅可用于實現(xiàn)?IComparable<T>?的類型參數(shù)。
C#復制
void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T>
{T temp;if (lhs.CompareTo(rhs) > 0){temp = lhs;lhs = rhs;rhs = temp;}
}
泛型方法可重載在數(shù)個泛型參數(shù)上。 例如,以下方法可全部位于同一類中:
C#復制
void DoWork() { }
void DoWork<T>() { }
void DoWork<T, U>() { }
還可使用類型參數(shù)作為方法的返回類型。 下面的代碼示例顯示一個返回?T
?類型數(shù)組的方法:
C#復制
T[] Swap<T>(T a, T b)
{return [b, a];
}
10.06?泛型和數(shù)組
下限為零的單維數(shù)組自動實現(xiàn)?IList<T>。 這可使你創(chuàng)建可使用相同代碼循環(huán)訪問數(shù)組和其他集合類型的泛型方法。 此技術的主要用處在于讀取集合中的數(shù)據(jù)。?IList<T>?接口無法用于添加元素或從數(shù)組刪除元素。 如果在此上下文中嘗試對數(shù)組調用?IList<T>?方法(例如?RemoveAt),則會引發(fā)異常。
如下代碼示例演示具有?IList<T>?輸入?yún)?shù)的單個泛型方法如何可循環(huán)訪問列表和數(shù)組(此例中為整數(shù)數(shù)組)。
C#復制
class Program
{static void Main(){int[] arr = [0, 1, 2, 3, 4];List<int> list = new List<int>();for (int x = 5; x < 10; x++){list.Add(x);}ProcessItems<int>(arr);ProcessItems<int>(list);}static void ProcessItems<T>(IList<T> coll){// IsReadOnly returns True for the array and False for the List.System.Console.WriteLine("IsReadOnly returns {0} for this collection.",coll.IsReadOnly);// The following statement causes a run-time exception for the// array, but not for the List.//coll.RemoveAt(4);foreach (T item in coll){System.Console.Write(item?.ToString() + " ");}System.Console.WriteLine();}
}
10.07?泛型委托
委托可以定義它自己的類型參數(shù)。 引用泛型委托的代碼可以指定類型參數(shù)以創(chuàng)建封閉式構造類型,就像實例化泛型類或調用泛型方法一樣,如以下示例中所示:
C#復制
public delegate void Del<T>(T item);
public static void Notify(int i) { }Del<int> m1 = new Del<int>(Notify);
C# 2.0 版具有一種稱為方法組轉換的新功能,適用于具體委托類型和泛型委托類型,使你能夠使用此簡化語法編寫上一行:
C#復制
Del<int> m2 = Notify;
在泛型類中定義的委托可以用類方法使用的相同方式來使用泛型類類型參數(shù)。
C#復制
class Stack<T>
{public delegate void StackDelegate(T[] items);
}
引用委托的代碼必須指定包含類的類型參數(shù),如下所示:
C#復制
private static void DoWork(float[] items) { }public static void TestStack()
{Stack<float> s = new Stack<float>();Stack<float>.StackDelegate d = DoWork;
}
根據(jù)典型設計模式定義事件時,泛型委托特別有用,因為發(fā)件人參數(shù)可以為強類型,無需在它和?Object?之間強制轉換。
C
?未完待續(xù)。。。