接下来几篇文章,我将带大家一起过一篇重要的 C# 基础,本文先从拆解一个简单的 C# 应用程序开始。
01 入口函数与应用程序对象
使用上节课讲的方法,我们在终端使用命令行创建一个 C# 项目:
dotnet new console -n SimpleConsoleApp
code .
查看生成的 Program.cs
文件是这样的:
using System;
namespace SimpleConsoleApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
C# 要求所有的数据成员和方法都包含在一个类型定义中(这里的类型包括类、接口、结构、枚举、委托)。所以上面这段代码就是一个最简单的 C# 应用程序。
在这段通过模板生成的代码中,使用了 System
命名空间中的 Console
类。这个类有一个静态的 WriteLine
方法,它向标准输出发送一个文本字符串和回车。
这里的 Main
函数/方法被称为入口函数,任何可执行的项目(如控制台应用)都必须至少有一个入口函数。Main
函数必须是静态的,它的签名不能随意修改,包括函数名、参数个数、参数类型和返回值,否则运行时会报错。
通常,我们把包含入口函数的类称为应用程序对象。默认情况,使用模板创建的可执行项目的应用程序对象是一个名为 Program
的类,你也可以修改为其它的类名。
一个可执行的应用程序可能会有多个应用程序对象(一般用于单元测试),但编译器必须知道哪个 Main
方法应该被用作入口点,这可以通过项目文件中的 <StartupObject>
元素来指定。
02 入口函数的多种签名
默认情况下,模板生成的 Main 函数,它的返回值是 void
,它的参数是一个字符串数组。然而,这并不是 Main
函数的唯一形式。这的签名可以是下面这几种:
static void Main()
static int Main()
static void Main(string[])
static int Main(string[])
从 C# 7.1 开始,Main
方法支持异步,所以它的签名也可以是上面的异步形式:
static Task Main()
static Task<int> Main()
static Task Main(string[])
static Task<int> Main(string[])
首先,当 Main
方法执行完成后,如果你想告诉外部调用程序执行是否成功结束,你需要返回一个 int
类型的数据。按照约定,0
表示成功结束。
其次,如果你是否需要处理用户提供的命令行的参数,则需要用到 Main
方法的字符串数组参数。在运行 dotnet run
或 dotnet xxx.dll
时,后面可以追加多个参数,使用空格分隔,它会被存入 Main
方法的字符串数组参数中。
最后,方法的访问修饰符(如 public
、private
等)不属于方法的签名,所以 Main
方法支持任意访问修饰符,默认是 private
(当一个类中的方法没有写访问修饰符时,默认就是 private
)。
03 使用顶层语句
虽然在 C# 9.0 之前,所有的 C# 应用程序都必须有一个
方法。C# 9.0 引入了顶层(Top Level)语句,这使得 C# 应用程序的入口不再需要一些形式化的命名空间、应用程序对象(Main
Program
)和 Main
函数,这些都可以省略。我们上面的 C# 代码,可以简写成这样:
using System;
Console.WriteLine("Hello World!");
这和之前的写法效果是完全一样的。顶层语句只是省略了命名空间、类和入口函数,它依然可以使用字符串数组 args
参数,依然可以通过 return
返回一个整型数。
但,一个应用程序中只能有一个文件可以使用顶层语句。当使用顶层语句时,程序不能声明其它入口函数。
可以在顶层语句中定义方法,它会变成顶层语句的局部函数(本地函数)。也可以在顶层语句中声明额外的类型,但这些语句必须在顶层语句之后声明,否则会导致编译错误。
我们不妨来看一下上面顶层代码所生成的元数据中的 TypeDef
信息:
TypeDef #1 (02000002)
-------------------------------------------------------
TypDefName: <Program>$ (02000002)
Flags : [NotPublic] [AutoLayout] [Class] [Abstract] [Sealed] [AnsiClass] [BeforeFieldInit] (00100180)
Extends : 0100000D [TypeRef] System.Object
Method #1 (06000001) [ENTRYPOINT]
-------------------------------------------------------
MethodName: <Main>$ (06000001)
Flags : [Private] [Static] [HideBySig] [ReuseSlot] (00000091)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
1 Arguments
Argument #1: SZArray String
1 Parameters
(1) ParamToken : (08000001) Name : args flags: [none] (00000000)
可以看到,虽然顶层语言中省略了类和入口函数,但编译器在编译的时候会自动帮我们加上这些信息。
04 返回应用程序错误代码
虽然绝大多数的 Main
方法(或顶层语句)将 void
作为返回值,但为了使 C# 与其他语言保持一致,也支持返回 int
(或 Task<int>
)类型的值,这个返回值一般用于表示错误级别或错误代码。
按照惯例,返回值 0
表示程序已经成功终止,而其它的值(如 -1
)则表示程序执行发生了错误。请注意,即使你的 Main
方法返回的是 void
,或顶层语句没有明确的返回值,成功结束后也会自动返回 0
值。
在 Windows 操作系统中,一个应用程序的返回值被存储在一个名为 %ERRORLEVEL%
的系统环境变量中。如果你要创建一个以编程方式启动另一个可执行程序的应用程序,你可以使用启动进程的 ExitCode
属性来获得 %ERRORLEVEL%
的值。
鉴于应用程序的返回值是在应用程序终止时传递给系统的,应用程序显然不可能在运行时获得并显示其最终的错误代码。然而,为了说明如何在程序终止时查看这个错误级别,我们来举个例子,先改下我们的代码,使它返回一个 -1
值。
using System;
Console.WriteLine("Hello World!");
// 返回一个任意的错误代码
return -1;
现在让我们在批处理文件中捕获程序的返回值。在项目文件夹中添加一个文本文件(使用 GB 2312 编码,不然中文会乱码),命名为 SimpleConsoleApp.cmd
。修改该文件内容如下:
@echo off
rem 一个捕获 SimpleConsoleApp.exe 返回值的批处理文件
dotnet run
@if "%ERRORLEVEL%" == "0" goto success
:fail
echo 应用程序执行失败
goto end
:success
echo 应用程序执行成功
goto end
:end
echo 返回值为:%ERRORLEVEL%
echo 结束
或者如果你熟悉 PowerShell,也可以将文件扩展名改为 .ps1
,然后修改文件内容为:
dotnet run
if ($LastExitCode -eq 0) {
Write-Host "应用程序执行成功"
} else {
Write-Host "应用程序执行失败"
}
Write-Host "返回值:"$LastExitCode
Write-Host "结束"
然后在命令行终端运行 .\SimpleConsleApp.ps1
:
PS D:\Samples\SimpleConsoleApp> .\SimpleConsleApp.ps1
Hello World!
应用程序执行失败
返回值为:-1
结束
我们的程序返回的是 -1
,你会看到打印消息是“应用程序执行失败”。
绝大多数 C# 应用程序都使用 void
作为入口函数的返回值,正如前文所说,它隐含地返回的错误代码为 0
。
05 命令行参数
现在我们已经更好地理解了 Main
方法或顶层语句的返回值,接下来让我们来看一下传入给入口函数的字符串数组。
我们先改一下程序代码,把 args
参数通过 for
循环打印出来:
using System;
Console.WriteLine("Hello World!");
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine("Arg {0}: {1}", i, args[i]);
}
注意,这个例子使用的是顶层语句,args
是关键字,代表入口函数的字符串数组参数。
然后在命令行终端运行:
PS D:\Samples\SimpleConsoleApp>dotnet run /arg0 -arg1
Hello World!
Arg 0: /arg0
Arg 1: -arg1
另外,你还可以使用 System.Environment
类的静态方法 GetCommandLineArgs()
访问命令行参数,这个方法的返回值是一个字符串的数组。不同的是,该字符串数组第一个值是应用程序本身的完整路径和名称,数组中的其余元素才是各个命令行参数。
using System;
Console.WriteLine("Hello World!");
string[] myArgs = Environment.GetCommandLineArgs();
for (int i = 0; i < myArgs.Length; i++)
{
Console.WriteLine("Arg {0}: {1}", i, myArgs[i]);
}
在命令行终端使用同样的参数运行:
PS D:\Samples\SimpleConsoleApp> dotnet run /arg0 -arg1
Hello World!
Arg 0: D:\Samples\SimpleConsoleApp\bin\Debug\net5.0\SimpleConsoleApp.dll
Arg 0: /arg0
Arg 1: -arg1
最后,如果你用 Visual Studio 调试程序,也可以设定命令行参数。右键项目,依次选择Properties - Debug
,在 Application arguments
设置程序运行的命令行参数:
这和前面的命令行中指定参数是一样的,在启动调试的时候,这些参数会被存入 args
数组参数。
在这里,我们只是简单地传入了一些命令行参数,并把它们直接打印出来。在实际应用中可能需要自己定义规则去对参数进行格式化和提取,比如约定只有 /
或 -
字符开头的参数才被视为有效的命令行参数。
06 小结
通过本文,我们学习了入口函数和应用程序对象的特点。任何可执行运用程序至少包含一个应用程序对象。入口函数有多种固定的签名形式,其它以外的签名都会导致程序报错。
我还们学习了顶层语句的使用和一些使用规则。顶层语句中可以声明方法,也可以定义额外的类型。通过查看元数据,我们知道顶层语句就是一种简写,编译器会自动加上类、入口函数的声明,它可以使用 args
参数,也可以有整型返回值。
最后,我们了解了一下命令行参数的使用,它可以通过字符串数组 args
参数来接收,也可以通过 Environment.GetCommandLineArgs()
来获取。
本文来自http://cnblogs.com/willick,经授权后发布,本文观点不代表个人技术分享立场,转载请联系原作者。