[.NET大牛之路 010] 拆解一个简单的 C# 程序

接下来几篇文章,我将带大家一起过一篇重要的 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 方法的字符串数组参数中。

最后,方法的访问修饰符(如 publicprivate 等)不属于方法的签名,所以 Main 方法支持任意访问修饰符,默认是 private(当一个类中的方法没有写访问修饰符时,默认就是 private)。

03 使用顶层语句

虽然在 C# 9.0 之前,所有的 C# 应用程序都必须有一个  Main 方法。C# 9.0 引入了顶层(Top Level)语句,这使得 C# 应用程序的入口不再需要一些形式化的命名空间、应用程序对象(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 设置程序运行的命令行参数:

[.NET大牛之路 010] 拆解一个简单的 C# 程序

这和前面的命令行中指定参数是一样的,在启动调试的时候,这些参数会被存入 args 数组参数。

在这里,我们只是简单地传入了一些命令行参数,并把它们直接打印出来。在实际应用中可能需要自己定义规则去对参数进行格式化和提取,比如约定只有 / 或 - 字符开头的参数才被视为有效的命令行参数。

06 小结

通过本文,我们学习了入口函数和应用程序对象的特点。任何可执行运用程序至少包含一个应用程序对象。入口函数有多种固定的签名形式,其它以外的签名都会导致程序报错。

我还们学习了顶层语句的使用和一些使用规则。顶层语句中可以声明方法,也可以定义额外的类型。通过查看元数据,我们知道顶层语句就是一种简写,编译器会自动加上类、入口函数的声明,它可以使用 args 参数,也可以有整型返回值。

最后,我们了解了一下命令行参数的使用,它可以通过字符串数组 args 参数来接收,也可以通过 Environment.GetCommandLineArgs() 来获取。

本文来自http://cnblogs.com/willick,经授权后发布,本文观点不代表Chaoqiang's Blog立场,转载请联系原作者。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

近期个人博客正在迁移中,原博客请移步此处,抱歉!