C# 是微軟開發的簡單、快捷、通用的物件導向程式設計語言。C# 語言被廣泛應用於微軟 .NET 應用程式的開發。2022年,微軟整合了原來半死不活的 Xamarin.Forms,正式釋出了 .NET MAUI 多平台開發 Framework。MAUI 在官方和社區的雙重支援下,已經能夠做到全端運行。目前已經包括了 iOS、Android、Windows、macOS、Samsung Tizen、Linux(社區)的支援等。 這篇文章不建議程式設計初學者來看,需要你有一定的 C++ 和 Java 程式設計經驗。
.NET 安裝和 IDE 選擇
儘管 .NET 是由微軟釋出的框架,肯定在 Windows 上開發要來得更好,但無奈筆者是一位忠誠的蘋果 fans,手上只有一台 MacBook Pro。但是沒有關係,我們仍然可以在這上面正常安裝 .NET 的環境,因為 .NET 是全平台通用的。
.NET 安裝器官方下載:https://dotnet.microsoft.com/en-us/download 。筆者寫這篇文章的時候版本是 8.0。
按照安裝器的步驟去安裝即可。
因為我們是要聚焦 .NET MAUI 開發,所以我們尚需安裝 MAUI 套件。打開你的終端機,首先檢查電腦上安裝的 dotnet:
然後使用 root 權力安裝 .NET MAUI 套件:
關於 IDE 的選擇,如果你使用的是 Windows,那麼我自然是推薦你使用微軟的官方工具 Visual Studio。
但是如果你和我一樣使用的是 Mac,雖然也有 Mac 版本的 Visual Studio 可以選,但是 Mac 版本的軟體將會在 2024年8月徹底失去支援,所以這時候我一定不會推薦你使用它。那麼在 Mac 下,我推薦使用 Rider,這是由大名鼎鼎的 Jetbrains 公司開發的專為 .NET 開發者打造的 IDE。喔對了,作為開發人員,我也是 Jetbrains 家族的無腦 fans 喔!
言歸正傳啦。
這篇文章主要講的是 C# 的文法基礎,因此我們暫時用不到 MAUI,我們安裝好後就把它放到一邊就好。我們打開 Rider,創建一個 Console App的Solution 即可。
C# 程式結構
C# 程式檔的副檔名為 .cs
。一個 C# 程式主要包括以下部分:
- 命名空間(Namespace)的聲明
- 一個 class
- Class 方法
- Class
- Main 方法
- 語句、表達式、註解等
有沒有覺得很熟悉?好像 C++ 和 Java 某天晚上喝醉了⋯⋯
的確,C# 借鑒了許多 C++ 和 Java 的設計理念,像我們這樣的 C 家族程式設計師會很快入門。
我們來看一段最簡單的 C# 程式:
像啊!太像了!這簡直就是 C++ 和 Java 的完美愛情結晶。
我們來看一下這段程式碼:
using System;
:使用System
命名空間。關於命名空間,C++ 開發人員應該很熟悉, Java 開發人員的話,你可以暫時將其類比於我們的“套件”(package)。之後會有詳細的介紹。class HelloWorld
:這個應該都不會陌生,定義型別。因為 C# 是物件導向的,所以和 Java 十分相似。static void Main(string[] args)
:真的,我哭了。這和 Java 有什麼區別?甚至你真的能在前面加上public
關鍵字。主函式,程式的唯一入口。Console.WriteLine()
:終端機列印語句。
C# 資料類型
收收心思。儘管 C# 和 Java 的確很像,但是畢竟是不同的兩門程式語言。所以我們還是忍耐學下去。
C# 的資料類型分為 Value 和 Reference 兩個種類。Value 類型,包括傳統的 bool
(布林)、byte
(8 位元無符號整數)、char
(16 位元 Unicode 字元)、decimal
(128 位元精確的十進位值,28-29 有效位數)、double
(64 位元雙精度浮點)、float
(32 位元單精度浮點)、int
(32 位元有符號整數)、long
(64 位元有符號整數)、sbyte
(8 位元有符號整數)、short
(16 位元有符號整數)、uint
(32 位元無符號整數)、ulong
(64 位元無符號整數)、ushort
(16 位元無符號整數)這幾種類型。都是十分常見的類型,我就不做解釋。
Reference 類型等同於 Java 中的 Reference 類型,不包含變數的實際資料,而包含變數的引用。C# 內建的 Reference 類型一共只有三種:object
、dynamic
和 string
。
object
地位等同於 Java 中的 Object
型別,是 C# 中所有型別的終極祖宗型別。其他所有型別都是 object 型別的子型別。
dynamic
動態類型變數,可以接受任何型別的資料,也可以變更為任何型別。所以別再爭論到底動態型別系統還是靜態型別系統更好了,小孩子才做選擇!
string
C# 延續了 C 家族語言中字元和字串的表示方法,即字元使用單引號 ''
、字串使用雙引號 ""
包圍。
不同的是,除了常規的一些玩法,C# 還引入了另一種字串定義方式:@""
。
使用 @""
定義的字串可以自動將字串中的跳脫字元恢復到普通字元,而不需要再次跳一下。例如:
此外,這種方式定義的字串可以任意換行,換行字元等都算字串的長度。例如:
除上述的各種資料類型外,C# 還有一個重要類型,也是 C 家族語言的靈魂,你們的指針類型!關於指針類型,具體的情況下面再說。
C# 類型轉換
有隱形轉換、強制轉換、方法轉換三種方法。
隱形轉換是指將一個較小範圍的資料類型轉換為較大範圍的資料類型時,編譯器會自動完成類型轉換,這些轉換是 C# 預設的以安全方式進行的轉換,不會導致資料遺失。例如,從小的整數類型轉換為大的整數類型,從衍生類別轉換為基底類別。
強制轉換和所有 C 家族一樣,只需要在變數值之前加上 (<type>)
即可。當然,如果無法轉換的話,編譯器會報紅。
C# 還內建了一些方法用來進行類型轉換,與 Java 類似,比如 ToString()
、ToInt32()
等等。用法和 Java 相同。
C# 判斷、迴圈
C# 的判斷語句和迴圈語句幾乎和 Java、C++ 沒有任何區別。需要注意的是 C# 也支援 foreach
迴圈,語法稍有不同:
除此之外,if-else
判斷、switch-case
判斷、三元運算判斷、while
迴圈、for
迴圈、do-while
迴圈的用法都是完全相同的。
C# 封裝
封裝是物件導向程式設計的三大核心概念(封裝,繼承,多型)之一,概念我們都已經十分熟悉了,反映到 C# 中,大約只需要瞭解一下訪問修飾字元了。
訪問修飾字元,在 C# 中包括 public
、private
、protected
、internal
、protected internal
六種。
這和 Java 中也比較類似,但是由於 C# 和 Java 在程式結構上的不同,這些字元的作用範圍不一定相同。
我們先來看相同的幾個:
public
:可以被任意外部型別訪問。private
:只有一個型別中的函式可以訪問,即便是型別的物件也不能夠訪問。如果一個變數或方法沒有使用任何修飾元,則預設使用 private。protected
:僅限於本型別和子型別可以訪問。所不同的是,Java 中除了該型別和子型別之外,還確定了同一個 package 中的其他型別可以直接存取 protected 的物件,C# 由於沒有 package 的概念,因此只有該型別和子型別可以存取。
接下來是 C# 獨有的 internal
和 protected internal
。
要完全理解這兩個概念,我們首先要理解一個在 C# 中的基本概念——組件(Assembly)。不知道大家有沒有發現,在 Rider 中,我們剛開始創建的叫一個 Solution(解決方案)而不是常見的叫 Project(專案)。
當我們在最上面的 TestConsole Solution 上面按下滑鼠右鍵,你會驚恐的發現,居然有一個 New Project 選項。
是的,在 C# 或者說 .NET 的結構中,居然有比 Project 還高一級的結構!
那麼言歸正傳,什麼叫 Assembly 呢?簡單來講,一個 Solution 下面的每一個 Project 都叫一個 Assembly。根據 Microsoft 官方的定義,Assembly 有如下的特點:
- 組件會實作為
.exe
或.dll
檔案。 針對以 .NET Framework 為目標的程式庫,您可以藉由將組件放進全域組件快取 (GAC),在應用程式之間共用組件。 您必須先為組件設定強式名稱,才能將其放進 GAC 中。 如需詳細資訊,請參閱強式名稱的組件。 - 系統只會在需要時才將組件載入到記憶體。 若系統不需要組件,則不會執行載入程序。 因此在較大型的專案中,組件可提升資源管理效率。
- 藉由使用反映,您能以程式設計方式取得組件的相關資訊。 如需詳細資訊,請參閱反映 (C#) 或 Reflection (Visual Basic) (反映 (Visual Basic))。
- 您可以使用 .NET 和 .NET Framework 上的 MetadataLoadContext 類別來載入組件並進行檢查。 MetadataLoadContext 會取代 Assembly.ReflectionOnlyLoad 方法。
好,理解完了 Assembly,我們繼續來看 internal 修飾元。internal
修飾元表示“組建內可訪問”。而 protected internal
表示允許在本型別、派生型別(不一定要在同一個Assembly)、包含該型別的組件中訪問。
C# 方法/函式
在 C# 中的函式定義方法和其他 C 家族語言完全相同。不同的是,C# 函式的引數遞送可以透過三種方式進行——值、引用、釋出。
這很類似於 C 語言中的傳值和傳址的概念。
值引數只將引數的值傳送給函式,函式中對形式引數的任何改變都不會影響實際引數。
而引用引數則不同。引用引數相當於拷貝了一份實際引數的引用,在函式中對形式引數的改變都會影響到實際引數的真實值。
釋出引數是一個絕無僅有的設計,它以巧妙的方式允許函式回傳多個值。
C# 空類型和合併運算子
類似於 Java 和 Swift 中的 Optional 類型,表示當前變數要麼是一個所定類型的值,要麼是一個 null。在 C# 中,空類型使用 ?
字元定義:
使用合併運算子 ??
來為空類型變數確定一個為空時的預設值,以防該變數為空對程式造成的壞影響。
C# 陣列與集合
陣列 Array 是包含相同類型變數的固定長度的存儲單元。關於 Array 的定義,和 Java 完全相同,不過多闡釋。
需要注意的是,陣列可以用作函式的引數。當函式的引數個數不固定時,可以使用陣列:
這樣就有了可變引數了。
C# 中的集合有這麼幾類:
型別 | 描述 | Java 對照 |
---|---|---|
ArrayList | 動態陣列,一個可以調整大小的陣列 | ArrayList |
Hashtable | 哈希表,鍵值對存儲。 | Map |
SortedList | 排序列表,是前兩種的集合,可以使用鍵訪問或使用索引訪問 | SortedMap |
Stack | 堆疊,後進先出的資料格局 | Stack |
Queue | 隊列,先進先出的資料格局 | Queue |
BitArray | 點陣列,用於存儲二進位資料的陣列 | BitArray |
這些集合類型所有的內建方法和 Java 十分相似,在此也不再贅述。
C# 結構體、枚舉
C# 中的結構體被稱為小型別。其跟型別不同的是,結構體比較簡單,也比較輕量。相應的也會有一些功能上的犧牲。比如結構體無法進行繼承,也無法被繼承,無法被標記為 abstract、virtual 和 protected。結構體也不能有零引數的構造子。
枚舉 enum 也是完全熟悉的用法,不過多闡述。
C# 型別
終於,我們抵達了物件導向的核心——型別。C# 的型別幾乎和 Java 沒有任何區別,唯一需要注意的一點叫做解構子。這個概念在 Java 中很少用到,但在 C++ 中比較常用。
解構子是一個特殊的成員函式,和構造子對應。它用於在物件被銷毀之前自動執行指令,比如關閉資料庫連線,釋放記憶體等,就可以使用解構子。
解構子在 C# 中以 ~<ClassName>() {}
被宣告。
C# 繼承
物件導向三大概念之一。其概念和 Java 的繼承沒有很大差別,但是還是有不少細節的差距:
- C# 的繼承符號使用的是
:
,而不是extends
。 - 使用
base
來使用父型別的和方法,而不是super
。 - 需要複寫的成員需要在父型別中以
virtual
標之,否則不能夠被複寫。使用override
來在子型別中複寫virtual成員。
- 父型別中使用
abstract
標示的成員必須被複寫。
介面繼承
和 Java 相同,對型別的繼承只能是單一繼承,然而我們可以透過對介面繼承來實現多繼承。
介面一樣用 interface
來定義,繼承的時候只需要 class <ClassName> : <Interface1>, <Interface2>
即可。
與 Java 相同,介面繼承也必須完全實現介面中的方法,包括介面從其他介面繼承來的方法。
C# 多型
在 Java 講多型的時候,我們會有講到這樣一個例子:每一種動物都會吃飯,都會叫,都會跑。但是小貓小狗會有不同的動作來吃、叫和跑。所以我們可以使用一個抽象出來的介面,然後不同的動物實作這個介面來實現不同形式的動作。
C# 中的多型分為靜態和動態。靜態多型特指函式多載和運算子多載;動態多型則是透過抽象型別和虛函式實現的。
靜態多型
函式多載
有 Java 基礎,則十分容易理解:
運算子多載
C# 中的運算子也可以看作是特殊的函式。因此我們也可以在型別中特別多載適用於本型別的多載運算子。
以上運算子多載函式,實現了運算子 +
的多載。
動態多型
有兩種情況:型別抽象或者型別不抽象。
當型別抽象的時候,沒有什麼特別說明的。抽象的型別,抽象的函式,一切都是自然而然的。
當型別不抽象的時候,若想要其中的某個函式被複寫,需要使用關鍵字 virtual
來將其定義為一個虛函式。
C# 命名空間
命名空間的設計目的是提供一種讓一組名稱與其他名稱分隔開的方式。在一個命名空間中聲明的類別的名稱與另一個命名空間中聲明的相同的類別的名稱不衝突。
我們舉一個電腦系統中的例子,一個資料夾(目錄)中可以包含多個資料夾,每個資料夾中不能有相同的檔案名,但不同資料夾中的檔案可以重新命名。
命名空間的宣告
使用 namespace
來宣告命名空間:
呼叫命名空間中的函式或變數,使用 .
操作子:
using
using
表示程式使用的是給定命名空間中的名稱。例如,我們在程式中使用 System 命名空間,其中定義了型別 Console。我們可以只寫:
或者也可以寫完全限定名稱:
巢狀命名空間
命名空間可以寫成巢狀,依然適用 .
操作子呼叫函式或者變數:
C# 預處理器
在 C 語言和 C++ 中,我們已經十分熟悉在程式開始之前使用 #include<>
去為程式添加一些 head 檔案,也有使用過 #define
去進行宏定義的操作。這些都叫做預處理器。
作為 C++ 的參考程式語言,C# 幾乎照搬了這一點。
C# 中的預處理器有下面幾類:
預處理器 | 描述 |
---|---|
#define | 定義為一系列成為符號的字元 |
#undef | 它用於取消定義符號 |
#if | 它用於測試符號是否為真 |
#else | 它用於建立複合條件指令,與#if 一起使用 |
#elif | 它用於創建複合條件指令 |
#endif | 指定一個條件指令的結束 |
#line | 它可以讓您修改編譯器的行數以及(可選地)輸出錯誤和警告的檔案名稱 |
#error | 它允許從程式碼的指定位置產生一個錯誤 |
#warning | 它允許從程式碼的指定位置產生一級警告 |
#region | 它可以讓您在使用 Visual Studio Code Editor 的大綱特性時,指定一個可展開或折疊的程式碼區塊 |
#endregion | 表示 #region 的結束 |
比較重要的是 #define
預處理器和條件預處理器。
#define
#define
預處理器存在的意義事實上是條件編譯。即透過這個預處理器濾掉的程式碼根本不會被編譯。
#define
在 C# 中的用法和在 C 語言中的用法不相同。在 C# 中,它的用法是:
在 C# 中的 #define
通常與條件預處理器同時使用。比如下面的例子:
條件指令
緊接著我們講條件指令。條件指令包括 #if
、#elif
、#else
和 #endif
。使用方法和程式碼幾乎相同,不再贅述。
C# 例外處理
C# 中的例外處理幾乎和 Java 中一模一樣。熟悉的 try-catch-finally
、throw
等等。
客製化例外需要繼承 System.ApplicationException
型別。
C# 檔案讀寫
使用 FileStream
來實現簡單的檔案讀寫。其用法如下:
它的引數如下:
引數 | 描述 |
---|---|
FileMode | Append :開啟一個已有的檔案,並將遊標放置在檔案的末端。如果檔案不存在,則建立檔案。Create :建立一個新的檔案。如果檔案已存在,則刪除舊檔案,然後建立新檔案。CreateNew :指定作業系統應建立一個新的檔案。 如果檔案已存在,則拋出異常。Open :開啟一個現有的檔案。 如果檔案不存在,則丟擲例外。OpenOrCreate :指定作業系統應開啟一個已有的檔案。如果檔案不存在,則用指定的名稱建立新的檔案開啟。Truncate :開啟一個現有的文件,而檔案一旦打開,就會被截斷為零位元組大小。然後我們可以向文件寫入全新的數據,但保留文件的初始建立日期。如果檔案不存在,則拋出異常。 |
FileAccess | FileAccess 枚舉的成員有:Read 、ReadWrite 和Write |
FileShare | Inheritable :允許檔案句柄可由子程序繼承。Win32 不直接支援此功能。None :謝絕共享目前檔案。檔案關閉前,打開該檔案的任何請求(由此進程或另一個進程發出的請求)都會失敗。Read :允許隨後開啟檔案讀取。 如果未指定此標誌,則在檔案關閉前,任何開啟該檔案以進行讀取的請求(由此進程或另一進程發出的請求)都會失敗。但是,即使指定了此標誌,仍可能需要附加權限才能夠存取該檔案。ReadWrite :允許隨後開啟檔案讀取或寫入。如果未指定此標誌,則在檔案關閉前,任何開啟該檔案以進行讀取或寫入的請求(由此進程或另一進程發出)都會失敗。但是,即使指定了此標誌,仍可能需要附加權限才能夠存取該檔案。Write :允許隨後開啟檔案寫入。如果未指定此標誌,則在檔案關閉前,任何開啟該檔案以進行寫入的請求(由此進程或另一進程序發出的請求)都會失敗。但是,即使指定了此標誌,仍可能需要附加權限才能夠存取該檔案。Delete :允許隨後刪除檔案。 |
例子:
C# attribution
C# 中的 attribution 幾乎類似於 Java 中的標註(annotation),可以幫助你在一定程度上左右程式的執行。
在 C# 中,一共有三個 .NET 提供的 attribution,分別是 Obsolete
、Conditional
和 AttributeUsage
。我們分別來看。
Obsolete
這個 attribution 用於標記應該過時但仍然希望保留的程式碼。在使用的過程中會丟擲一個警告或者錯誤。
message
為字串,用於描述過時的資訊。
iserror
預設為 false
,表示丟擲的是一個 warning,如果設為 true
,則表示丟擲一個 error。
Conditional
用於條件編譯,與 #define
預處理器一起使用。用法是:
例如:
AttributeUsage
用於描述一個客製化的 attribution 如何使用。用法如下:
引數 validon 用於定義目標 attribution 可以被用到哪裡,預設為 AttributeTargets.All
;引數 AllowMultiple 是一個 boolean 值,如果為 true,則目標 attribution 是多用的,預設 false
;引數 Inherited 定義是否可被繼承,預設為 false
,即不可繼承。
客製化
首先要創建一個客製化 attribution,派生自 System.Attribution
型別:
然後我們需要定義其中的客製化存儲資訊:
然後應用這個客製化特性:
接下來我們可以使用 Reflection 來檢索這些資訊。
C# 反映
反映在 Java 中也有。它允許你在程式運行的過程中修改程式中的後設資料。
書銜上文,使用反映來處理 Attribution 中的資料:
C# Property
最常見的就是 get
和 set
,我們之前講過它和 Java 的區別。直接看例子:
我們還可以抽象之,然後在繼承的時候將其實作:
C# 委託
委託宣告
上面的委託可用來引用任何一個帶有一個單一的 string 引數的方法,並傳回一個 int 類型變數。
委託創建和使用
下面的程式碼表示了一個委託從宣告、創建到使用的全過程:
應該比較好理解。
委託的合併委派
使用 +
運算子,可以把相同屬性的方法全部委派給一個委託。當呼叫委託的時候,會按照順序呼叫這些方法:
C# 事件
事件(Event) 基本上說是使用者操作,如按鍵、點擊、滑鼠移動等等,或是一些提示訊息,如係統產生的通知。 應用程式需要在事件發生時響應事件。 例如,中斷。
C# 中的事件處理是典型的“發布-訂閱”委託模型。
- 發佈器(publisher)是一個包含事件和委託定義的物件。事件和委託之間的聯繫也定義在這個物件中。發佈器(publisher)類別的物件呼叫這個事件,並通知其他的物件。
- 訂閱器(subscriber)是一個接受事件並提供事件處理程序的物件。在發佈器(publisher)類別中的委託呼叫訂閱器(subscriber)類別中的方法(事件處理)。
接下來我們逐步來創建一個完整的事件發布和訂閱。
這樣,當 Publisher 的 sendMsg
方法被呼叫後,會自動透過事件處理通知 Subscriber。