C# 程序的常規結構
C# 語言規範
C# 程序由一個或多個文件組成。每個文件都包含零個或多個命名空間。命名空間包含類、結構、接口、枚舉和委託或其他命名空間等類型。下面的示例是包含所有這些元素的 C# 程序的框架。
using System;
Console . WriteLine ( "Hello world!" );
namespace YourNamespace
{
class YourClass
{
}
struct YourStruct
{
}
interface IYourInterface
{
}
delegate int YourDelegate ( );
enum YourEnum
{
}
namespace YourNestedNamespace
{
struct YourStruct
{
}
}
}
前面的示例對程序的入口點使用頂級語句。只有一個文件可以有頂級語句。程序的入口點是該文件中程序文本的第一個文本行。在本例中,該值為 Console . WriteLine ( "Hello world!" );。還可以創建一個名為 Main 的靜態方法作為程序的入口點,如以下示例所示:
// 一個 C# 程序的框架結構
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 )
{
Console . WriteLine ( "Hello world!" );
}
}
}
在這種情況下,程序從 Main 方法的左大括號開始,即 Console . WriteLine ( "Hello world!" );。
生成和運行 C# 程序
C# 是一種已編譯的語言。在大多數 C# 程序中,使用 dotnet build 命令將一組源文件編譯為二進制包。然後,使用 dotnet run 命令運行程序。(可以簡化此過程,因為 dotnet run 會在必要時編譯程序後再運行它)。這些工具支持豐富的配置選項和命令行開關。. dotnet NET SDK 中包含的命令行接口(CLI)提供了許多工具來生成和修改 C# 文件。
從 C# 14 和 .NET 10 開始,可以創建基於文件的程序,從而簡化 C# 程序的生成和運行。使用 dotnet run 命令運行包含在單個 *.cs 文件中的程序。例如,如果以下代碼片段存儲在名為 hello-world.cs 的文件中,可以通過鍵入 dotnet run hello-world.cs 運行它。
#!/usr/local/share/dotnet/dotnet run
Console . WriteLine ( "Hello, World!" );
程序的第一行包含 Unix shell 的 #! 序列。dotnet CLI 的位置可能因不同的發行版而異。在任何 Unix 系統上,如果對 C# 文件設置了 execute (+x) 權限,則可以從命令行運行 C# 文件:
./hello-world.cs
這些程序的源必須是單個文件,否則所有 C# 語法都有效。可以將基於文件的程序用於小型命令行實用工具、原型或其他試驗。基於文件的程序允許預處理器指令配置生成系統。
表達式和語句
C# 程序是通過表達式和語句來構建的。表達式會生成一個值,而語句則會執行一項操作:
表達式是計算為單個值的值、變量、運算符和方法調用的組合。表達式生成結果,可在預期值的位置使用。以下示例是表達式:
- 42 // (文本值)
- x + y // (算術運算)
- Math . Max ( a , b ) // (方法調用)
- condition ? trueValue : falseValue // (條件表達式)
- new Person ( "John" ) // (對象創建)
語句是執行操作的完整指令。語句不返回值;相反,它們控制程序流、聲明變量或執行作。以下示例是陳述:
- int x = 42; // (申報單)
- Console . WriteLine ( "Hello" ); // (表達式語句 - 封裝方法調用表達式)
- if ( condition ) { / code / } // (條件語句)
- return result; // (return 語句)
關鍵區別:表達式計算結果為值,而語句執行操作。某些構造(如方法調用)可以是兩者。例如,Math . Max ( a , b ) 在 int result = Math . Max ( a , b); 中時是一個表達式,但單獨寫成 Math . Max ( a , b ); 時,它是一條表達式語句。
相關部分
您可以在基礎指南的 “類型” 部分中瞭解到這些程序元素的相關內容:
- 類
- 結構
- 命名空間
- 接口
- 枚舉
- 委託
主函數(Main ( ))和命令行參數
“Main” 方法是 C# 應用程序的入口點。當應用程序啓動時,Main 方法是首先被調用的方法。
在 C# 程序中只能有一個入口點。如果您的程序中有多個類都具有 Main 方法,那麼您必須使用 “StartupObject”(啓動對象)編譯選項來指定要作為入口點使用的 Main 方法。以下示例會將命令行參數的數量作為其第一個操作來顯示:
class TestClass
{
static void Main ( string [ ] args )
{
Console . WriteLine ( args . Length );
}
}
您還可以在一個文件中使用頂級語句作為應用程序的入口點。與主方法一樣,頂級語句也可以返回值並訪問命令行參數。以下示例使用 foreach 循環使用 args 變量顯示命令行參數,並在程序結束時返回一個成功代碼(0):如果一個程序不止有一個具有主方法的類,您必須使用 StartupObject 編譯器選項編譯您的程序,以指定要作為入口點使用的主方法。以下示例在執行的第一個操作中顯示命令行參數的數量:
using System . Text;
StringBuilder sb = new ( );
sb . AppendLine ( "以下的參數被傳遞了:" );
foreach ( var arg in args )
{
sb . AppendLine ( $"參數 = {arg}" );
}
Console . WriteLine ( sb . ToString ( ) );
return 0;
從 C# 14 版本開始,程序可以是基於文件的程序,即一個單獨的文件包含整個程序內容。要運行基於文件的程序,可以使用命令 “dotnet run <文件名.cs>”,或者在第一行使用 “#!/usr/local/share/dotnet/dotnet run” 這樣的指令(僅限於 Unix 命令行環境)。
概述
- Main 方法是可執行程序的入口點;它是程序控制的起始點和終止點。
- Main 方法必須聲明在類或結構體內部。其外部類可以是靜態的。
- Main 方法必須是 static 的。
- Main 方法可以具有任何訪問修飾符(除了 file 修飾符)。
- Main 方法可以具有 void、int、Task 或 Task < int > 這樣的返回類型。
- 只有當 Main 方法返回 Task 或 Task < int > 時,其聲明中才可以包含 async 修飾符。此規則明確排除了 async void Main 方法。
- Main 方法可以帶有或不帶有包含命令行參數的 string [ ] 參數。使用 Visual Studio 創建 Windows 應用程序時,您可以手動添加該參數,或者使用 GetCommandLineArgs ( ) 方法獲取命令行參數。參數以零索引的命令行參數形式讀取。與 C 和 C++ 不同,程序的名稱在 args 數組中不被視為第一個命令行參數,而是 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 ) { }
添加了 “async” 和 “Task” 以及 “Task < int >” 的返回類型後,當控制枱應用程序需要在 “Main” 方法中啓動並等待異步操作時,程序代碼就會變得更加簡潔。
Main 函數的返回值
您可以通過以下任一方式在主函數中定義該方法,從而從該函數返回一個整數:
| 主聲明 | 主方法代碼 |
|---|---|
| static int Main ( ) | 不使用 args 或 await |
| static int Main ( string [ ] args ) | 使用 args 但不使用 await |
| static async Task < int > Main ( ) | 使用 await 但不使用 args |
| static async Task < int > Main ( string [ ] args ) | 使用 args 並使用 await |
如果主方法的返回值不被使用,返回 void 或 Task 可以使代碼稍微簡潔一些。
| 主聲明 | 主方法代碼 |
|---|---|
| static void Main ( ) | 不使用 args 或 await |
| static void Main ( string [ ] args ) | 使用 args 但不使用 await |
| static async Task Main ( ) | 使用 await 但不使用 args |
| static async Task Main ( string [ ] args ) | 使用 args 並使用 await |
然而,返回 int 或 Task < int > 可使程序向調用可執行文件的其他程序或腳本傳遞狀態信息。
以下示例展示瞭如何獲取進程的退出代碼。
此示例使用了 .NET Core 命令行工具。
通過運行 “dotnet new console” 命令創建一個新的應用程序。在 “Program.cs” 文件中修改 “Main” 方法如下:
class MainReturnValTest
{
static int Main ( )
{
// ……
return 0;
}
}
記得將此程序保存為 “MainReturnValTest.cs”。
在 Windows 系統中執行程序時,Main 函數返回的任何值都會被存儲在一個環境變量中。可以通過批處理文件中的 ERRORLEVEL 或 PowerShell 中的 $LastExitCode 來獲取該環境變量的值。
您可以使用 dotnet CLI 的 dotnet build 命令來構建該應用程序。
接下來,創建一個 PowerShell 腳本來運行該應用程序並顯示結果。將以下代碼粘貼到一個文本文件中,並將其保存為名為 “test.ps1” 的文件,存放在包含項目文件的文件夾中。通過在 PowerShell 提示符處輸入 “test.ps1” 來運行此 PowerShell 腳本。
因為代碼返回了 0,所以批處理文件會報告成功。然而,如果您將 MainReturnValTest.cs 文件中的返回值更改為非 0 值,然後重新編譯程序,之後執行 PowerShell 腳本時就會報告失敗。
dotnet run
if ( $LastExitCode -eq 0 )
{
Write-Host "成功執行"
}
else
{
Write-Host "執行有錯誤"
}
Write-Host "返回值 = " $LastExitCode
// 成功執行
// 返回值 = 0
異步 Main 函數返回值
當您為 Main 函數聲明異步返回值時,編譯器會生成在 Main 函數中調用異步方法的樣板代碼:
class Program
{
static async Task < int > Main ( string [ ] args )
{
await AsyncConsoleWork ( );
}
private static async Task < int > AsyncConsoleWork ( )
{
return 0;
}
}
在兩個例子中,程序的主要部分都在 AsyncConsoleWork ( ) 方法的內部。
將 Main 函數聲明為異步的一個優點在於,編譯器總是能生成正確的代碼。
當應用程序入口點返回一個 Task 或 Task < int > 類型時,編譯器會生成一個新的入口點,該入口點會調用應用程序代碼中聲明的入口方法。假設這個入口點被命名為 $GeneratedMain,那麼編譯器會為這些入口點生成以下代碼:
- static Task Main ( ) 會導致編譯器生成類似於以下的代碼:private static void $GeneratedMain ( ) => Main ( ) . GetAwaiter ( ) . GetResult ( );
- static Task Main ( string [ ] ) 會導致編譯器生成類似於以下的代碼:private static void $GeneratedMain ( string [ ] args ) => Main ( args ) . GetAwaiter ( ) . GetResult ( );
- static Task < int > Main ( ) 會導致編譯器生成類似於以下的代碼:private static int $GeneratedMain ( ) => Main ( ) . GetAwaiter ( ) . GetResult ( );
- static Task < int > Main ( string [ ] ) 會導致編譯器生成類似於以下的代碼:private static int $GeneratedMain ( string [ ] args ) => Main ( args ) . GetAwaiter ( ) . GetResult ( );
注意:如果在主方法上使用了 “async” 修飾符,編譯器將會生成相同的代碼。
命令行參數
您可以通過以下任一方式在 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 |
如果不需要使用這些參數,可以在方法聲明中省略 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 窗體應用程序中訪問命令行參數。要在 Windows 窗體應用程序的 Main 方法聲明中啓用命令行參數,您必須手動修改 Main 的聲明。Windows 窗體設計器生成的代碼在創建 Main 時沒有輸入參數。
Main 方法的參數是一個字符串數組,該數組代表命令行參數。通常,您可以通過測試“長度”屬性來確定參數是否存在,例如:
if ( args . Length == 0 )
{
Console . WriteLine ("請輸入一個數字參數。" );
return 1;
}
提示:“args” 數組不能為 null。因此,在不進行 null 檢查的情況下訪問其 “Length” 屬性是安全的。
您還可以通過使用 Convert 類或 Parse 方法將字符串參數轉換為數值類型。例如,以下語句使用 Parse 方法將字符串轉換為一個 long:
long num = Int64 . Parse ( args [ 0 ] );
此外,還可以使用 C# 中的 long 類型(它與 Int64 類型等價):
long num = long . Parse ( args [ 0 ] );
您還可以使用 Convert 類的 ToInt64 方法來實現相同的功能:
long num = Convert . ToInt64 ( s );
提示:解析命令行參數可能會比較複雜。不妨考慮使用 “System . CommandLine” 庫(目前處於測試階段)來簡化這一過程。
以下示例展示瞭如何在控制枱應用程序中使用命令行參數。該應用程序在運行時接收一個參數,將該參數轉換為整數,並計算該數字的階乘。如果未提供任何參數,該應用程序會發出一條消息,説明程序的正確使用方法。
要從命令提示符窗口編譯並運行該應用程序,請按照以下步驟操作:
-
將以下代碼粘貼到任何文本編輯器中,然後將其保存為名為 “Factorial.cs” 的文本文件。
public class LeiFF { public static long JCH ( int n ) { // 對無效輸入進行測試 if ((n < 0) || (n > 20)) { return -1; } // 採用迭代方式而非遞歸方式計算階乘 long JG = 1; for (int i = 1; i <= n; i++) { JG *= i; } return JG; } } public class LeiMain { private static int Main(string[] args) { if (args.Length == 0) { Console.WriteLine("請輸入一個數字參數。"); Console.WriteLine("使用説明:階乘 < int >"); return 1; } bool b = int.TryParse(args[0], out int z); if (b == false) { Console.WriteLine("請輸入一個數字參數。"); Console.WriteLine("使用説明:階乘 < int >"); return 1; } long JG = LeiFF.JCH(z); if (JG == -1) { Console.WriteLine("輸入的值必須大於等於 0 且小於等於 20。"); return 0; } else { Console.WriteLine($"方法輸出以下內容:{z} 的階乘:{JG}。"); return 0; } } }在主方法的開頭,程序會檢查輸入參數是否未被提供,通過比較 args 參數的長度是否為 0 來進行判斷。如果沒有找到任何參數,則會顯示幫助信息。
如果提供了參數(即 args . Length > 0),程序會嘗試將輸入參數轉換為數字。如果參數不是數字,則此示例會拋出異常。
在計算完階乘(將其存儲在類型為 long 的 JG 變量中)後,會根據 result 變量的值打印詳細的結果。
- 在 “開始” 屏幕或 “開始” 菜單中,打開一個 Visual Studio 開發者命令提示符窗口,然後導航至包含您所創建文件的文件夾。
- 要編譯該應用程序,請輸入以下命令:
dotnet build
如果您的應用程序沒有編譯錯誤,就會生成一個名為 “C__命令行的階乘.dll” 的二進制文件。 - 請輸入以下命令來計算 3 的階乘:
“dotnet run -- 3” - 如果在命令行中將數字 3 作為程序的參數輸入,程序的輸出結果為:3 的階乘是 6。
頂級語句 - 沒有 Main 方法的程序
在控制枱應用程序項目中,您不必顯式包含一個 Main 方法。相反,您可以使用頂級語句功能來減少需要編寫的代碼量。
頂級語句允許您直接在文件的根目錄編寫可執行代碼,無需將代碼封裝在類或方法中。這意味着您可以創建程序,而無需像以往那樣創建 Program 類和 Main 方法。在這種情況下,編譯器會為應用程序生成一個 Program 類和一個入口點方法。生成的方法名稱不是 Main,這是實現細節,您的代碼無法直接引用。
這是一個完整的 C# 程序的 Program.cs 文件:
Console . WriteLine ( "Hello World!" );
頂級語句讓您能夠編寫簡單的程序,用於諸如 Azure Functions 和 GitHub Actions 這樣的小型實用工具。它們還使新的 C# 程序員更容易開始學習和編寫代碼。
以下各節將説明關於頂級語句您可以做什麼以及不能做什麼的規則。
僅一個頂級文件
應用程序必須只有一個入口點。一個項目中只能有一個包含頂級語句的文件。在一個項目中將頂級語句放在多個文件中會導致以下編譯錯誤:
警告 CS8802:只能有一個編譯單元包含頂級語句。
一個項目可以有任意數量的源代碼文件,這些文件沒有頂級語句。
沒有其他入口點
您可以顯式編寫一個 Main 方法,但它不能作為入口點發揮作用。編譯器會發出以下警告:
警告 CS7022:程序的入口點是全局代碼;忽略 'Main ( )' 入口點。
在包含頂級語句的項目中,您不能使用 -main 編譯器選項來選擇入口點,即使該項目具有一個或多個 Main 方法。
using 指令
對於包含頂級語句的單個文件,using 指令必須位於該文件的開頭,例如:
using System . Text;
StringBuilder sb = new ( );
sb . AppendLine ( "傳遞了如下參數:" );
foreach ( var arg in args )
{
sb . AppendLine ( $"參數 = {arg}" );
}
Console . WriteLine ( sb . ToString ( ) );
return 0;
全局命名空間(global namespace)
頂層語句默認處於全局命名空間中。
命名空間與類型定義
包含頂級語句的文件還可以包含命名空間和類型定義,但這些內容必須在頂級語句之後出現。例如:
MyClass . FFTest ( );
MyNamespace . MyClass . FFMy ( );
public class MyClass
{
public static void FFTest ( )
{
Console . WriteLine ( "Hello World!" );
}
}
namespace MyNamespace
{
class MyClass
{
public static void FFMy ( )
{
Console . WriteLine ( "Hello World from MyNamespace . MyClass . FFMy!" );
}
}
}
參數
頂層語句可以引用 “args” 變量來訪問輸入的任何命令行參數。該 “args” 變量永遠不會為空,但如果未提供命令行參數,則其長度為零。例如:
if ( args . Length > 0 )
{
foreach ( var arg in args )
{
Console . WriteLine ( $"參數 = {arg}" );
}
}
else
{
Console . WriteLine ( "沒有參數" );
}
await
您可以使用 “await” 關鍵字來調用異步方法。例如:
Console . WriteLine ( “Hello ” );
await Task . Delay ( 5000 );
Console . WriteLine ( “World!” );
進程的退出代碼(Exit code)
若要在應用程序結束時返回一個整數值,請像在返回整數的 Main 方法中那樣使用 return 語句。例如:
string? s = Console . ReadLine ( );
int Zhi = int . Parse ( s ?? "-1" );
return Zhi;
隱式入口點方法
編譯器會生成一個方法,作為項目中具有頂級語句的程序的入口點。該方法的簽名取決於頂級語句中是否包含 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 ) |
從 C# 14 開始,程序可以是基於文件的程序,即單個文件包含整個程序。使用命令 “dotnet run <文件.cs>” 或在第一行使用 “#!/usr/local/share/dotnet/dotnet run”(僅限 Unix shells 環境)來運行基於文件的程序。