教程:生成基于文件的 C# 程序

重要

基于文件的应用是 .NET 10 的一项功能,它以预览版提供。 某些信息与可能在发布前修改的预发行版产品有关。 Microsoft对此处提供的信息不作任何明示或暗示的保证。

基于文件的应用 是包含在单个 *.cs 文件中的程序,该文件在没有相应的项目 (*.csproj) 文件的情况下生成和运行。 基于文件的应用非常适合学习 C#,因为它们的复杂性较低:整个程序存储在单个文件中。 基于文件的应用也可用于生成命令行实用工具。 在 Unix 平台上,可以使用 (shebang) 指令运行 #! 基于文件的应用。

在本教程中,你将:

  • 创建基于文件的程序。
  • 添加 Unix shebang (#!) 支持。
  • 读取命令行参数。
  • 处理标准输入。
  • 编写 ASCII 艺术输出。
  • 处理命令行参数。
  • 使用分析的命令行结果。
  • 测试最终应用程序。

构建一个基于文件的程序,以 ASCII 艺术的形式写入文本。 该应用包含在单个文件中,使用实现某些核心功能的 NuGet 包。

先决条件

创建基于文件的程序

  1. 打开 Visual Studio Code 并创建一个名为 AsciiArt.cs的新文件。 输入以下文本:

    Console.WriteLine("Hello, world!");
    
  2. 保存文件。 然后,在 Visual Studio Code 中打开集成终端并键入:

    dotnet run AsciiArt.cs
    

首次运行此程序时, dotnet 主机会从源文件生成可执行文件,将生成项目存储在临时文件夹中,然后运行创建的可执行文件。 可以通过再次键入 dotnet run AsciiArt.cs 来验证此体验。 这一次, dotnet 主机确定可执行文件是最新的,并运行可执行文件而不再次生成它。 看不到任何生成输出。

前面的步骤演示基于文件的应用不是脚本文件。 它们是使用临时文件夹中生成的项目文件的 C# 源文件。 生成程序时显示的输出行之一应如下所示(在 Windows 上):

AsciiArt succeeded (7.3s) → AppData\Local\Temp\dotnet\runfile\AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc\bin\debug\AsciiArt.dll

在 unix 平台上,输出文件夹类似于:

AsciiArt succeeded (7.3s) → Library/Application Support/dotnet/runfile/AsciiArt-85c58ae0cd68371711f06f297fa0d7891d0de82afde04d8c64d5f910ddc04ddc/bin/debug/AsciiArt.dll

该输出会告知你放置临时文件和生成输出的位置。 在本教程中,每当编辑源文件时,主机都会 dotnet 在运行可执行文件之前更新可执行文件。

基于文件的应用是常规 C# 程序。 唯一的限制是必须在一个源文件中写入它们。 可以使用顶级语句或经典 Main 方法作为入口点。 可以声明任何类型:类、接口和结构。 可以在基于文件的程序中构建算法,就像在任何 C# 程序中一样。 甚至可以声明多个命名空间来组织代码。 如果发现基于文件的程序对于单个文件来说太大,则可以将其转换为基于项目的程序,并将源拆分为多个文件。 基于文件的应用是一种出色的原型制作工具。 可以开始尝试以最少的开销来证明概念和生成算法。

Unix shebang (#!) 支持

注释

#! 指令的支持仅适用于 unix 平台。 Windows 没有类似的指令可以直接执行 C# 程序。 在 Windows 上,必须在命令行上使用 dotnet run

在 unix 上,可以直接运行基于文件的应用,在命令行而不是命令行 dotnet run上键入源文件名称。 需要进行两项更改:

  1. 设置对源文件 的执行 权限:

    chmod +x AsciiArt.cs
    
  2. 添加 shebang (#!) 指令作为文件的第一行 AsciiArt.cs

    #!/usr/local/share/dotnet/dotnet run
    

dotnet在不同 unix 安装上的位置可能不同。 使用命令 whence dotnet 在本地环境中本地 dotnet 主机。

进行这两项更改后,可以直接从命令行运行程序:

./AsciiArt.cs

如果愿意,可以删除扩展,以便可以改为键入 ./AsciiArt 。 即使使用 Windows, #! 也可以将源文件添加到源文件。 Windows 命令行不支持 #!,但 C# 编译器允许在所有平台上基于文件的应用中使用该指令。

读取命令行参数

现在,将命令行上的所有参数写入输出。

  1. 将当前内容 AsciiArt.cs 替换为以下代码:

    if (args.Length > 0)
    {
        string message = string.Join(' ', args);
        Console.WriteLine(message);
    }
    
  2. 可以通过键入以下命令来运行此版本:

    dotnet run AsciiArt.cs -- This is the command line.
    

    -- 选项指示应将所有以下命令参数传递给 AsciiArt 程序。 This is the command line.参数作为字符串数组传递,其中每个字符串都是一个单词:This、、isthecommandline.

此版本演示了以下新概念:

  • 命令行参数使用预定义变量 args传递给程序。 变量 args 是字符串数组: string[]. 如果长度 args 为 0,则表示未提供任何参数。 否则,参数列表中的每个单词都存储在数组中的相应条目中。
  • 该方法使用 string.Join 指定的分隔符将多个字符串联接到单个字符串中。 在这种情况下,分隔符是一个空格。
  • Console.WriteLine 将字符串写入标准输出控制台,后跟一个新行。

处理标准输入

这可以正确处理命令行参数。 现在,添加代码来处理从标准输入(stdin)而不是命令行参数读取输入。

  1. 将以下 else 子句添加到 if 前面代码中添加的语句:

    else
    {
        while (Console.ReadLine() is string line && line.Length > 0)
        {
            Console.WriteLine(line);
        }
    }
    

    前面的代码将读取控制台输入,直到读取空行或 null 读取。 (如果输入流通过键入 Console.ReadLine 关闭,该方法null将返回

  2. 通过在同一文件夹中创建新的文本文件来测试读取标准输入。 命名文件 input.txt 并添加以下行:

    Hello from ...
    dotnet!
    
    You can create
    file-based apps
    in .NET 10 and
    C# 14
    
    Have fun writing
    useful utilities
    

    使行保持短,以便在添加功能以使用 ASCII 艺术时正确设置格式。

  3. 再次运行程序。

    使用 bash:

    cat input.txt | dotnet run AsciiArt.cs
    

    或者,使用 PowerShell:

    Get-Content input.txt | dotnet run AsciiArt.cs
    

现在,程序可以接受命令行参数或标准输入。

写入 ASCII Art 输出

接下来,添加支持 ASCII 艺术 、彩色.Console 的包。 若要将包添加到基于文件的程序中,请使用该 #:package 指令。

  1. 在AsciiArt.cs文件中的 #! 指令后面添加以下指令:

    #:package Colorful.Console@1.2.15
    

    重要

    上次更新本教程时,该版本 1.2.15 是包的 Colorful.Console 最新版本。 检查包的 NuGet 页 以获取最新版本,以确保将包版本与最新的安全修补程序一起使用。

  2. 更改调用 Console.WriteLine 以改用方法的 Colorful.Console.WriteAscii 行:

    async Task WriteAsciiArt(AsciiMessageOptions options)
    {
        foreach (string message in options.Messages)
        {
            Colorful.Console.WriteAscii(message);
            await Task.Delay(options.Delay);
        }
    }
    
  3. 运行程序,你会看到 ASCII 艺术输出而不是回显文本。

进程命令选项

接下来,让我们添加命令行分析。 当前版本将每个单词写入不同的输出行。 添加的命令行参数支持两个功能:

  1. 对应用一行编写的多个单词进行引号引用:

    AsciiArt.cs "This is line one" "This is another line" "This is the last line"
    
  2. 添加一个选项 --delay 以在每个行之间暂停:

    AsciiArt.cs --delay 1000
    

用户应能够同时使用这两个参数。

大多数命令行应用程序都需要分析命令行参数才能有效地处理选项、命令和用户输入。 System.CommandLine提供用于处理命令、子命令、选项和参数的综合功能,使你能够专注于应用程序执行的作,而不是分析命令行输入的机制。

System.CommandLine 库提供几个关键优势:

  • 自动帮助文本生成和验证。
  • 支持 POSIX 和 Windows 命令行约定。
  • 内置选项卡完成功能。
  • 跨应用程序进行一致的分析行为。
  1. System.CommandLine添加包。 在现有包指令后面添加此指令:

    #:package System.CommandLine@2.0.0-beta6
    

    重要

    2.0.0-beta6上次更新本教程时,版本是最新版本。 如果有较新版本可用,请使用最新版本来确保具有最新的安全包。 检查包的 NuGet 页 以获取最新版本,以确保将包版本与最新的安全修补程序一起使用。

  2. 在文件顶部添加必要的 using 语句(在和#!指令之后#:package):

    using System.CommandLine;
    using System.CommandLine.Parsing;
    
  3. 定义延迟选项和消息参数。 添加以下代码以创建 CommandLine.OptionCommandLine.Argument 对象来表示命令行选项和参数:

    Option<int> delayOption = new("--delay")
    {
        Description = "Delay between lines, specified as milliseconds.",
        DefaultValueFactory = parseResult => 100
    };
    
    Argument<string[]> messagesArgument = new("Messages")
    {
        Description = "Text to render."
    };
    

    在命令行应用程序中,选项通常以(双短划线)开头 -- ,并且可以接受参数。 该 --delay 选项接受一个整数参数,该参数指定延迟(以毫秒为单位)。 定义 messagesArgument 在将选项分析为文本之后的任何剩余令牌的方式。 每个标记将成为数组中的一个单独的字符串,但文本可以引用以在一个标记中包含多个单词。 例如, "This is one message" 变为单个令牌,而 This is four tokens 变为四个单独的令牌。

    前面的代码定义选项的参数类型 --delay ,参数是值的数组 string 。 此应用程序只有一个命令,因此请使用 根命令

  4. 创建根命令,并使用选项和参数对其进行配置。 将参数和选项添加到根命令:

    RootCommand rootCommand = new("Ascii Art file-based program sample");
    
    rootCommand.Options.Add(delayOption);
    rootCommand.Arguments.Add(messagesArgument);
    
  5. 添加代码以分析命令行参数并处理任何错误。 此代码验证命令行参数,并将分析的参数存储在对象中 System.CommandLine.ParseResult

    ParseResult result = rootCommand.Parse(args);
    foreach (ParseError parseError in result.Errors)
    {
        Console.Error.WriteLine(parseError.Message);
    }
    if (result.Errors.Count > 0)
    {
        return 1;
    }
    

前面的代码验证所有命令行参数。 如果验证失败,错误将写入控制台,并且应用退出。

使用分析的命令行结果

现在,完成应用以使用已分析的选项并写入输出。 首先,定义用于保存已分析选项的记录。 基于文件的应用可以包括类型声明,例如记录和类。 它们必须位于所有顶级语句和本地函数之后。

  1. 添加声明 record 以存储消息和延迟选项值:

    public record AsciiMessageOptions(string[] Messages, int Delay);
    
  2. 在记录声明之前添加以下本地函数。 此方法处理命令行参数和标准输入,并返回新的记录实例:

    async Task<AsciiMessageOptions> ProcessParseResults(ParseResult result)
    {
        int delay = result.GetValue(delayOption);
        List<string> messages = [.. result.GetValue(messagesArgument) ?? Array.Empty<string>()];
    
        if (messages.Count == 0)
        {
            while (Console.ReadLine() is string line && line.Length > 0)
            {
                Colorful.Console.WriteAscii(line);
                await Task.Delay(delay);
            }
        }
        return new([.. messages], delay);
    }
    
  3. 创建本地函数以使用指定的延迟编写 ASCII 艺术。 此函数在记录中写入每个消息,并在每条消息之间指定延迟:

    async Task WriteAsciiArt(AsciiMessageOptions options)
    {
        foreach (string message in options.Messages)
        {
            Colorful.Console.WriteAscii(message);
            await Task.Delay(options.Delay);
        }
    }
    
  4. if 前面编写的子句替换为处理命令行参数并编写输出的以下代码:

    var parsedArgs = await ProcessParseResults(result);
    
    await WriteAsciiArt(parsedArgs);
    return 0;
    

你创建了一个 record 类型,该类型为分析的命令行选项和参数提供结构。 新的本地函数创建记录的实例,并使用记录来写入 ASCII 艺术输出。

测试最终应用程序

通过运行多个不同的命令来测试应用程序。 如果遇到问题,下面是与所生成内容进行比较的已完成示例:

#!/usr/local/share/dotnet/dotnet run

#:package Colorful.Console@1.2.15
#:package System.CommandLine@2.0.0-beta6

using System.CommandLine;
using System.CommandLine.Parsing;

Option<int> delayOption = new("--delay")
{
    Description = "Delay between lines, specified as milliseconds.",
    DefaultValueFactory = parseResult => 100
};

Argument<string[]> messagesArgument = new("Messages")
{
    Description = "Text to render."
};

RootCommand rootCommand = new("Ascii Art file-based program sample");

rootCommand.Options.Add(delayOption);
rootCommand.Arguments.Add(messagesArgument);

ParseResult result = rootCommand.Parse(args);
foreach (ParseError parseError in result.Errors)
{
    Console.Error.WriteLine(parseError.Message);
}
if (result.Errors.Count > 0)
{
    return 1;
}

var parsedArgs = await ProcessParseResults(result);

await WriteAsciiArt(parsedArgs);
return 0;

async Task<AsciiMessageOptions> ProcessParseResults(ParseResult result)
{
    int delay = result.GetValue(delayOption);
    List<string> messages = [.. result.GetValue(messagesArgument) ?? Array.Empty<string>()];

    if (messages.Count == 0)
    {
        while (Console.ReadLine() is string line && line.Length > 0)
        {
            // <WriteAscii>
            Colorful.Console.WriteAscii(line);
            // </WriteAscii>
            await Task.Delay(delay);
        }
    }
    return new([.. messages], delay);
}

async Task WriteAsciiArt(AsciiMessageOptions options)
{
    foreach (string message in options.Messages)
    {
        Colorful.Console.WriteAscii(message);
        await Task.Delay(options.Delay);
    }
}

public record AsciiMessageOptions(string[] Messages, int Delay);

在本教程中,你学习了生成基于文件的程序,并在其中在单个 C# 文件中生成程序。 这些程序不使用项目文件,并且可以在 unix 系统上使用该 #! 指令。 学习者可以在尝试 联机教程 之后以及构建基于项目的大型应用之前创建这些程序。 基于文件的应用也是命令行实用工具的绝佳平台。