控制台应用程序Console Application

此教程将介绍 .NET Core 和 C# 语言的许多功能。This tutorial teaches you a number of features in .NET Core and the C# language. 你将了解:You’ll learn:

  • .NET Core 命令行接口 (CLI) 的基础知识The basics of the .NET Core Command Line Interface (CLI)
  • C# 控制台应用程序的结构The structure of a C# Console Application
  • 控制台 I/OConsole I/O
  • .NET 中文件 I/O API 的基础知识The basics of File I/O APIs in .NET
  • .NET 中基于任务的异步编程基础知识The basics of the Task-based Asynchronous Programming in .NET

你将生成一个应用程序,用于读取文本文件,然后将文本文件的内容回显到控制台。You’ll build an application that reads a text file, and echoes the contents of that text file to the console. 按配速大声朗读控制台输出。The output to the console is paced to match reading it aloud. 可以按“<”(小于)或“>”(大于)键加速或减速显示。You can speed up or slow down the pace by pressing the ‘<’ (less than) or ‘>’ (greater than) keys.

此教程将介绍许多功能。There are a lot of features in this tutorial. 我们将逐个生成这些功能。Let’s build them one by one.

系统必备Prerequisites

必须将计算机设置为运行 .NET Core。You’ll need to setup your machine to run .NET Core. 有关安装说明,请访问 .NET Core 页。You can find the installation instructions on the .NET Core page. 可以在 Windows、Linux、macOS 或 Docker 容器中运行此应用程序。You can run this application on Windows, Linux, macOS or in a Docker container. 必须安装常用的代码编辑器。You’ll need to install your favorite code editor.

创建应用程序Create the Application

第一步是新建应用程序。The first step is to create a new application. 打开命令提示符,然后新建应用程序的目录。Open a command prompt and create a new directory for your application. 将新建的目录设为当前目录。Make that the current directory. 在命令提示符处,键入命令 dotnet new consoleType the command dotnet new console at the command prompt. 这将为基本的“Hello World”应用程序创建起始文件。This creates the starter files for a basic "Hello World" application.

在开始进行修改之前,我们先来逐步了解一下如何运行简单的 Hello World 应用程序。Before you start making modifications, let’s go through the steps to run the simple Hello World application. 创建应用程序之后,在命令提示符处键入 dotnet restoreAfter creating the application, type dotnet restore at the command prompt. 此命令将运行 NuGet 包还原进程。This command runs the NuGet package restore process. NuGet 是 .NET 程序包管理器。NuGet is a .NET package manager. 此命令会下载项目缺少的所有依赖项。This command downloads any of the missing dependencies for your project. 由于这是一个新项目,尚无任何依赖项,因此首次运行只会下载 .NET Core 框架。As this is a new project, none of the dependencies are in place, so the first run will download the .NET Core framework. 执行这一初始步骤后,只需运行 dotnet restore,即可添加新的依赖项包,或更新任意依赖项的版本。After this initial step, you will only need to run dotnet restore when you add new dependent packages, or update the versions of any of your dependencies.

备注

从 .NET Core 2.0 SDK 开始,无需运行 dotnet restore,因为它由所有需要还原的命令隐式运行,如 dotnet newdotnet builddotnet runStarting with .NET Core 2.0 SDK, you don't have to run dotnet restore because it's run implicitly by all commands that require a restore to occur, such as dotnet new, dotnet build and dotnet run. 在执行显式还原有意义的某些情况下,例如 Azure DevOps Services 中的持续集成生成中,或在需要显式控制还原发生时间的生成系统中,它仍然是有效的命令。It's still a valid command in certain scenarios where doing an explicit restore makes sense, such as continuous integration builds in Azure DevOps Services or in build systems that need to explicitly control the time at which the restore occurs.

还原包后,运行 dotnet buildAfter restoring packages, you run dotnet build. 这将运行生成引擎,并创建应用程序可执行文件。This executes the build engine and creates your application executable. 最后,执行 dotnet run 来运行应用程序。Finally, you execute dotnet run to run your application.

简单的 Hello World 应用程序代码全都在 Program.cs 中。The simple Hello World application code is all in Program.cs. 使用常用文本编辑器打开此文件。Open that file with your favorite text editor. 我们将执行首轮更改。We’re about to make our first changes. 在此文件的最上面,你会看到 using 语句:At the top of the file, see a using statement:

using System;

此语句指示编译器,System 命名空间中的任何类型都在范围内。This statement tells the compiler that any types from the System namespace are in scope. 与你可能用过的其他面向对象的语言一样,C# 也使用命名空间来整理类型。Like other Object Oriented languages you may have used, C# uses namespaces to organize types. 此 Hello World 程序也一样。This Hello World program is no different. 你可以看到,此程序封闭在名称基于当前目录名的命名空间内。You can see that the program is enclosed in the namespace with the name based on the name of the current directory. 对于本教程,我们将命名空间的名称更改为 TeleprompterConsoleFor this tutorial, let's change the name of the namespace to TeleprompterConsole:

namespace TeleprompterConsole

读取和回显文件Reading and Echoing the File

要添加的第一项功能是读取文本文件,然后在控制台中显示全部文本。The first feature to add is the ability to read a text file and display all that text to the console. 首先,让我们来添加文本文件。First, let’s add a text file. 将此示例的 GitHub 存储库中的 sampleQuotes.txt 文件复制到项目目录中。Copy the sampleQuotes.txt file from the GitHub repository for this sample into your project directory. 这将用作应用程序脚本。This will serve as the script for your application. 如果需要有关如何下载本主题示例应用的信息,请参阅示例和教程主题中的说明。If you would like information on how to download the sample app for this topic, see the instructions in the Samples and Tutorials topic.

接下来,在 Program 类中添加以下方法(即 Main 方法的下方):Next, add the following method in your Program class (right below the Main method):

static IEnumerable<string> ReadFrom(string file)
{
    string line;
    using (var reader = File.OpenText(file))
    {
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

此方法使用两个新命名空间中的类型。This method uses types from two new namespaces. 为了让编译能够顺利进行,需要在文件的最上面添加以下两行代码:For this to compile you’ll need to add the following two lines to the top of the file:

using System.Collections.Generic;
using System.IO;

IEnumerable<T> 接口是在 System.Collections.Generic 命名空间中进行定义。The IEnumerable<T> interface is defined in the System.Collections.Generic namespace. File 类是在 System.IO 命名空间中进行定义。The File class is defined in the System.IO namespace.

这是一种称为“Iterator 方法”的特殊类型 C# 方法。This method is a special type of C# method called an Iterator method. 枚举器方法返回延迟计算的序列。Enumerator methods return sequences that are evaluated lazily. 也就是说,序列中的每一项是在使用序列的代码提出请求时生成。That means each item in the sequence is generated as it is requested by the code consuming the sequence. Enumerator 方法包含一个或多个 yield return 语句。Enumerator methods are methods that contain one or more yield return statements. ReadFrom 方法返回的对象包含用于生成序列中所有项的代码。The object returned by the ReadFrom method contains the code to generate each item in the sequence. 在此示例中,这涉及读取源文件中的下一行文本,然后返回相应的字符串。In this example, that involves reading the next line of text from the source file, and returning that string. 每当调用代码请求生成序列中的下一项时,代码就会读取并返回文件中的下一行文本。Each time the calling code requests the next item from the sequence, the code reads the next line of text from the file and returns it. 读取完整个文件时,序列会指示没有其他项。When the file is completely read, the sequence indicates that there are no more items.

还有两个 C# 语法元素你可能是刚开始接触。There are two other C# syntax elements that may be new to you. 此方法中的 using 语句用于管理资源清理。The using statement in this method manages resource cleanup. using 语句中初始化的变量(在此示例中,为 reader)必须实现 IDisposable 接口。The variable that is initialized in the using statement (reader, in this example) must implement the IDisposable interface. 该接口定义一个方法(Dispose),应在释放资源时调用此方法。That interface defines a single method, Dispose, that should be called when the resource should be released. 当快执行到 using 语句的右大括号时,编译器会生成此调用。The compiler generates that call when execution reaches the closing brace of the using statement. 编译器生成的代码可确保资源得到释放,即使代码块中用 using 语句定义的代码抛出异常,也不例外。The compiler-generated code ensures that the resource is released even if an exception is thrown from the code in the block defined by the using statement.

reader 变量是使用 var 关键字进行定义。The reader variable is defined using the var keyword. var 定义的是隐式类型本地变量。var defines an implicitly typed local variable. 也就是说,变量的类型是由分配给变量的对象的编译时类型决定的。That means the type of the variable is determined by the compile-time type of the object assigned to the variable. 此处,它为 OpenText(String) 方法的返回值,即 StreamReader 对象。Here, that is the return value from the OpenText(String) method, which is a StreamReader object.

现在,让我们在 Main 方法中填充用于读取文件的代码:Now, let’s fill in the code to read the file in the Main method:

var lines = ReadFrom("sampleQuotes.txt");
foreach (var line in lines)
{
    Console.WriteLine(line);
}

使用 dotnet run 运行程序,可以看到控制台中打印输出所有文本行。Run the program (using dotnet run) and you can see every line printed out to the console.

添加延迟和设置输出格式Adding Delays and Formatting output

现在的问题是,输出显示过快,无法大声朗读。What you have is being displayed far too fast to read aloud. 此时,需要为输出添加延迟。Now you need to add the delays in the output. 首先,将生成一些可实现异步处理的核心代码。As you start, you’ll be building some of the core code that enables asynchronous processing. 不过,在执行这些初始步骤时,将遵循一些反面模式。However, these first steps will follow a few anti-patterns. 反面模式会在你添加代码时在注释中指出,代码将在后面的步骤中进行更新。The anti-patterns are pointed out in comments as you add the code, and the code will be updated in later steps.

这部分包含两步操作。There are two steps to this section. 首先,将迭代器方法更新为返回单个字词,而不是整行文本。First, you’ll update the iterator method to return single words instead of entire lines. 为此,执行下面这些修改。That’s done with these modifications. 用以下代码替换 yield return line; 语句:Replace the yield return line; statement with the following code:

var words = line.Split(' ');
foreach (var word in words)
{
    yield return word + " ";
}
yield return Environment.NewLine;

接下来,需要修改对文件行的使用方式,并在写入每个字词后添加延迟。Next, you need to modify how you consume the lines of the file, and add a delay after writing each word. 用以下代码块替换 Main 方法中的 Console.WriteLine(line) 的语句:Replace the Console.WriteLine(line) statement in the Main method with the following block:

Console.Write(line);
if (!string.IsNullOrWhiteSpace(line))
{
    var pause = Task.Delay(200);
    // Synchronously waiting on a task is an
    // anti-pattern. This will get fixed in later
    // steps.
    pause.Wait();
}

由于 Task 类位于 System.Threading.Tasks 命名空间中,因此需要在文件的最上面添加 using 语句:The Task class is in the System.Threading.Tasks namespace, so you need to add that using statement at the top of file:

using System.Threading.Tasks;

运行此示例并检查输出。Run the sample, and check the output. 现在,每打印输出一个字词后,就会有 200 毫秒的延迟。Now, each single word is printed, followed by a 200 ms delay. 不过,显示的输出反映出一些问题,因为源文本文件有好几行都超过 80 个字符,且没有换行符。However, the displayed output shows some issues because the source text file has several lines that have more than 80 characters without a line break. 很难滚动读取这些文本。That can be hard to read while it's scrolling by. 此问题很容易解决。That’s easy to fix. 只需跟踪每行长度,然后在行长度达到特定阈值时生成新的一行即可。You’ll just keep track of the length of each line, and generate a new line whenever the line length reaches a certain threshold. ReadFrom 方法中声明 words 后声明一个局部变量,用于保存行长度:Declare a local variable after the declaration of words in the ReadFrom method that holds the line length:

var lineLength = 0;

然后,在 yield return word + " "; 语句后(在右大括号前)添加以下代码:Then, add the following code after the yield return word + " "; statement (before the closing brace):

lineLength += word.Length + 1;
if (lineLength > 70)
{
    yield return Environment.NewLine;
    lineLength = 0;
}

运行此示例,将能够按预配速大声朗读文本。Run the sample, and you’ll be able to read aloud at its pre-configured pace.

异步任务Async Tasks

最后一步将是添加代码,以便在一个任务中异步编写输出,同时运行另一任务来读取用户输入(如果用户想要加快或减慢文本显示速度,或完全停止文本显示的话)。In this final step, you’ll add the code to write the output asynchronously in one task, while also running another task to read input from the user if they want to speed up or slow down the text display, or stop the text display altogether. 此过程分为几步操作,最后将完成所需的全部更新。This has a few steps in it and by the end, you’ll have all the updates that you need. 第一步是创建异步 Task 返回方法,用于表示已创建的用于读取和显示文件的代码。The first step is to create an asynchronous Task returning method that represents the code you’ve created so far to read and display the file.

将以下方法(截取自 Main 方法主体)添加到 Program 类中:Add this method to your Program class (it’s taken from the body of your Main method):

private static async Task ShowTeleprompter()
{
    var words = ReadFrom("sampleQuotes.txt");
    foreach (var word in words)
    {
        Console.Write(word);
        if (!string.IsNullOrWhiteSpace(word))
        {
            await Task.Delay(200);
        }
    }
}

你会注意到两处更改。You’ll notice two changes. 首先,此版本在方法主体中使用 await 关键字,而不是调用 Wait() 同步等待任务完成。First, in the body of the method, instead of calling Wait() to synchronously wait for a task to finish, this version uses the await keyword. 为此,需要将 async 修饰符添加到方法签名中。In order to do that, you need to add the async modifier to the method signature. 此方法返回 TaskThis method returns a Task. 请注意,没有用于返回 Task 对象的返回语句。Notice that there are no return statements that return a Task object. 相反,Task 对象由编译器在你使用 await 运算符时生成的代码进行创建。Instead, that Task object is created by code the compiler generates when you use the await operator. 可以想象,此方法在到达 await 时返回。You can imagine that this method returns when it reaches an await. 返回的 Task 指示工作未完成。The returned Task indicates that the work has not completed. 在等待的任务完成时,此方法继续执行。The method resumes when the awaited task completes. 执行完后,返回的 Task 会指示已完成。When it has executed to completion, the returned Task indicates that it is complete. 调用代码可以通过监视返回的 Task 来确定完成时间。Calling code can monitor that returned Task to determine when it has completed.

可以在 Main 方法中调用以下新方法:You can call this new method in your Main method:

ShowTeleprompter().Wait();

此时,在 Main 中,代码确实是同步等待。Here, in Main, the code does synchronously wait. 应尽可能使用 await 运算符,而不是采用同步等待的方式。You should use the await operator instead of synchronously waiting whenever possible. 不过,在控制台应用程序的 Main 方法中,不能使用 await 运算符。But, in a console application’s Main method, you cannot use the await operator. 这会导致应用程序在所有任务完成前退出。That would result in the application exiting before all tasks have completed.

备注

如果使用 C# 7.1 或更高版本,则可以使用 async Main 方法创建控制台应用程序。If you use C# 7.1 or later, you can create console applications with async Main method.

接下来,需要编写第二个异步方法,从控制台读取键,并监视“<”(小于)、“>”(大于)和“X”或“x”键。Next, you need to write the second asynchronous method to read from the Console and watch for the ‘<’ (less than), ‘>’ (greater than) and ‘X’ or ‘x’ keys. 下面是为此任务添加的方法:Here’s the method you add for that task:

private static async Task GetInput()
{
    var delay = 200;
    Action work = () =>
    {
        do {
            var key = Console.ReadKey(true);
            if (key.KeyChar == '>')
            {
                delay -= 10;
            }
            else if (key.KeyChar == '<')
            {
                delay += 10;
            }
            else if (key.KeyChar == 'X' || key.KeyChar == 'x')
            {
                break;
            }
        } while (true);
    };
    await Task.Run(work);
}

这创建了一个表示 Action 委托的 lambda 表达式,用于在用户按“<”(小于)或“>”(大于)键时,从控制台读取键,并修改表示延迟的局部变量。This creates a lambda expression to represent an Action delegate that reads a key from the Console and modifies a local variable representing the delay when the user presses the ‘<’ (less than) or ‘>’ (greater than) keys. 当用户按下“X”或“x”键时,委托方法结束,允许用户随时停止文本显示。The delegate method finishes when user presses the ‘X’ or ‘x’ keys, which allow the user to stop the text display at any time. 此方法使用 ReadKey() 来阻止并等待用户按键。This method uses ReadKey() to block and wait for the user to press a key.

若要完成这项功能,需要新建 async Task 返回方法,用于启动这两项任务(GetInputShowTeleprompter),并管理这两项任务之间共享的数据。To finish this feature, you need to create a new async Task returning method that starts both of these tasks (GetInput and ShowTeleprompter), and also manages the shared data between these two tasks.

是时候创建一个类来处理这两项任务之间共享的数据了。It’s time to create a class that can handle the shared data between these two tasks. 此类包含两个公共属性,即延迟和指示已读取完整个文件的标志 DoneThis class contains two public properties: the delay, and a flag Done to indicate that the file has been completely read:

namespace TeleprompterConsole
{
    internal class TelePrompterConfig
    {
        public int DelayInMilliseconds { get; private set; } = 200;

        public void UpdateDelay(int increment) // negative to speed up
        {
            var newDelay = Min(DelayInMilliseconds + increment, 1000);
            newDelay = Max(newDelay, 20);
            DelayInMilliseconds = newDelay;
        }

        public bool Done { get; private set; }

        public void SetDone()
        {
            Done = true;
        }
    }
}

将此类放入新文件,并将此类封闭在 TeleprompterConsole 命名空间中,如上所示。Put that class in a new file, and enclose that class in the TeleprompterConsole namespace as shown above. 还需要添加 using static 语句,以便可以引用 MinMax 方法,而无需使用封闭类或命名空间名称。You’ll also need to add a using static statement so that you can reference the Min and Max methods without the enclosing class or namespace names. using static 语句从一个类导入方法。A using static statement imports the methods from one class. 这与一直使用的 using 语句相反,后者导入命名空间中的所有类。This is in contrast with the using statements used up to this point that have imported all classes from a namespace.

using static System.Math;

接下来,需要将 ShowTeleprompterGetInput 方法更新为使用新的 config 对象。Next, you need to update the ShowTeleprompter and GetInput methods to use the new config object. 编写最后一个 Task 返回 async 方法,用于启动这两项任务,并在第一项任务完成时退出:Write one final Task returning async method to start both tasks and exit when the first task finishes:

private static async Task RunTeleprompter()
{
    var config = new TelePrompterConfig();
    var displayTask = ShowTeleprompter(config);

    var speedTask = GetInput(config);
    await Task.WhenAny(displayTask, speedTask);
}

此处的一种新方法是 WhenAny(Task[]) 调用。The one new method here is the WhenAny(Task[]) call. 这会创建 Task,只要自变量列表中的任意一项任务完成,它就会完成。That creates a Task that finishes as soon as any of the tasks in its argument list completes.

接下来,需要同时将 ShowTeleprompterGetInput 方法更新为对延迟使用 config 对象:Next, you need to update both the ShowTeleprompter and GetInput methods to use the config object for the delay:

private static async Task ShowTeleprompter(TelePrompterConfig config)
{
    var words = ReadFrom("sampleQuotes.txt");
    foreach (var word in words)
    {
        Console.Write(word);
        if (!string.IsNullOrWhiteSpace(word))
        {
            await Task.Delay(config.DelayInMilliseconds);
        }
    }
    config.SetDone();
}

private static async Task GetInput(TelePrompterConfig config)
{
    Action work = () =>
    {
        do {
            var key = Console.ReadKey(true);
            if (key.KeyChar == '>')
                config.UpdateDelay(-10);
            else if (key.KeyChar == '<')
                config.UpdateDelay(10);
            else if (key.KeyChar == 'X' || key.KeyChar == 'x')
                config.SetDone();
        } while (!config.Done);
    };
    await Task.Run(work);
}

这个新版 ShowTeleprompterTeleprompterConfig 类中调用新方法。This new version of ShowTeleprompter calls a new method in the TeleprompterConfig class. 现在,需要将 Main 更新为调用 RunTeleprompter(而不是 ShowTeleprompter):Now, you need to update Main to call RunTeleprompter instead of ShowTeleprompter:

RunTeleprompter().Wait();

结束语Conclusion

此教程介绍了与处理控制台应用程序相关的许多 C# 语言和 .NET Core 库功能。This tutorial showed you a number of the features around the C# language and the .NET Core libraries related to working in Console applications. 可以在此教程的基础上进一步探索语言和本文介绍的类。You can build on this knowledge to explore more about the language, and the classes introduced here. 你已了解文件和控制台 I/O 的基础知识、基于任务的异步编程的阻止性和非阻止性用途、C# 语言介绍、C# 程序的组织结构,以及 .NET Core 命令行接口和工具。You’ve seen the basics of File and Console I/O, blocking and non-blocking use of the Task-based asynchronous programming, a tour of the C# language and how C# programs are organized and the .NET Core Command Line Interface and tools.

有关文件 I/O 的详细信息,请参阅文件和流 I/O 主题。For more information about File I/O, see the File and Stream I/O topic. 有关本教程中使用的异步编程模型的详细信息,请参阅基于任务的异步编程主题和异步编程主题。For more information about asynchronous programming model used in this tutorial, see the Task-based Asynchronous Programming topic and the Asynchronous programming topic.