本教程介绍了 .NET 和 C# 语言中的许多功能。 学习内容:
- .NET CLI 的基础知识
- C# 控制台应用程序的结构
- 控制台输入/输出
- .NET 中文件 I/O API 的基础知识
- .NET 中基于任务的异步编程的基础知识
你将生成一个读取文本文件的应用程序,并将该文本文件的内容回显到控制台。 按配速大声朗读控制台输出。 可以通过按“<”(小于)或“>”(大于)键来加快或放慢速度。 可以在 Windows、Linux、macOS 或 Docker 容器中运行此应用程序。
本教程中有很多功能。 我们将逐个生成这些功能。
先决条件
- 最新的 .NET SDK
- Visual Studio Code 编辑器
- C# 开发套件
创建应用
第一步是创建新应用程序。 打开命令提示符并为应用程序创建新目录。 使该目录成为当前目录。 在命令提示符处键入命令 dotnet new console
。 这会为基本的“Hello World”应用程序创建启动文件。
在开始进行修改之前,让我们运行简单的 Hello World 应用程序。 创建应用程序后,在命令提示符处键入 dotnet run
。 此命令运行 NuGet 包还原过程,创建应用程序可执行文件,并运行可执行文件。
简单的 Hello World 应用程序代码全部在 Program.cs中。 使用你喜欢的文本编辑器打开该文件。 将 Program.cs 中的代码替换为以下代码:
namespace TeleprompterConsole;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
该文件顶部将出现 namespace
语句。 与你可能使用的其他面向对象的语言一样,C# 使用命名空间来组织类型。 这个 Hello World 程序和其他程序没有什么不同。 可以看到程序位于名称 TeleprompterConsole
的命名空间中。
读取和回显文件
添加的第一项功能是能够读取文本文件并将所有文本显示到控制台。 首先,添加文本文件。 将此 示例 的 GitHub 存储库中的 sampleQuotes.txt 文件复制到项目目录中。 这将充当应用程序的脚本。 有关如何下载本教程的示例应用的信息,请参阅 示例和教程中的说明。
接下来,在 Program
类中添加以下方法(Main
方法下方):
static IEnumerable<string> ReadFrom(string file)
{
string? line;
using (var reader = File.OpenText(file))
{
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
}
此方法是一种特殊的 C# 方法类型,称为 迭代器方法。 迭代器方法返回延迟计算的序列。 也就是说,序列中的每一项是在使用序列的代码提出请求时生成。 迭代器方法是包含一个或多个 yield return
语句的方法。 ReadFrom
方法返回的对象包含用于生成序列中每个项的代码。 在此示例中,涉及从源文件中读取下一行文本,并返回该字符串。 每次调用代码从序列请求下一项时,代码都会读取文件中的下一行文本并返回它。 当文件完全读取时,序列指示没有更多项。
可能有两个 C# 语法元素对你来说是新的。 此方法中的 using
语句管理资源清理。 在 using
语句(在此示例中reader
)中初始化的变量必须实现 IDisposable 接口。 该接口定义在释放资源时应调用的单个方法 Dispose
。 当执行到达 using
语句的右大括号时,编译器将生成该调用。 编译器生成的代码可确保资源得到释放,即使代码块中用 using 语句定义的代码抛出异常,也不例外。
reader
变量是使用 var
关键字定义的。 var
定义隐式类型局部变量。 这意味着变量的类型由分配给变量的对象编译时类型确定。 此处,这是 OpenText(String) 方法的返回值,这是一个 StreamReader 对象。
现在,让我们填写代码以在 Main
方法中读取文件:
var lines = ReadFrom("sampleQuotes.txt");
foreach (var line in lines)
{
Console.WriteLine(line);
}
运行程序(使用 dotnet run
),可以看到输出到控制台的每一行。
添加延迟和设置输出格式
现在的问题是,输出显示过快,无法大声朗读。 现在,你需要在输出中添加延迟。 一开始,你将生成一些支持异步处理的核心代码。 但是,这些第一步将遵循一些反模式。 添加代码时,注释中指出了反模式,后续步骤中将更新代码。
本部分有两个步骤。 首先,更新迭代器方法以返回单个单词而不是整个行。 这是通过这些修改完成的。 将 yield return line;
语句替换为以下代码:
var words = line.Split(' ');
foreach (var word in words)
{
yield return word + " ";
}
yield return Environment.NewLine;
接下来,你需要修改读取文件行的方式,并在编写每个单词后添加延迟。 将 Main
方法中的 Console.WriteLine(line)
语句替换为以下代码块:
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();
}
运行示例并检查输出。 现在,每个单词都会被打印出来,然后有 200 毫秒的延迟。 但是,显示的输出显示一些问题,因为源文本文件有多个行,这些行的字符数超过 80 个字符,没有换行符。 很难滚动读取这些文本。 这很容易修复。 只需跟踪每行的长度,并在行长度达到特定阈值时生成一个新行。 在 ReadFrom
方法中,在声明 words
之后,声明一个用于存储行长度的局部变量:
var lineLength = 0;
然后,在 yield return word + " ";
语句后面添加以下代码(右大括号前):
lineLength += word.Length + 1;
if (lineLength > 70)
{
yield return Environment.NewLine;
lineLength = 0;
}
运行此示例,将能够按预配速大声朗读文本。
异步任务
在此最后一步中,你将添加代码以异步方式在一个任务中编写输出,同时运行另一个任务来读取用户输入(如果他们想要加快或减慢文本显示速度,或完全停止文本显示)。 这有一些步骤,最后,你将获得所需的所有更新。 第一步是创建一个异步 Task 返回方法,该方法表示到目前为止创建的代码以读取和显示文件。
将此方法添加到 Program
类(它取自 Main
方法的正文):
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);
}
}
}
你会注意到两个更改。 首先,在方法正文中,此版本使用 await
关键字,而不是调用 Wait() 同步等待任务完成。 为此,需要将 async
修饰符添加到方法签名。 此方法返回 Task
。 请注意,没有返回 Task
对象的返回语句。 相反,Task
对象是由编译器在使用 await
运算符时生成的代码创建的。 可以想象,此方法在到达 await
时返回。 返回的 Task
指示工作尚未完成。 该方法将在等待的任务完成后恢复。 当它执行到完成时,返回的 Task
指示它已完成。
调用代码可以监控返回的 Task
,以判断何时完成。
在调用 ShowTeleprompter
之前添加 await
关键字:
await ShowTeleprompter();
这要求将 Main
方法签名更改为:
static async Task Main(string[] args)
在基础知识部分详细了解async Main
方法。
接下来,需要编写第二个异步方法,从控制台读取键,并监视“<”(小于)、“>”(大于)和“X”或“x”键。 下面是为此任务添加的方法:
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 表达式,用于在用户按“<”(小于)或“>”(大于)键时,从控制台读取键,并修改表示延迟的局部变量。 当用户按下“X”或“x”键时,委托方法将完成,这允许用户随时停止文本显示。 此方法使用 ReadKey() 阻止并等待用户按键。
若要完成此功能,需要创建一个新的 async Task
返回方法,该方法启动这两个任务(GetInput
和 ShowTeleprompter
),并管理这两个任务之间的共享数据。
是时候创建一个类来处理这两个任务之间的共享数据了。 此类包含两个公共属性,即延迟和指示已读取完整个文件的标志 Done
:
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
命名空间中,如下所示。 此外,还需要在文件顶部添加 using static
语句,以便无需封闭类或命名空间名称即可引用 Min
和 Max
方法。 using static
语句从一个类导入方法。 这与不带 static
的 using
语句形成鲜明对比,该语句从命名空间导入所有类。
using static System.Math;
接下来,需要更新 ShowTeleprompter
和 GetInput
方法来使用新的 config
对象。 编写一个最终 Task
返回 async
方法,以启动任务并在第一个任务完成时退出:
private static async Task RunTeleprompter()
{
var config = new TelePrompterConfig();
var displayTask = ShowTeleprompter(config);
var speedTask = GetInput(config);
await Task.WhenAny(displayTask, speedTask);
}
这里的一个新方法是 WhenAny(Task[]) 调用。 这会创建 Task
,只要自变量列表中的任意一项任务完成,它就会完成。
接下来,需要更新 ShowTeleprompter
和 GetInput
方法,以便将 config
对象用于延迟:
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);
}
此新版本的 ShowTeleprompter
调用 TeleprompterConfig
类中的新方法。 现在,需要更新 Main
以调用 RunTeleprompter
而不是 ShowTeleprompter
:
await RunTeleprompter();
结论
本教程介绍了 C# 语言和与在控制台应用程序中工作相关的 .NET Core 库的一些功能。 你可以利用此知识进一步探索语言,以及此处介绍的课程。 你已经了解了文件和控制台 I/O 的基础知识、任务异步编程模型中的阻塞和非阻塞使用、C# 语言概览及其程序组织方式,以及 .NET CLI。
有关文件 I/O 的详细信息,请参阅 文件和流 I/O。 有关本教程中使用的异步编程模型的详细信息,请参阅 基于任务的异步编程 和 异步编程。