用 C# 堆砌程式

Windows Vista 起內建 .NET Framework Developer Pack,既然都被迫裝進去了,還占用不少硬碟空間,為什麼不用看看再說 😀


準備


使用內建的 C# 編譯器

在 C:\Windows\Microsoft.NET\Framework\ 和 C:\Windows\Microsoft.NET\Framework64\ 裡有各種版本的 csc.exe 可編譯 C# 程式碼。

建議將 csc.exe 所在路徑加入 PATH,以便隨時能呼叫 csc.exe 編譯 C# 原始碼。以 Windows 10 內建 .NET Framework 4.8 為例,做法是:

控制台 → 系統及安全性 → 系統 → 進階系統設定 → 環境變數 → PATH → 編輯 → 新增 → C:\Windows\Microsoft.NET\Framework64\v4.0.30319 → 確定 → 確定

Windows 10 內建的編譯器只支援到 C# 5,所以本文不會介紹 C# 6 以上的語法功能 😏


Hello, world!

用記事本寫入如下程式,並儲存檔案為 launch.cs:

notepad('launch.cs','class Launch','{',' static void Main()',' {',' System.Console.Write("Hello, world!");',' }','}')

進入命令提示字元,切換到 launch.cs 位置,然後用 csc.exe 來編譯程式,正常的話會編譯出 launch.exe 檔案,執行它會顯示 Hello, world!:

cmd('C:\\Users\\User\\Desktop>csc *.cs','','C:\\Users\\User\\Desktop>launch','Hello, world!','','C:\\Users\\User\\Desktop>_')


目錄


Microsoft Docs

A tour of the C# language:C# 導覽,快速了解 C# 語言。
C# programming guide:程式設計手冊,完整掌握 C# 語言。
C# reference:語法辭典,快速查閱想要的語法功能。
Language Integrated Query (LINQ):直接像資料庫般寫程式調閱資料。
.NET API browser:程式庫文件。(連方法名稱都機翻,有夠離譜 😱)


基本

語法簡介、資料型態、指標、陣列、字串、流程控制類別、抽象、介面匿名物件結構列舉泛型協變、逆變命名空間組件特性函式擴充方法迭代器屬性索引器運算子重載委派、函數物件、函數傳參、匿名函式、λ 運算式、多播、事件反射例外處理垃圾收集查詢語句


應用

LINQ
集合
打亂資料
Random 的建議用法
操作檔案
讀寫檔案資料
XMLXPath
SQLite
序列化(Binary、SOAP、XML、JSON)
Zip 壓縮檔案
播放音樂
調用外部程式功能
終止外部程式執行
暫停一段時間
日期和時間
主控台
WinForms 視窗程式設計
使用者介面與程式碼分離的 WPF + XAML 視窗程式設計
CGI 程式設計
用 HTTPListener 開發後端
用 HTTPListener 架網頁伺服器

WebSocket 伺服器
用 WebClient 取得網頁內容
解析網址
取得內網 IP 和主機名稱


附錄

各 Windows 內建的 .NET Framework 版本與相容性問題
升級 .NET Framework 新版編譯器
用 gsudo 解決「以系統管理員身分執行」的問題
改用 JScript 設計 .NET Framework 程式
如何執行 ASP.NET 程式
離線查閱 .NET API
Effective C#
為什麼方法和屬性要大寫開頭?
為什麼介面要用 I 開頭?

對 csc.exe 下 /win32icon:圖示檔 設定應用程式的圖示。


C# 語法簡介


與 C++ 的關係

只要 C++ 已經有現成的語法,C# 就會照抄過來。所以像 if、switch、while、for 是一樣的,用 class 設計類別和使用 : 繼承也一樣,命名空間用 namespace{} 和運算子重載用 operator 也一樣,支援 struct 和 enum,連指標的 * 和 & 還有 # 開頭的前置處理器都保留下來 😉

C# 在照抄 C++ 各語法的同時,也簡化其複雜的功能。像 private 和 public 並不是 C++ 影響整塊區域的範疇,而是像 Java 個別宣告在屬性和方法的開頭;不允許多重繼承,改用 interface;一律使用 . 符號存取成員,不用再分 -> 和 :: 的場合。

若想要的功能和語法 C++ 沒有,C# 才會自行擴充上去,例如 C++ 沒有委派,因此 C# 新增了 delegate 指令。C++ 沒有 foreach,因此 C# 也自行新增上去。

C# 2 時,會 C++ 的人基本上都看得懂 C# 程式碼。然而,C# 3 開始,有逐漸往動態型別表述語言學習的趨勢,發展到 C# 6 已經越來越不像 C++,到了 C# 9 更是完全看不懂在寫什麼(雖然能把新語法全用上的人不多),不能再說 C# 是照搬 C++ 語法進來的程式語言了~


與 Java 的異同

檔案名稱不用與類別名稱相同。

無論有多少個類別,都只會編譯有 Main() 的類別為 *.exe 執行檔,所用到的類別都會編譯在裡面,而不是每個類別都編譯出一個 *.class 檔。

沒有 package 綁資料夾的規定,只有用巢狀 namespace 在語法上建立關係。

變數和方法預設 private,但類別預設為 internal,表示同屬一整個程式底下的組件(Assemblies)都能存取,也就是同屬於一個 *.exe 或 *.dll 底下的,都能存取。換句話說,如果 library.dll 有個 internal 的類別、屬性、方法,那就無法在 launch.exe 中存取使用,只有同屬 library.dll 裡面的可以。

介面只能宣告函式,不能建立變數,而且無法指定存取權限,一律視為 public。

公開的一律大寫開頭,私有的小寫開頭。因此類別名稱開頭大寫,欄位小寫,方法大寫。

Java 鼓勵寫一堆 setXXX()、getXXX() 來改變狀態,避免使用 = 改變物件的狀態。但 C# 引進 set 和 get 語法發明了 property 的概念,可以控制使用 = 改變物件狀態時的行為。結果,查 API 時,Java 只要看有哪些 method 就好,C# 還得看有哪些 property。C# 操作一個物件有時候用 () 呼叫函式、有時用 = 設定狀態,不像 Java 一律都是用呼叫函式的。


C# 的特點

資料型態

intlongshort 都有 u 開頭的無號版!而 byte 本身就是無號的,要用 s 開頭表示有號。

dynamic 執行時期判定型別,適合動態型別程式設計。var 編譯時期決定型別,適合靜態型別程式設計,它看似自動型別判定的語法,但其實是用來宣告 AnonymousType 型別物件用的,好比用 int 宣告 Int32 型態的資料那樣。C# 畢竟是強型別的程式語言,設計程式時應該明確寫上型別,不該全面改用 var,只用在 AnonymousType 類型的物件。但 Microsoft 官方的《C# 編碼慣例》,倒是建議一看就知道資料是什麼型態時使用 var,而不是只用在 AnonymousType,且整篇文章的範例就大量使用 var 來宣告物件或保存方法的傳回值,所以看自己編碼風格去用吧!使不使用 var,編譯出來的程式完全一樣,所以不會影響性能,純粹就是可讀性的取捨問題~

bool 就是 falsetrue,無法用 int 的 0 和 1 替換。

宣告資料型態時後面加上 ? 符號,表示變數可指定為 null,等同於用 System.Nullable<T> 宣告一個結構。這種資料賦值給其他標準資料型態的變數,自然會有不相容的情況發生,因此 C# 提供 變數 = 可空類型資料 ?? 預設值 的語法解決問題。

readonly 類似於 const,但只能用在類別的成員變數,不能用在函式之類的區塊變數,而且宣告同時即使給了值,也可以在建構式修改。

指標

將類別或函式宣告為 unsafe,編譯時下 /unsafe 參數,就能在裡面使用 C 語言的指標!

stackalloc 型態[大小] 可用來配置記憶體空間。

sizeof(資料型態) 取得資料的位元組大小。

對指標使用 fixed 可防止垃圾回收機制清理指標,對陣列使用可建立 C 語言風格的陣列,讓指標與陣列的對等關係跟 C 語言一致。

在 C/C++,int* A, B; 只有 A 是指標,B 是 int 變數,C# 不一樣,兩個都宣告為指標,這也造成 C/C++ 真正慣用的 int *A, *B; 寫法,在 C# 會無法辨識而報錯。

所謂「不安全(Unsafe)」並非指這樣的程式碼有危害,而是指跳脫 CLR(Common Language Runtim)對記憶體驗證的安全機制。

陣列

陣列大家都很熟了,來看 C# 讓事情變有趣的語法糖,自動型別判定且匿名的陣列:

new[] { 資料, 資料, ... };

new[]
{
 new[] { 資料, 資料, ... },
 new[] { 資料, 資料, ... },
 new[] { 資料, 資料, ... }
};

new[,]
{
 { 資料, 資料, ... },
 { 資料, 資料, ... },
 { 資料, 資料, ... }
};

[,] 是名符其實的多維陣列,而不是 [][] 陣列的陣列。

[,] 陣列每組維度大小是相同的,且迭代時會一次跑所有維度的資料。

[][] 陣列可以混合不同大小的維度,且迭代只會跑一個維度的資料。

這兩種可以混搭在一起,例如 array[1,2][3]。

字串

字串可以用 [] 索引值直接取出字元。

@"字串" 裡面可以直接使用跳脫字元,例如 \ 符號和換行字元,所以有多行字串的功能。也因此字串裡面不能使用 \",必須寫成 ""。

流程控制

switch 為避免 fall through 的弊端,規定 case 區塊一定要用 break 結束,連 default 也不例外。但 case 沒有寫區塊程式的話,允許 fall through。有了這項規定,default 可以放在任意位置,不用放在最後。

其它

並行相關指令有:lockvolatileasyncawait


類別(Class)、抽象(Abstract)、介面(Interface)


hiden
override
abstract
interface

將類別和方法宣告為 partial,可以寫在不同檔案。

要覆寫父類別的函式,做法是將父類別的函式宣告為有實作的 virtual 或不實作的 abstract,子類別的函式宣告為 override。不這麼做的話無法多型,即使傳入子類別物件,也會調用父類別的方法,而不是子類別覆寫的方法。

雖然子類別可以在同名成員前使用 new 強行覆寫過去,但這叫隱藏(hiden),不是覆載(override),所以情況還是一樣,在多型的場合不會調用子類別覆寫的方法。這麼做只是讓編譯器不會發出 warning 警告而已,如果只是要編譯器別發出 warning,還不如下 /w:0 參數關閉 warning,保持程式碼的簡潔 😛

可用 sealed 禁止類別被繼承或禁止覆寫成員函式。

要在類別的建構函式呼叫另一個建構函式,做法是建構函式後面使用 :this(參數)。子類別的建構函式要呼叫父類別的建構函式,則是 :base(參數)

變數 is 型別 用來檢查變數是否為型別。

物件 as 型別 用來轉換物件為型別,與 (型別)變數 不一樣的是,在無法轉型時不會拋出例外,而是變成 null,且只能用於類別的物件,不能用於基本資料型態的變數。


匿名物件(Anonymous types)

var 變數 = new { 鍵=值, 鍵=值, ... };

鍵可以是英文字,不需使用 "" 符號。

這語法糖傳回自動判定型別的 AnonymousType 物件,所以指派給變數必須用 var,不能用明確的型態定義變數。

匿名物件是唯讀的,所以無法修改資料。


結構(Structure types)


#58 Twideem Civs


其它語法

結構的成員變數在宣告時不能直接用 = 預設,只能在建構式初始化。

建構式不能有空參數的建構子,且建構子裡必須設定所有成員變數,不能只設定部分成員變數。

結構裡成員的預設存取權限是 private,所以包括建構式都要 public 才能使用。

結構不能繼承。


為何使用

結構當參數傳入函式是傳值,類別是傳參考。

結構是在 stack 記憶體建立,存取資料更直接而快速,用完會自動釋放空間,節省記憶體。類別是在 heap 記憶體建立,存取資料是鍊式的,所以比較間接,加上自動垃圾回收機制的關係,不但占用大量記憶體空間,資料還可能東一塊西一塊的不連續,性能沒有結構好。

但並不是類別的性能差,而是結構的性能要比類別更好!我們是為了更好的性能而使用結構,而不是認為類別性能差不敢用通通改成結構。不要小看 Microsoft 團隊菁英為 .NET 設計的 heap 記憶體模式,性能已經是完美無缺的了,一點也不慢,不需要擔心類別拖累性能的問題。

總之,盡量用類別組織程式架構,不用擔心性能的問題。程式架構中用的資料,能用結構去做是最好的選擇,但是用類別去做也不要緊。


列舉(Enumeration types)


Two
0
Eleven
7


泛型(Generic)

進行物件導向程式設計,經常要寫一堆只有型別不同,剩下幾乎相同的程式碼,泛型可以消除重複:


123
ABC

函式也可以用泛型,將重複程式碼的多載函式寫成一個!


協變(Covariance)、逆變(Contravariance)

不像 string 可以向上轉型為 object,泛型 G<string> 無法轉 G<object>,不然就失去型別安全的目的了,等於開放做出越過限制的行為。

但介面和委託並沒有實作功能,也就沒有破壞型別安全的問題,因此介面和委託的泛型,可以在型別前面使用 out,讓型別能向上轉型,也就是協變:

interface G<out T>{}

相反的,要向下轉型的話(逆變),在型別前面使用 in:

interface G<in T>{}


命名空間(Namespace)


namespace


Class 1
Class 2
Class 3


using

class1.cs

class2.cs

class3.cs

launch.cs


Class 1
Class 2
Class 3
Class 3


組件(Assembly)


編譯成 DLL 並載入執行

library.cs


csc /t:library library.cs

launch.cs


csc /r:library.dll launch.cs
launch
Hello!

善用動態連結程式庫在執行期載入的特性,是很有幫助的程式設計手法 💪


別名

載入多個組件時,若命名空間和類別名稱重複,可先在編譯器下參數 /r:別名一=組件一.dll,然後在程式碼開頭使用 extern alias 別名一; 載入別名,就能用 別名一::命名空間.類別 解決衝突問題。別名二依此類推。


特性(Attribute)

特性可夾帶數據(Metadata)給類別,語法為:

[特性]
class 類別名稱
{
 …
}

除了 C# 已提供的特性,我們也可以透過繼承 System.Attribute 產生新的特性來用。


函式(Method)

呼叫函式時,可以用 參數名稱:值 不照順序下參數。

設計函式時,參數可以用 = 指定預設值。

在陣列型態的參數前面使用 params,表示不定數量參數。

參數前面使用 ref 表示傳址參數,傳入的變數必須有資料,不能空值。(另外還有 out 傳址參數,必須在函式中先初始化,也因為這樣的關係,可以傳入未初始化、空值的變數。)

可將其它程式語言編譯出來的函式引用到 C# 裡面,語法為:

[DllImport("程式庫檔")]
extern 傳回值 函式名稱(參數);

程式庫除了 DLL 檔,也支援 Linux 常用的 shared object 檔(*.so),但 DllImport 要下更多參數解決差異性問題,請參考 System.Runtime.InteropServices.DllImport 的 API。


擴充方法(Extension method)


ABCABCABC
369
TRIPLE

重點在方法的參數使用了 this,且必須宣告為 static。

有這語法,就不用為了擴充方法另外繼承一個新的類別,而是直接追加進去!


迭代器(Iterator)


AAA
BBB
CCC

也可以用泛型的 System.Collections.Generic.IEnumerable<T>

在迴圈中可以使用 yield break 完成迭代


屬性(Property)


123 Hello, ABCABC!
get 和 set 這樣的語法功能,在 C# 叫做存取器(Accessor)。

若狀態會保存起來被使用到,就設計為屬性!方法用在不會保存狀態,單純計算出資料或運行些動作的場合。

但如果狀態不是保存在自身,而是其它物件,則用方法,而不是屬性。也就是寫成:

自身.方法(他物件, 狀態)

而不是:

自身.他物件.屬性 = 狀態


索引器(Indexer)


indexer


運算子重載(Operator overloading)


**
****
2


委派、函數物件、函數傳參、匿名函式、λ 運算式、多播、事件

委派的相關功能,是隨著 C# 版本不斷追加進去的,所以不同寫法做的卻是同一件事時,盡量用新的寫法取代舊的,除非編譯器版本老舊不支援 😎


用委派設計函數物件


123
246
369

function = new Function(F1) 的寫法,可以寫成 function = F1


傳遞函數物件為參數


Hello!


藉由函數物件參數實現匿名函式(Anonymous function)


Hello, world!
Hello, my friend!


匿名函式可改寫為 λ 運算式(Lambda expression)


Hello, world!
Hello, my friend!

只有一個參數時,可以省略 () 符號,也就是 (x)=> 寫成 x=>


多播(Multicast)


111
222
333


事件(Event)

.NET 提供的事件,就是以委派為架構,搭配 event 關鍵字自動化多播,所設計出來的!雖然平常不太需要自己設計事件來用,但了解一下是怎麼設計的,滿足一下求知欲也不錯:


ON EVENT 1
ON EVENT 2

event 的多播可以用 add 存取器和 remove 存取器覆寫 += 和 -= 的功能。

為相容 .NET 的事件,通常委派時設計成 void Handler(object sender, EventArgs e) 的形式,即使這些參數沒有要用也沒關係,系統會自動略過。


反射(Reflection)

反射可以讓我們在執行期檢視物件內部結構,當我們不知道執行期會遇到哪個物件、也不知道會有哪些功能,就可以透過反射來使用未知的物件…雖然這通常不會是很好的設計,往往都在破壞物件導向設計原則。

.NET 提供了 System.Type 類別,用來分析類別和物件的內部組成,可用如下方式取得 Type 物件:

typeof(類別名稱)
物件實體.GetType()
Type.GetType(字串)

要調用內部功能的話,可對 Type 物件呼叫 GetMethods() 取得 MethodInfo 物件,再執行 MethodInfo 的 Invoke(object obj, object[] parameters) 即可。其它內部成員依此類推,取得對應的 XxxInfo 物件,再看怎樣去使用它。

反射是深入研究的話會很龐大的主題,在 System.Reflection 命名空間提供完整的支援。


用反射機制列出物件規格

若不需要範例和說明,只想速查物件有哪些功能可用,不妨直接寫 C# 程式反射出來:

foreach(MemberInfo n in typeof(類別).GetMembers()) Console.WriteLine(n);
foreach(Array n in typeof(列舉).GetEnumValues()) Console.WriteLine(n);

不然每次都翻 API 太麻煩了 😩


例外處理(Exception handling)

過去透過函式傳回值來判斷錯誤訊息,經常因為除錯的程式碼和執行的程式碼混在一起,苦不堪言。例外為除錯提供專屬區塊,要執行的程式碼放在 try,要捕捉的錯誤放在 catch,結構化變得更清晰,大幅提升可讀性。語法如下:

try
{
 可能會發生狀況的程式
 checked
 {
  整數發生不會導致錯誤的溢位時仍拋出例外
 }
 unchecked
 {
  整數發生會導致錯誤的溢位時也不拋出例外
 }
}
catch(XxxException)
{
 捕捉到特定狀況的訊息
}
catch(XxxException e)
{
 透過物件進一步獲取狀況的內容
}
catch
{
 捕捉到其他狀況的訊息
}
finally
{
 一定會執行的程式
}

可以繼承 System.Exception 新增自己的例外,然後用 throw 拋出,拋出例外時記得 new 成物件。


垃圾收集(Garbage collection)


回收

建議交由系統自動回收,真要強制回收的話,執行 GC.Collect()


丟棄

使用完需要 Dispose() 的物件,可以寫在 using(){} 裡面,用完會自動呼叫。

Dispose() 表示棄置物件,釋放物件佔用的記憶體資源,通常會在裡面呼叫 Close()。

Close() 表示結束物件的操作,釋放操作時佔用的系統資源,但物件還在記憶體裡,並未清除,不見得會呼叫 Dispose()。

每個物件的 Close() 意思不一樣,像是 Form 的 Close() 是關閉視窗,與記憶體資源和系統資源無關。因此 .NET 統一使用 Dispose() 來關閉物件,設計在 IDisposable 介面讓需要自動關閉的物件遵循,只要實作了 Dispose() 就會在 using 區塊裡自動呼叫。


查詢語句(Query Syntax)

C# 獨有,簡化 LINQ API 而來,下語法就能操作資料。

由於查詢語句的語法很接近 SQL,已經會的人學習門檻低,而且用起來方便有效率,可以突破不少傳統程式設計處理資料的瓶頸與限制,讓大家對它愛不釋手。


語法概述

查詢語句一律以 from.. in.. 開頭,以 selectgroup.. by.. 結束,中間可加入各種子句,像是 join.. in.. on.. equals..whereorderbylet

開頭和中間子句能使用多個一樣指令的句子,一層層或一批批處理資料。巧妙應用的話,經常能把幾十行程式碼才能搞定的事,縮短成三四行。

若要重複整個查詢語句,可以追加 into 構成分句,將這一次查詢語句的結果保存在識別名稱,做為下一個查詢語句的資料來源。

另外,在結束子句用 new 將資料結構化成物件傳回也是很常見的用法。

查詢語句結束後,會傳回實作 IEnumerable<T> 的物件,查閱這介面的 API 可以了解更多操作資料的方式。由於查詢語句並未提供該介面的所有功能,因此查詢語句經常會用 (查詢語句).API 方法() 的形式,呼叫 API 的方法操作資料。所以並不是只會查詢語句就算完整,還得了解 Enumerable 物件有哪些方法可以操作。

查詢語句是用起來很精妙的語法!我們除了把自己日常處理資料的語句建立起來外,日後還要多看別人寫的查詢語句,把好的句子抄下來,找機會用上。


基本子句(from、where、select)


796810

CDE

2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
2 * 4 = 8
2 * 5 = 10
2 * 6 = 12
2 * 7 = 14
2 * 8 = 16
2 * 9 = 18
(太長省略)
9 * 9 = 81


其它常用子句

join

句法:join 變數 in 資料 on FROM變數 equals JOIN變數

相當於關聯式資料庫將資料表依相同鍵值互相關聯起來使用一樣,依 equals 將 join 與 from 互相關聯起來使用。(如果你不知道關聯式資料庫,就是將兩份資料中相同鍵值的部分篩選出來,要注意的是並非合併成新的一份資料,同樣是兩份資料,只是各自都只剩篩選出來的資料。)

換句話說,join 緊接在 from 句子的後面,將兩份資料互相關聯起來。如果你不太能理解 join,不妨想成這是 from 開頭子句的更多指令,而不是真有一個 join 子句…雖然這麼想是錯的,但確實能幫助理解 join 與其它中間子句的不同。


C:C
D:D
E:E

orderby

句法:orderby 資料 descending

排序資料。descending 是遞減排序,省略的話是遞增排序。

let

句法:let 變數 = 算式

建立一個保存資料的變數,讓其它子句使用。

group

句法:group 資料 by 鍵值

將資料依鍵值分組為 IGrouping<TKey, TElement> 物件。

假設有一組球員資料,想依位置分組,就可以用 group 簡單完成:


C
赤木剛憲
角田悟
SG
三井寿
潮崎哲士
SF
木暮公延
流川楓
石井健太郎
PG
宮城リョータ
安田靖春
桑田登紀
PF
佐々岡智
桜木花道

into

句法:into 變數 新的語句

將查詢語句結果,命名為變數,展開新的查詢語句。

換句話說,into 接在 select 或 group 結束子句後面,然後就跟展開新的 from 開始語句一樣,繼續串接中間子句。


查詢語句轉 Array


49
64
81