C# 7.0本質論
Essential C# 7.0

[美] 馬克米凱利斯(Mark Michaelis)著
周 靖 譯
第1章
C#概述
C#是一種成熟的語言,基于作為前身的C風格語言(C、C++和Java)的功能而設計,有經驗的程式員能很快熟悉。此外,可用C#建構在多種作業系統(平台)上運作的軟體元件和應用程式。
本章用傳統HelloWorld程式介紹C#,重點是C#文法基礎,包括定義C#程式入口。通過本章的學習,你将熟悉C#的文法風格和結構,能開始寫最簡單的C#程式。讨論C#文法基礎之前,将簡單介紹托管執行環境,并解釋C#程式在運作時如何執行。最後讨論變量聲明、控制台輸入/輸出以及基本的C#代碼注釋機制。
1.1 Hello, World
學習新語言最好的辦法就是寫代碼。第一個例子是經典HelloWorld程式,它在螢幕上顯示一些文本。代碼清單1.1展示了完整HelloWorld程式,我們将在之後的小節編譯代碼。
注意 C#是區分大小寫的語言,大小寫不正确會使代碼無法成功編譯。
有Java、C或者C++程式設計經驗的讀者很快就能看出相似的地方。類似于Java,C#也從C和C++繼承了基本的文法。文法标點(比如分号和大括号)、特性(比如區分大小寫)和關鍵字(比如class、public和void)對于這些程式員來說并不陌生。初學者和其他語言背景的程式員通過這個程式能很快體會到這些構造的直覺性。
1.1.1 建立、編輯、編譯和運作C#源代碼
寫好C#代碼後需要編譯和運作。這時要選擇使用哪個.NET實作(或者說.NET架構)。這些實作通常打包成一個軟體開發包(Software Development Kit,SDK),其中包括編譯器、運作時執行引擎、“運作時”能通路的語言可通路功能架構(參見本章後面的1.7.1節),以及可能和SDK捆綁的其他工具(比如供自動化生成的生成引擎)。由于C#自2000年便已公布,目前有多個不同的.NET架構供選擇(參見本章後面的1.7節)。
取決于開發的目标作業系統以及你選擇的.NET架構,每種.NET架構的安裝過程都有所差別。有鑒于此,建議通路
https://www.microsoft.com/net/download了解具體的下載下傳和安裝訓示。先選好.NET架構,再根據目标作業系統選擇要下載下傳的包。雖然我可以在這裡提供更多細節,但.NET下載下傳站點為支援的各種組合提供了最新、最全的指令。
如不确定要使用的.NET架構,就預設選擇.NET Core。它可運作于Linux、macOS和Microsoft Windows,是.NET開發團隊投入最大的實作。另外,由于它具有跨平台能力,是以本書優先使用.NET Core。
有許多源代碼編輯工具可供選擇,包括最基本的Windows記事本、Mac/macOS TextEdit和Linux vi。但建議選擇一個稍微進階點的工具,至少應支援彩色标注。支援C#的任何代碼編輯器都可以。如果還沒有特别喜歡的,推薦開源編輯器Visual Studio Code(
https://code.visualstudio.com)。如果在Windows或Mac上工作,也可考慮Microsoft Visual Studio 2017(或更高版本),詳情參考
https://www.visualstudio.com。兩者都是免費的。
後兩節我會提供這兩種編輯器的操作訓示。Visual Studio Code依賴指令行(Dotnet CLI)建立初始的C#程式基架并編譯和運作。Windows和Mac則一般使用Visual Studio 2017。
使用Dotnet CLI
Dotnet指令dotnet.exe是Dotnet指令行接口(或稱Dotnet CLI),可用于生成C#程式的初始代碼庫并編譯和運作程式。注意這裡的CLI代表“指令行接口”(Command-Line Interface)。為避免和代表“公共語言基礎結構”(Common Language Infrastructure)的CLI混淆,本書在提到Dotnet CLI時都會附加Dotnet字首。無Dotnet字首的CLI才是“公共語言基礎結構”。安裝好之後,驗證可以在指令行上執行dotnet。
以下是在Windows、macOS或Linux上建立、編譯和執行HelloWorld程式的訓示:
- 在Microsoft Windows上打開指令提示符,在Mac/macOS上打開Terminal應用。(也可考慮使用跨平台指令行接口PowerShell。)
- 在想要放代碼的地方建立一個目錄。考慮./HelloWorld或./EssentialCSharp/HelloWorld這樣的名稱。在指令行上執行:
- 導航到新目錄,使之成為指令行的目前目錄:
- 在HelloWorld目錄中執行dotnet new console指令來生成程式基架。這會生成幾個檔案,最主要的是Program.cs和項目檔案:
- 運作生成的程式。這會編譯并運作由dotnet new console指令建立的預設Program.cs程式。程式内容和代碼清單1.1相似,隻是輸出變成“Hello World!”。
雖然沒有顯式請求應用程式編譯(或生成),但dotnet run command指令在執行時隐式執行了這一步。
- 編輯Program.cs檔案并修改代碼使之和代碼清單1.1一緻。用Visual Studio Code打開并編輯Program.cs會體驗到支援C#的編輯器的好處,代碼會用彩色标注不同類型的構造。(輸出1.1展示了在Bash和PowerShell中适合指令行的一種方式。)
- 重新運作程式:
帶你讀《C# 7.0本質論》之一:C# 概 述第1章
輸出1.1展示了上述步驟的輸出。
使用Visual Studio 2017
在Visual Studio 2017中的操作相似,隻是不用指令行,而是用內建開發環境(IDE)。有菜單可選,不必一切都在指令行上進行。
- 啟動Visual Studio 2017。
- 選擇“檔案”|“建立”|“項目”(Ctrl + Shift + N)菜單打開“建立項目”對話框。
- 在搜尋框(Ctrl + E)中輸入“控制台應用”并選擇“控制台應用(.NET Core)—Visual C#”。在“名稱”框中輸入HelloWorld。在“位置”處選擇你的工作目錄。如圖1.1所示。
- 項目建立好後會打開Program.cs檔案供編輯,如圖1.2所示。
- 選擇“調試”|“開始執行(不調試)”(Ctrl + F5)來生成并運作程式。會顯示如輸出1.2所示的指令視窗,隻是第一行目前為“Hello World! ”。
- 将Program.cs修改成代碼清單1.1的樣子。
- 傳回程式并重新運作,獲得如輸出1.2所示的結果。
輸出1.2
IDE最重要的一個功能是調試。按以下額外的步驟試驗:
- 光标定位到System.Console.WriteLine這一行,選擇“調試”|“切換斷點”(F9)在該行激活斷點。
- 選擇“調試”|“開始調試”(F5)重新啟動應用程式,但這次激活了調試功能。注意會在斷點所在行停止執行。此時可将滑鼠放到某個變量(例如args)上以觀察它的值。還可以拖動左側黃箭頭将程式執行從目前行移動到方法内的另一行。
- 要繼續執行,選擇“調試”|“繼續”(Ctrl + F5)或者點選工具欄上的“繼續”按鈕。
調試時輸出視窗不再出現“請按任意鍵繼續...”提示,而是自動關閉。注意Visual Studio Code也可作為IDE使用,詳情參見
https://code.visualstudio.com/docs/languages/csharp。其中還提供了一個連結來解釋用Visual Studio Code進行調試的問題。
1.1.2 建立項目
無論Dotnet CLI還是Visual Studio都會自動建立幾個檔案。第一個是名為Program.cs的C#檔案。雖然可選擇任何名稱,但一般都用Program這一名稱作為控制台程式起點。.cs是所有C#檔案的标準擴充名,也是編譯器預設要編譯成最終程式的擴充名。為了使用代碼清單1.1中的代碼,可打開Program.cs檔案并将其内容替換成代碼清單1.1的。儲存更新檔案之前,注意代碼清單1.1和預設生成的代碼相比,唯一功能上的差異就是引号間的文本。還有就是後者多了using System;指令,這是一處語義上的差異。
雖然并非一定需要,但通常都會為C#項目生成一個項目檔案。項目檔案的内容随不同應用程式類型和.NET架構而變。但至少會指出哪些檔案要包含到編譯中,要生成什麼應用程式類型(控制台、Web、移動、測試項目等),支援什麼.NET架構,調試或啟動應用程式需要什麼設定,以及代碼的其他依賴項(稱為庫)。例如,代碼清單1.2列出了一個簡單的.NET Core控制台應用項目檔案。
注意應用程式辨別為.NET Core版本2.0(netcoreapp2.0)的控制台應用(Exe)。其他所有設定(比如要編譯哪些C#檔案)則沿用預設值。例如,和項目檔案同一目錄(或子目錄)中的所有*.cs檔案都會包含到編譯中。第10章會更多地讨論項目檔案。
1.1.3 編譯和執行
dotnet build指令生成名為HelloWorld.dll的程式集(assembly)。擴充名.dll代表“動态連結庫”(Dynamic Link Library,DLL)。對于.NET Core,所有程式集都使用.dll擴充名。控制台程式也不例外,就像本例這樣。.NET Core應用程式的編譯輸出預設放到子目錄./bin/Debug/netcoreapp2.0/。之是以使用Debug這個名稱,是因為預設配置就是debug。該配置造成輸出為調試而不是性能而優化。編譯好的輸出本身不能執行。相反,需用CLI來寄宿(host)代碼。對于.NET Core應用程式,這要求Dotnet.exe程序作為應用程式的寄宿程序。
開發人員可以不用dotnet run建立能直接運作的控制台程式,而是建立可由其他(較大的)程式來引用的庫。庫也是程式集。換言之,一次成功的C#編譯,結果必然是程式集,無論該程式集是程式還是庫。
1.1.4 使用本書源代碼
本書源代碼包含解決方案檔案EssentialCSharp.sln,它組合了全書所有代碼。Visual Studio和Dotnet.exe都能生成、運作和測試這些源代碼。或許最簡單的方式是将源代碼拷貝到早先建立的HelloWorld程式中并執行。但是,解決方案包含了各章的項目檔案,還提供了一個菜單來選擇要執行的代碼清單。詳情參見以下兩節。
要用Dotnet CLI生成并執行代碼,請打開指令提示符,将目前目錄設為EssentialCSharp.sln檔案所在的目錄。執行dotnet build指令編譯所有項目。
要運作特定項目的源代碼,導航到項目檔案所在目錄并執行dotnet run指令。另外,在任何目錄都可以執行dotnet run -p 指令。其中是要執行的項目檔案的路徑(例如dotnet run -p .srcChapter01Chapter01.csproj)。随後會運作程式,并提示運作的是哪個代碼清單。
許多代碼清單都在Chapter[??].Tests目錄中提供了相應的單元測試。其中[??]是章的編号。要執行測試,在相應目錄中執行dotnet test指令(在EssentialCSharp.sln所在目錄執行該指令,則所有單元測試都會執行)。
使用Visual Studio
在Visual Studio中打開解決方案檔案後,選擇“生成”|“生成解決方案”(F6)來編譯代碼。要執行某一章的項目,需要先将該章的項目設為啟動項目。例如,要執行第1章的示例,請右擊Chapter01項目并選擇“設為啟動項目”。若不這樣做,執行時輸入非啟動項目所在章的代碼清單編号會抛出異常。
設定好正确項目後,選擇“調試”|“開始執行(不調試)”(Ctrl + F5)來運作項目。如需調試則按F5。運作時程式會提示輸入代碼清單的編号(例如1.1)。如前所述,隻能輸入已啟動項目中的代碼清單。
許多代碼清單都有對應的單元測試。要執行測試,打開測試項目(Chapter[??].Tests),導航到與代碼清單對應的測試(比如HelloWorldTests)。輕按兩下它在代碼編輯器中顯示。右擊要測試的方法(比如public void Main_InigoHello()),右擊并選擇“運作測試”(Ctrl + R, T)或“調試測試”(Ctrl + R, Ctrl + T)。
1.2 C#文法基礎
成功編譯并運作HelloWorld程式之後,我們來分析代碼,了解它的各個組成部分。首先熟悉一下C#關鍵字以及可供開發者選擇的辨別符。
1.2.1 C#關鍵字
表1.1總結了C#關鍵字。
C# 1.0之後沒有引入任何新的保留關鍵字,但在後續版本中,一些構造使用了上下文關鍵字,它們在特定位置才有意義,在其他位置則無意義。這樣大多數C# 1.0代碼都能相容後續版本。
1.2.2 辨別符
和其他語言一樣,C#用辨別符辨別程式員編碼的構造。在代碼清單1.1中,HelloWorld和Main均為辨別符。配置設定辨別符之後,以後将用它引用所辨別的構造。是以,開發者應配置設定有意義的名稱,不要随性而為。
好的程式員總能選擇簡潔而有意義的名稱,這使代碼更容易了解和重用。清晰和一緻是如此重要,以至于“架構設計準則”(
http://t.cn/RD6v4RB)建議不要在辨別符中使用單詞縮寫,甚至不要使用不被廣泛接受的首字母縮寫詞。即使被廣泛接受(如HTML),使用時也要一緻。不要一會兒這樣用,一會兒那樣用。為避免濫用,可限制所有首字母縮寫詞都必須包含到術語表中。總之,要選擇清晰(甚至是詳細)的名稱,尤其是在團隊中工作,或者開發要由别人使用的庫的時候。
辨別符有兩種基本的大小寫風格。第一種風格是.NET架構建立者所謂的Pascal大小寫(PascalCase),它在Pascal程式設計語言中很流行,要求辨別符的每個單詞首字母大寫,例如ComponentModel、Configuration和HttpFileCollection。注意在HttpFileCollection中,由于首字母縮寫詞HTTP的長度超過兩個字母,是以僅首字母大寫。第二種風格是camel大小寫(camelCase),除第一個字母小寫,其他約定一樣,例如quotient、firstName、httpFileCollection、ioStream和theDreadPirateRoberts。
下劃線雖然合法,但辨別符一般不要包含下劃線、連字号或其他非字母/數字字元。此外,C#不像其前輩那樣使用匈牙利命名法(為名稱附加類型縮寫字首)。這避免了資料類型改變時還要重命名變量,也避免了資料類型字首經常不一緻的情況。
極少數情況下,有的辨別符(比如Main)可能在C#語言中具有特殊含義。
進階主題:關鍵字
雖然罕見,但關鍵字附加“@”字首可作為辨別符使用,例如可命名局部變量@return。類似地(雖不符合C#大小寫規範),可命名方法@throw()。
在Microsoft的實作中,還有4個未文檔化的保留關鍵字:__arglist,__makeref,__reftype,__refvalue。它們僅在罕見的互操作情形下才需要使用,平時完全可以忽略。注意這4個特殊關鍵字以雙下劃線開頭。C#設計者保留将來把這種辨別符轉化為關鍵字的權利。為安全起見,自己不要建立這樣的辨別符。
1.2.3 類型定義
C#所有代碼都出現在一個類型定義的内部,最常見的類型定義以關鍵字class開頭。如代碼清單1.3所示,類定義是class <辨別符> { ... }形式的代碼塊。
類型名稱(本例是HelloWorld)可以随便取,但根據約定,它應當使用PascalCase風格。就本例來說,可選擇的名稱包括Greetings、HelloInigoMontoya、Hello或者簡單地稱為Program。(對于包含Main()方法的類,Program是個很好的名稱。Main()方法的詳情稍後講述。)
1.2.4 Main方法
C#程式從Main方法開始執行。該方法以static void Main()開頭。在指令控制台中輸入HelloWorld.exe執行程式,程式将啟動并解析Main的位置,然後執行其中第一條語句。如代碼清單1.4所示。
雖然Main方法聲明可進行某種程度的變化,但關鍵字static和方法名Main是始終都需要的。
将Main方法指定為static意味着這是“靜态”方法,可用類名.方法名的形式調用。若不指定static,用于啟動程式的指令控制台還要先對類進行執行個體化,然後才能調用方法。第6章将用整節篇幅講述靜态成員。
Main()之前的void表明方法不傳回任何資料(将在第2章進一步解釋)。
C#和C/C++一樣使用大括号封閉構造(比如類或者方法)的主體。例如,Main方法主體就是用大括号封閉起來的。本例方法主體僅一條語句。
1.2.5 語句和語句分隔符
Main方法隻含一條語句,即System.Console.WriteLine();,它在控制台上輸出一行文本。C#通常用分号辨別語句結束。每條語句都由代碼要執行的一個或多個行動構成。聲明變量、控制程式流程或調用方法都是語句的例子。
由于換行與否不影響語句的分隔,是以可将多條語句放到同一行,C#編譯器認為這一行包含多條指令。例如,代碼清單1.6在同一行包含了兩條語句。執行時在控制台視窗分兩行顯示Up和Down。
C#還允許一條語句跨越多行。同樣地,C#編譯器根據分号判斷語句結束位置。代碼清單1.7展示了一個例子。
代碼清單1.7的WriteLine()語句的原始版本來自HelloWorld程式,它在這裡跨越了多行。
1.2.6 空白
分号使C#編譯器能忽略代碼中的空白。除少數特殊情況,C#允許代碼随意插入空白而不改變語義。在代碼清單1.6和代碼清單1.7中,在語句中或語句間換行都可以,對編譯器最終建立的可執行檔案沒有任何影響。
程式員經常利用空白對代碼進行縮進來增強可讀性。來看看代碼清單1.8和代碼清單1.9展示的兩個版本的HelloWorld程式。
雖然這兩個版本看起來和原始版本頗有不同,但C#編譯器認為所有版本無差别。
1.3 使用變量
前面我們已接觸了最基本的C#程式,下面聲明局部變量。變量聲明後可以指派,可将值替換成新值,并可在計算和輸出等操作中使用。但變量一經聲明,資料類型就不能改變。在代碼清單1.10中,string max就是變量聲明。
1.3.1 資料類型
代碼清單1.10聲明的是string類型的變量。本章還使用了int和char。
- int是指C#的32位整型。
- char是字元類型,長度為16位,足以表示無代理項的Unicode字元。
下一章将更詳細地探讨這些以及其他常見資料類型。
1.3.2 變量的聲明
代碼清單1.10中的string max是變量聲明,它聲明名為max的string變量。還可在同一條語句中聲明多個變量,辦法是指定資料類型一次,然後用逗号分隔不同辨別符,如代碼清單1.11所示。
由于聲明多個變量的語句隻允許提供一次資料類型,是以所有變量都具有相同類型。
C#變量名可用任何字母或下劃線(_)開頭,後跟任意數量的字母、數字或下劃線。但根據約定,局部變量名采用camelCase命名(除了第一個單詞外,其他每個單詞的首字母大寫),而且不包含下劃線。
1.3.3 變量的指派
局部變量聲明後必須在讀取前指派。一個辦法是使用=操作符,或者稱為簡單指派操作符。操作符是一種特殊符号,辨別了代碼要執行的操作。代碼清單1.12示範了如何利用指派操作符指定miracleMax和valerie變量要指向的字元串值。
從中可以看出,既可在聲明變量的同時指派(比如變量miracleMax),也可在聲明後用另一條語句指派(比如變量valerie)。要賦的值必須放在指派操作符右側。
運作編譯好的程式生成如輸出1.3所示的結果。
輸出1.3
本例列出了dotnet run指令,以後會省略,除非要附加額外參數來指定程式的運作方式。
C#要求局部變量在讀取前“明确指派”。此外,指派作為一種操作會傳回一個值。是以C#允許在同一語句中進行多個指派操作,如代碼清單1.13所示。
1.3.4 變量的使用
指派後就能用變量名引用值。是以,在System.Console.WriteLine(miracleMax)語句中使用變量miracleMax時,程式在控制台上顯示Have fun storming the castle!,也就是miracleMax的值。更改miracleMax的值并執行相同的System.Console.WriteLine(miracleMax)語句,會顯示miracleMax的新值,即It would take a miracle.。
1.4 控制台輸入和輸出
本章已多次使用System.Console.WriteLine将文本輸出到指令控制台。除了能輸出資料,程式還需要能接收使用者輸入的資料。
1.4.1 從控制台擷取輸入
可用System.Console.ReadLine()方法擷取控制台輸入的文本。它暫停程式執行并等待使用者輸入。使用者按Enter鍵,程式繼續。System.Console.ReadLine()方法的輸出,也稱為傳回值,其内容即使用者輸入的文本字元串。代碼清單1.14和輸出1.4是一個例子。
輸出1.4
在每條提示資訊後,程式都用System.Console.ReadLine()方法擷取使用者輸入并賦給變量。在第二個System.Console.ReadLine()指派操作完成之後,firstName引用值Inigo,而lastName引用值Montoya。
C# 2.0新增了System.Console.ReadKey()方法。它和System.Console.Read()方法不同,傳回的是使用者的單次按鍵輸入。可用它攔截使用者按鍵操作,并執行相應行動,比如校驗按鍵或是限制隻能按數字鍵。
1.4.2 将輸出寫入控制台
代碼清單1.14是用System.Console.Write()而不是System.Console.WriteLine()方法提示使用者輸入名和姓。System.Console.Write()方法不在輸出文本後自動添加換行符,而是保持目前光标位置在同一行上。這樣使用者輸入就會和提示内容處于同一行。代碼清單1.14的輸出清楚示範了System.Console.Write()的效果。
下一步是将通過System.Console.ReadLine()擷取的值寫回控制台。在代碼清單1.16中,程式在控制台上輸出使用者的全名。但這段代碼使用了System.Console.WriteLine()的一個變體,利用了從C# 6.0開始引入的字元串插值功能。注意在Console.WriteLine調用中為字元串字面值附加的$字首。它表明使用了字元串插值。輸出1.5是對應的輸出。
輸出1.5
代碼清單1.16不是先用Write語句輸出"Your full name is",再用Write語句輸出firstName,用第三條Write語句輸出空格,最後用WriteLine語句輸出lastName。相反,是用C# 6.0的字元串插值功能一次性輸出。字元串中的大括号被解釋成表達式。編譯器會求值這些表達式,轉換成字元串并插入目前位置。不需要單獨執行多個代碼段并将結果整合成字元串,該技術允許一個步驟完成全部操作,進而增強了代碼的可讀性。
C# 6.0之前則采用不同的方式,稱為複合格式化。它要求先提供格式字元串來定義輸出格式,如代碼清單1.17所示。
本例的格式字元串是Your full name is {0} {1}.。它為要在字元串中插入的資料辨別了兩個索引占位符。每個占位符的順序對應格式字元串之後的實參。
注意索引值從零開始。每個要插入的實參,或者稱為格式項,按照與索引值對應的順序排列在格式字元串之後。在本例中,由于firstName是緊接在格式字元串之後的第一個實參,是以它對應索引值0。類似地,lastName對應索引值1。
注意,占位符在格式字元串中不一定按順序出現。例如,代碼清單1.18交換了兩個索引占位符的位置并添加了一個逗号,進而改變了姓名的顯示方式(參見輸出1.6)。
輸出1.6
占位符除了能在格式字元串中按任意順序出現,同一占位符還能在一個格式字元串中多次使用。另外,也可省略占位符。但每個占位符都必須有對應的實參。
1.5 注釋
本節修改代碼清單1.16來添加注釋。注釋不會改變程式的執行,隻是使代碼變得更容易了解。代碼清單1.19中展示了新代碼,輸出1.7是對應的輸出。
輸出1.7
雖然插入了注釋,但編譯并執行後産生的輸出和以前是一樣的。
程式員用注釋來描述和解釋自己寫的代碼,尤其是在文法本身難以了解的時候,或者是在另辟蹊徑實作一個算法的時候。隻有檢查代碼的程式員才需要看注釋,編譯器會忽略注釋,因而生成的程式集中看不到源代碼中的注釋的一絲蹤影。
表1.2總結了4種不同的C#注釋。代碼清單1.19使用了其中兩種。
第10章将更全面地讨論XML注釋。屆時會讨論各種XML标記。
程式設計史上确有一段時期,如代碼沒有詳盡的注釋,都不好意思說自己是專業程式員。然而時代變了。沒有注釋但可讀性好的代碼,比需要注釋才能說清楚的代碼更有價值。如開發人員發現需要寫注釋才能說清楚代碼塊的功用,則應考慮重構,而不是洋洋灑灑寫一堆注釋。寫注釋來重複代碼本來就講得清的事情,隻會使代碼變得臃腫并降低可讀性,還容易過時,因為将來可能更改代碼但沒有來得及更新注釋。
1.6 托管執行和CLI
處理器不能直接解釋程式集。程式集用的是另一種語言,即公共中間語言(Common Intermediate Language,CIL),或稱中間語言(IL)。C#編譯器将C#源代碼檔案轉換成中間語言。為了将CIL代碼轉換成處理器能了解的機器碼,還要完成一個額外的步驟(通常在運作時進行)。該步驟涉及C#程式執行的一個重要元素:VES(Virtual Execution System,虛拟執行系統)。VES也稱為運作時(runtime)。它根據需要編譯CIL代碼,這個過程稱為即時編譯或JIT編譯(just-in-time compilation)。如代碼在像“運作時”這樣的一個“代理”的上下文中執行,就稱為托管代碼(managed code),在“運作時”的控制下執行的過程則稱為托管執行(managed execution)。之是以稱為“托管”,是因為“運作時”管理着諸如記憶體配置設定、安全性和JIT編譯等方面,進而控制了主要的程式行為。執行時不需要“運作時”的代碼稱為本機代碼(native code)或非托管代碼(unmanaged code)。
“運作時”規範包含在一個包容面更廣的規範中,即CLI(Common Language Infrastructure,公共語言基礎結構)規範。作為國際标準,CLI包含了以下幾方面的規範。
- VES或“運作時”。
- CIL。
- 支援語言互操作性的類型系統,稱為CTS(Common Type System,公共類型系統)。
- 編寫通過CLI相容語言通路的庫的指導原則(這部分内容見公共語言規範(Common Language Specification,CLS))。
- 使各種服務能被CLI識别的中繼資料(包括程式集的布局或檔案格式規範)。
在“運作時”執行引擎的上下文中運作,程式員不需要直接寫代碼就能使用幾種服務和功能,包括:
- 語言互操作性:不同源語言間的互操作性。語言編譯器将每種源語言轉換成相同中間語言(CIL)來實作這種互操作性。
- 類型安全:檢查類型間轉換,確定相容的類型才能互相轉換。這有助于防範緩沖區溢出(這是産生安全隐患的主要原因)。
- 代碼通路安全性:程式集開發者的代碼有權在計算機上執行的證明。
- 垃圾回收:一種記憶體管理機制,自動釋放“運作時”為資料配置設定的空間。
- 平台可移植性:同一程式集可在多種作業系統上運作。要實作這一點,一個顯而易見的限制就是不能使用平台特有的庫。是以平台依賴問題需單獨解決。
B- CL(基類庫):提供開發者能(在所有.NET架構中)依賴的大型代碼庫,使其不必親自寫這些代碼。
CIL和ILDASM
前面說過,C#編譯器将C#代碼轉換成CIL代碼而不是機器碼。處理器隻了解機器碼,是以CIL代碼必須先轉換成機器碼才能由處理器執行。可用CIL反彙程式設計式将程式集解構為CIL。通常使用Microsoft特有的檔案名ILDASM來稱呼這種CIL反彙程式設計式(ILDASM是IL Disassembler的簡稱),它能對程式集執行反彙編,提取C#編譯器生成的CIL。
反彙編.NET程式集的結果比機器碼更易了解。許多開發人員害怕即使别人沒有拿到源代碼,程式也容易被反彙編并曝光其算法。其實無論是否基于CLI,任何程式防止反編譯唯一安全的方法就是禁止通路編譯好的程式(例如隻在網站上存放程式,不把它分發到使用者機器)。但假如目的隻是減小别人獲得源代碼的可能性,可考慮使用一些混淆器(obfuscator)産品。這種産品會打開IL代碼,轉換成一種功能不變但更難了解的形式。這可以防止普通開發者通路代碼,使程式集難以被反編譯成容易了解的代碼。除非程式需要對算法進行進階安全防護,否則混淆器足以確定安全。
1.7 多個.NET架構
本章之前說過,目前存在多個.NET架構。Microsoft的宗旨是在最大範圍的作業系統和硬體平台上提供.NET實作,表1.3列出了最主要的實作。
除非特别注明,否則本書所有例子都相容.NET Core和Microsoft .NET Framework。但由于.NET Core才是.NET的未來,是以本書配套代碼(從
http://github.com/IntelliTect/EssentialCSharp或
http://bookzhou.com下載下傳)都配置成預設使用.NET Core。
1.7.1 應用程式程式設計接口
資料類型(比如System.Console)的所有方法(正常地說是成員)定義了該類型的應用程式程式設計接口(Application Programming Interface,API)。API定義軟體如何與其他元件互動,是以單獨一個資料類型還不夠。通常,是一組資料類型的所有API結合起來為某個元件集合建立一個API。以.NET為例,一個程式集中的所有類型(及其成員)構成了該程式集的API。類似地,.NET Core或Microsoft .NET Framework中的所有程式集構成了更大的API。通常将這一組更大的API稱為架構,是以我們用“.NET架構”一詞指代Microsoft .NET Framework的所有程式集公開的API。API通常包含一組接口和協定(或指令),幫助你使用一系列元件進行程式設計。事實上,對于.NET來說,協定本身就是.NET程式集的執行規則。
1.7.2 C#和.NET版本控制
.NET架構的開發周期有别于C#語言,這造成底層.NET架構和對應的C#語言使用了不同的版本号。例如,使用C# 5.0編譯器将預設基于Microsoft .NET Framework 4.6來編譯。表1.4簡單總結了Microsoft .NET Framework和.NET Core的C#和.NET版本。
随C# 6.0增加的最重要的一個架構功能或許是對跨平台編譯的支援。換言之,不僅能用Windows上運作的Microsoft .NET Framework編譯,還能使用Linux和macOS上運作的.NET Core實作來編譯。雖然.NET Core的功能比完整的Microsoft .NET Framework少,但足以使整個ASP.NET網站在非Windows和IIS的系統上運作。這意味着同一個代碼庫可編譯并執行在多個平台上運作的應用程式。.NET Core是一套完整的SDK,包含從.NET Compiler Platform(即“Roslyn”,本身在Linux和macOS上運作)到.NET Core“運作時”的一切,另外還提供了像Dotnet指令行實用程式(dotnet.exe,自C# 7.0引入)這樣的工具。
1.7.3 .NET Standard
有這麼多不同的.NET實作,每個.NET架構還有這麼多版本,而且每個實作都支援一套不同的、但多少有點重疊的API,這造成架構分叉得越來越厲害。這增大了寫跨.NET架構可重用代碼的難度,因為要檢查特定API是否支援。為降低複雜度,Microsoft推出了.NET Standard來定義不同版本的标準應支援哪些API。換言之,要相容于某個.NET Standard版本,.NET架構必須支援該标準所規定的API。但由于許多實作已經釋出,是以哪個API要進入哪個标準的決策樹在一定程度上基于現有實作及其與.NET Standard版本号的關聯。
寫作本書時最新釋出的是.NET Standard 2.0。該版本的好處在于所有基礎架構都已經或準備實作這個标準。是以,.NET Standard 2.0事實上重新統合了各個老版本架構中被分叉的特色API。
1.8 小結
本章對C#進行初步介紹。通過本章你熟悉了基本C#文法。由于C#與C++風格語言的相似性,本章許多内容可能都是你所熟悉的。但C#和托管代碼确實有一些獨特性,比如會編譯成CIL等。C#的另一個關鍵特征在于它完全面向對象。即使是在控制台上讀取和寫入資料這樣的事情,也是面向對象的。面向對象是C#的基礎,這一點将貫穿全書。
下一章探讨C#的基本資料類型,并讨論如何将這些資料類型應用于操作數來建構表達式。