1. 从一个具体场景说起:命令行菜单工具为什么卡住了
很久以前我在写一个小工具,功能特别简单——在控制台里做一个菜单,用户按"1"执行数据采集,按"2"执行数据发送,按"3"退出程序。听起来很简单对吧?我用Console.ReadLine()读取用户输入,然后判断字符串是"1"、"2"还是"3"。跑起来之后发现一个问题:用户按数字键之后必须再多按一个回车,程序才有反应。如果是做内部测试工具倒也没啥,但放进生产环境配合扫码枪用,这个回车就会变成多余的步骤,甚至会把后面的流程带歪。
后来我把输入方式改成了Console.ReadKey(),问题立刻解决:用户按下数字键的瞬间程序就响应了,不需要等回车。但紧接着又踩了一个更隐蔽的坑——ReadKey()会把回车键本身也当作按键捕获到,菜单选完之后按回车确认,结果程序把回车当成了一次无效输入,逻辑直接错乱。
这件事让我意识到,很多 C# 初学者甚至工作两三年的开发者,对"输入"这件事的理解都停留在"有个方法能把用户输入读进来"这个层面。至于ReadLine、ReadKey、Read到底有什么区别、各自适合什么场景、混用会有什么后果,并没有系统梳理过。这篇文章就以"C# 里的两种核心输入方式"为主线,把原理、用法、实操经验和坑全部讲透,顺便结合上位机开发、扫码枪这类真实硬件场景做扩展。无论你是在校学生、转行学 .NET 的初学者,还是正在做工业软件的工程师,都可以参考。
2. 为什么 C# 要把"输入"拆成两条路径
先搞清楚一件事:Console.ReadLine()、Console.ReadKey()、Console.Read()这三个方法,很多人误以为只是返回值不同,换着用就行。这个理解有偏差,它们的核心差异在于"工作的层级"完全不同。
2.1 控制台输入的本质:缓冲区模型
Windows 控制台程序(以及 Linux 终端程序)的输入机制本质上是一个"缓冲区 + 事件"模型。键盘按键按下之后,字符并不会直接传给我们的程序,而是先进入操作系统的标准输入缓冲区(stdin)。程序通过 Console 类的方法去读取这个缓冲区的内容。
Console.ReadLine()的行为是:持续读取缓冲区里的字符,直到遇到换行符(\n)为止,然后把换行符之前的整串字符作为字符串返回。这意味着用户输入完内容后必须按回车键,ReadLine才会返回结果。如果用户只按了一个键没按回车,调用ReadLine的线程就会一直阻塞在那里。
Console.Read()的行为是:从缓冲区里读取下一个字符,返回的是该字符对应的整型编码。它只读取一个字符,但不会消费掉换行符。比如用户输入了 "abc" 然后回车,第一次调用Read()返回 97('a' 的 ASCII 码),第二次返回 98,第三次返回 99,第四次返回 13(回车键的 ASCII)。
Console.ReadKey()则完全不同。它不经过字符缓冲区,而是直接从键盘输入流中捕获按键事件,返回一个ConsoleKeyInfo结构体,里面包含按下的键位、修饰键(Shift、Ctrl、Alt)状态和对应的字符。按下键的瞬间ReadKey就能返回,不需要等回车。
2.2 三种方法的本质对比
| 方法 | 读取单位 | 是否需要回车 | 返回类型 | 工作层级 |
|---|---|---|---|---|
| Console.ReadLine() | 整行文本 | 需要 | string | 标准输入缓冲区 |
| Console.Read() | 单个字符 | 视情况 | int(字符编码) | 标准输入缓冲区 |
| Console.ReadKey() | 单个按键 | 不需要 | ConsoleKeyInfo | 键盘输入流 |
看到这里就明白了:ReadLine和Read站在"字符流"的层面工作,ReadKey站在"按键事件"的层面工作。这就是标题里说的"两种输入"的分界点。在实际工程中,你选哪种输入方式,取决于你是想"等用户输完一句话再处理",还是想"用户一碰键盘就立刻响应"。
2.3 一个直觉类比:点餐和按钮
为了帮助初学者记住这个区别,我常用点餐来类比。
Console.ReadLine()好比你去餐厅点餐:你说完一整句话(比如"来一份宫保鸡丁"),服务员听完、记录下来,然后才去后厨下单选。这中间有一个"说完整句话"和"服务员确认"的过程。对应到代码里,就是用户输入完一行文字并按下回车,程序才拿到完整内容。
Console.ReadKey()好比按电梯按钮:你伸手一按,电梯立刻就知道你要往上走,不用等你说"请帮我按一下上行按钮"这整句话。一个按键、一个动作、一次响应,这是面向事件的设计思维。
理解了这两者的区别,后面所有实际操作和避坑都有了解释的基础。
3. Console.ReadLine:用一整行输入解决"数据录入"类问题
3.1 基本用法和返回值陷阱
Console.ReadLine()返回的是去掉换行符之后的完整字符串。在 .NET 5/C# 9 之前的版本中,这个方法返回string类型,并且可能为null(在输入流结束时会返回 null)。从 .NET 5 开始,返回值被改为string?,编译器会强制你考虑 null 的情况。
先看一个最简单的示例:
Console.WriteLine("请输入设备编号:"); string input = Console.ReadLine() ?? string.Empty; Console.WriteLine($"你输入的设备编号是:{input}");这里的?? string.Empty是防空处理。在一些自动化测试、管道重定向或者程序被外部进程杀死的场景下,ReadLine()确实可能返回 null,不做处理直接往下用会导致NullReferenceException。
接下来是初学者最容易踩的坑:类型转换。控制台输入永远是字符串,但实际业务里往往需要数字。
Console.WriteLine("请输入采集通道数:"); string input = Console.ReadLine(); int channelCount = int.Parse(input); // 用户输入非数字字符时直接抛出异常这段代码在用户输入"abc"或者不输入直接回车时,会抛出FormatException。工业环境里,操作员随手误触一个字母是很常见的事情,程序不能因为这种输入直接崩溃。
更稳的写法是用int.TryParse:
Console.WriteLine("请输入采集通道数:"); string input = Console.ReadLine() ?? string.Empty; if (int.TryParse(input, out int channelCount)) { Console.WriteLine($"通道数设置为:{channelCount}"); } else { Console.WriteLine("输入无效,请输入整数。"); }TryParse的底层机制是:尝试将字符串转换为整数,成功就返回 true 并通过 out 参数输出结果,失败返回 false,不会抛异常。这个模式在处理用户输入时应该成为习惯。
3.2 ReadLine 背后的阻塞机制
很多初学者不理解为什么ReadLine()会"卡住"程序。这涉及到同步 IO 的阻塞本质。当主线程调用ReadLine()时,如果输入缓冲区里没有数据,当前线程会进入等待状态,不消耗 CPU,直到操作系统通知它有数据可读。这在单线程控制台程序里没有太大问题,但在上位机这类需要同时处理 UI 刷新和串口通信的程序里,如果在 UI 线程上直接调用ReadLine(),整个界面就会冻结。
后面我会专门讨论这个问题,这里先记住一个原则:凡是会阻塞 UI 线程的 IO 操作,都要想办法移到工作线程,或者用异步方式读取。在控制台程序里,ReadLine()阻塞的只是当前线程,如果程序没有其他线程要干活,那阻塞也无所谓。但一旦程序需要同时做别的事,比如边等待输入边刷新设备状态,就要格外小心。
3.3 循环读取多个参数:一个小型参数配置工具实例
在实际项目中,ReadLine最常见的用法是做一个顺序的参数录入流程。我写过一个温控设备的上位机参数配置工具,需要在控制台里让用户依次输入串口号、波特率、报警上限、报警下限。代码结构大概是这样:
Console.WriteLine("===== 温控设备参数配置 ====="); Console.Write("请输入串口号(如 COM3):"); string portName = Console.ReadLine() ?? string.Empty; Console.Write("请输入波特率(如 9600):"); if (!int.TryParse(Console.ReadLine(), out int baudRate)) { Console.WriteLine("波特率输入无效,使用默认值 9600。"); baudRate = 9600; } Console.Write("请输入报警上限温度:"); if (!double.TryParse(Console.ReadLine(), out double alarmHigh)) { Console.WriteLine("输入无效,报警上限不设置。"); alarmHigh = double.MaxValue; } Console.Write("请输入报警下限温度:"); if (!double.TryParse(Console.ReadLine(), out double alarmLow)) { Console.WriteLine("输入无效,报警下限不设置。"); alarmLow = double.MinValue; } Console.WriteLine("\n配置完成,参数如下:"); Console.WriteLine($"串口: {portName}"); Console.WriteLine($"波特率: {baudRate}"); Console.WriteLine($"报警上限: {alarmHigh}"); Console.WriteLine($"报警下限: {alarmLow}");这套录入流程的特点是:每个输入项是独立的"一行文本",用户输完按回车才进入下一步。信息的边界靠回车键来划分。
实际运行中我发现一个问题:用户输完波特率之后,往往还要瞟一眼屏幕确认有没有输错,然后再按下回车。这个过程本来就需要一个"确认"的时间,所以ReadLine反而是最合适的——它强迫用户形成"输入 + 确认"的完整节奏,不容易误操作。
3.4 用 ReadLine 做数据校验的进阶写法
参数校验是工业软件的刚需。直接TryParse只能保证格式对,不能保证范围合理。以温度报警参数为例,用户输入了一个 500 度,但设备量程上限是 200 度,这种输入应该被拒绝。
我通常的做法是写一个循环,直到用户输入合法值才退出:
static double ReadTemperature(string prompt, double min, double max) { while (true) { Console.Write(prompt); string input = Console.ReadLine() ?? string.Empty; if (double.TryParse(input, out double value) && value >= min && value <= max) { return value; } Console.WriteLine($"输入无效,请输入 {min} 到 {max} 之间的数值。"); } } // 使用 double alarmHigh = ReadTemperature("请输入报警上限温度:", 20.0, 200.0);这样写的好处是:非法输入不会让程序退出,而是给出提示后让用户重新输入。对操作人员来说,这个体验是友好的;对开发者来说,这种封装方式也避免了到处重复写校验逻辑。
4. Console.ReadKey:用单键输入实现"菜单选择"与"即时响应"
4.1 ReadKey 返回的到底是什么
Console.ReadKey()返回ConsoleKeyInfo,这是一个结构体,包含三个关键属性:
Key:枚举类型ConsoleKey,表示按下的物理键位,比如ConsoleKey.D1表示数字键 1,ConsoleKey.Enter表示回车键。KeyChar:字符类型,表示按键对应的 Unicode 字符。比如按下数字键 1 且没有按住 Shift,KeyChar就是'1'。Modifiers:枚举类型ConsoleModifiers,指示是否有 Shift、Ctrl、Alt 等修饰键被同时按下。
另外,ReadKey有一个重载版本ReadKey(bool intercept)。当intercept为true时,按下的键不会显示在控制台窗口上;为false时会先回显用户按键,再返回到代码逻辑。
实操中我做菜单选择器时,通常会传入true,因为菜单上已经显示了选项,用户按下的键再回显一次反而显得累赘。
4.2 菜单选择器的完整实现
以下是我常用的一段菜单选择代码,简单可复用:
Console.WriteLine("请选择操作:"); Console.WriteLine("1. 开始数据采集"); Console.WriteLine("2. 查看历史数据"); Console.WriteLine("3. 停止服务并退出"); Console.WriteLine("按 ESC 键直接退出程序。"); while (true) { ConsoleKeyInfo keyInfo = Console.ReadKey(true); switch (keyInfo.Key) { case ConsoleKey.D1: case ConsoleKey.NumPad1: Console.WriteLine("开始数据采集..."); StartDataAcquisition(); break; case ConsoleKey.D2: case ConsoleKey.NumPad2: Console.WriteLine("正在加载历史数据..."); ShowHistory(); break; case ConsoleKey.D3: case ConsoleKey.NumPad3: Console.WriteLine("服务正在停止..."); return; case ConsoleKey.Escape: Console.WriteLine("程序退出。"); return; default: Console.WriteLine("无效按键,请重新选择。"); break; } }注意这里同时判断了D1和NumPad1。前者是主键盘区的数字键,后者是小键盘区的数字键。由于Key枚举区分了物理键位,如果不做两个分支处理,用户用小键盘输入时就会掉进 default 分支。这是我做工业工具时实际踩过的细节坑。
另一个细节是ESC键的退出逻辑。对于长时间运行的采集工具,给用户一个在任何状态下都能安全退出的路径非常关键。用ESC而不是组合键(如 Ctrl+C)的好处是:不容易被误触,又比让用户去选择"退出菜单项"更快捷。
4.3 ReadKey vs Read:一个容易被忽略的差异
很多教材会把Console.Read()也归入"单字符输入"的范畴,但它和ReadKey的定位完全不同。Read()返回的是字符编码(int),并且本质上是缓冲区读取。举例来说:
int code = Console.Read(); Console.WriteLine($"读取到字符编码:{code}");如果用户输入 "A" 然后按回车,Read()返回 65(大写 A 的 ASCII)。同时缓冲区里还残留着\r(13)和\n(10),后面的Read调用会依次读到这两个。
如果你想用Read()做一个"按空格继续"的交互,用户按下空格后再按回车,程序会先处理空格,然后回车键也会被后续的输入逻辑读到,导致流程错乱。这就是Read()不适合做单键交互的原因。
ReadKey()没有这个问题,它直接从按键事件层拿数据,不经过缓冲区,不会把回车键残留给后续调用。
4.4 扫码枪场景:ReadKey 思路的硬件对应物
热词里出现了"c# 扫码枪触发事件",这里多说几句。USB 接口的扫码枪在电脑上通常被识别为键盘设备,扫读条码时相当于以极快的速度往系统里输入一串字符并最后附加一个回车。常见的扫码枪有两种工作模式:
- 串口模式:扫码枪通过串口发送数据,需要用
SerialPort类读取,读取方式是ReadLine()或ReadExisting(),读完一行就是一整条条码,不需要处理回车问题。 - 键盘模拟模式:扫码枪被系统识别为普通键盘,每扫一个条码就相当于按了十几下一个随机字符再加一个回车。这种模式下,用
Console.ReadKey()的思路就可以逐键捕获,直到捕获到回车键为止。
如果是在 WinForms 或 WPF 里接扫码枪,键盘模拟模式下通常会在窗口的KeyPress或者KeyDown事件里做缓存,把按键收集到一个 StringBuilder 里,等收到回车键时把整个字符串取走当作一条条码处理。这个套路和ReadKey单个按键捕获的思路一模一样,不同的只是事件模型。理解了控制台里的ReadKey,再去理解 WinForms 里的键盘事件就顺理成章了。
5. 两种输入在上位机与工业软件中的变形与扩展
5.1 从控制台输入到 UI 输入:事件驱动模型
控制台程序用ReadLine和ReadKey是"主动取数据",程序就在那儿等着,你给数据我才往下走。但在 WinForms / WPF 上位机里,大部分输入是"事件驱动"的:用户输入内容、控件把变化告诉程序、程序去响应。
典型如文本框输入。在 WinForms 中,TextBox 控件有TextChanged事件,用户每敲一个字符或者粘贴文本时,这个事件触发一次。它的行为就像ReadKey一样——每次输入立即通知程序。区别在于,TextChanged给的是控件里的完整文本,而不是单个按键。
扫码枪场景下,很多人喜欢用TextChanged去判断一条条码是否扫描完成。这个方案不够稳。因为扫码枪是一串字符连续输入,TextChanged事件会触发很多次,每次文本都在变,必须等到最后收到回车键才算结束。更稳妥的做法是在KeyPress或KeyDown事件里做判断:每次按键判断是否为回车,如果不是就缓存,是就取整条数据。
用代码说明:
StringBuilder barcodeBuffer = new StringBuilder(); private void txtBarcodeInput_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)13) // 回车键 { string barcode = barcodeBuffer.ToString(); barcodeBuffer.Clear(); ProcessBarcode(barcode); e.Handled = true; // 阻止回车在文本框里换行/触发默认行为 } else { barcodeBuffer.Append(e.KeyChar); e.Handled = true; // 可选:把键盘模拟输入屏蔽在文本框外 } }这套逻辑本质上是ReadKey思路在 UI 模型下的复刻。理解了控制台的按键读取,就能自然迁移到 UI 事件处理。
5.2 循环数据采集和 UI 刷新卡顿问题的根源
热词里还有一个高频痛点:"c# 循环数据采集和 ui 刷新卡顿"。这个问题的根源之一,就是有人把控制台输入的习惯带进了 UI 开发。
举个具体例子:上位机程序里有串口采集线程,每隔 50ms 从设备读取一组温度数据,要求实时显示在界面上。简单的做法是在采集线程里更新控件的Text属性,但 WinForms / WPF 的 UI 控件并非线程安全,跨线程更新会抛异常或者导致界面卡顿。
数据采集是高频操作,如果中间夹了一次ReadLine()或者Task.Delay之类的阻塞调用,整个采集节奏就被打乱了。更常见的情况是:UI 线程被某个事件处理器里的阻塞操作(比如等待用户输入、等待串口读完一整行)卡住,界面失去响应。
解决思路是明确分工:
- 采集线程:负责纯数据的读取和解析。如果用
SerialPort读取,要在子线程中调用ReadLine()或ReadExisting(),不能在 UI 线程等待。 - UI 线程:负责把数据刷新到控件上。推荐用
BeginInvoke或Dispatcher.BeginInvoke将更新逻辑排队到 UI 线程。
实际的完整代码可以这样:
private void DataReceptionLoop() { while (_isRunning) { try { string line = _serialPort.ReadLine(); // 阻塞读取一行设备数据 // 解析数据 double temperature = ParseTemperature(line); // 更新 UI:把操作切换到 UI 线程执行 this.BeginInvoke(new Action(() => { lblTemperature.Text = $"{temperature:F2} °C"; })); } catch (TimeoutException) { // 超时继续循环读取 } } }这里的关键在于:ReadLine()阻塞的是采集线程,不是 UI 线程,界面刷新立刻恢复流畅。把"读取输入/接收数据"和"界面更新"拆到不同的执行流上,是解决卡顿的核心原则。
5.3 ReadLine 在串口通信中的使用边界
顺带说一个与输入相关的常见误区:SerialPort类也有ReadLine()方法,但它和Console.ReadLine()完全是两码事。SerialPort.ReadLine()读取的是串口接收缓冲区中的字节流,直到遇到NewLine属性的值(默认是换行符)为止,返回解析后的字符串。
底层串口通信经常一包数据没有发完,或者中间有延迟,直接调用ReadLine()可能会超时。工业场景下的常规处理手段:
- 设置合理的
ReadTimeout(如 500ms) - 使用
DataReceived事件配合内部缓冲区,而不是主动去背靠背ReadLine - 用自定义的组帧协议,依靠起始位、结束位等标记判断一包数据是否完整
这与控制台输入的"人类打字节奏"不同:人打字是按下的键直接进缓冲区,回车即一行结束;串口数据是按帧到达的,帧的边界取决于通信协议。所以ReadLine这个名字在不同类中含义完全不同,别混用。
6. 实操中的典型翻车现场与排查思路
接下来这部分是重头戏。我在指导和实际开发中见过太多输入相关的 bug,挑几个最典型的讲透。
6.1 混用 ReadLine 和 ReadKey 造成的"残留回车"
这个 bug 太经典了。先看错误代码:
Console.WriteLine("是否继续?(y/n)"); string choice = Console.ReadLine(); Console.WriteLine("按任意键返回菜单..."); Console.ReadKey();这段代码在运行时会发现,第二个提示出现后不等按键直接跳过了。原因在于第一次Console.ReadLine()读取时,用户输入了 "y" 并按下回车。ReadLine会把 "y" 返回,同时回车键产生的\r\n被从缓冲区里消费掉或者部分消费。在高版本 .NET 的 Console 实现中,ReadLine在 Windows 上会读取直到并包含换行符,所以理论上不会残留。但在某些平台或者和Console.Read()混用时,问题就出现了。
比这更隐蔽的混用情况是把ReadLine()和Console.Read()放在一起:
int firstChar = Console.Read(); // 用户输入"abc",读取到 'a' = 97 string restOfLine = Console.ReadLine(); // 读取到 "bc"这其实是Read和ReadLine的规范配合,Read拿一个字符,ReadLine拿剩下的整行。但如果用户只输入了一个字符就回车,ReadLine()返回的就是空字符串,容易产生各种边界问题。
我的建议很简单:一个程序里,要么统一走ReadLine做行输入,要么统一走ReadKey做按键输入,尽量不要混合使用。如果确实需要混合(比如先输入一个文件名,再按任意键确认),就在两种输入之间加一段缓冲清理的辅助代码:
static void ClearInputBuffer() { while (Console.KeyAvailable) { Console.ReadKey(true); } }Console.KeyAvailable判断缓冲区中是否有按键,有就读掉,直到清空。这样能最大程度减少残留输入对后续读取的干扰。
6.2 ReadLine 循环读取时的空行死循环
很多人在做"循环接收命令行指令"时这样写:
while (true) { string line = Console.ReadLine(); if (line == "exit") break; ExecuteCommand(line); }因为ReadLine()在控制台窗口关闭或标准输入流结束时会返回 null,此时line == "exit"永远不成立,如果ExecuteCommand内部对 null 或空字符串处理不当,就可能产生死循环或者异常。正确写法是:
while (true) { string line = Console.ReadLine(); if (line is null) break; if (line.Trim().Length == 0) continue; if (line.Trim().Equals("exit", StringComparison.OrdinalIgnoreCase)) break; ExecuteCommand(line.Trim()); }把 null 判断放前面,空行 continue 跳过,exit命令用忽略大小写的比较方式。这套模式基本能应对控制台命令行工具的常见场景。
6.3 ReadKey 的立即响应特性带来的误触发问题
ReadKey的即时响应是优点,但也带来了误触发问题。比如菜单是"按 1 开始采集,按 2 停止",用户操作习惯是:先按"1"启动采集,接着快速按"2"停止。如果你的菜单只是一个简单的switch,那"1"和"2"都会被立即消费,采集还没真正拉起来就又被停止了。
这种情况我一般会在状态转换逻辑里加"防抖"或者"状态确认":
- 如果当前状态已经是"采集中",再次收到"开始采集"按键就忽略;
- 如果有需要,在两个状态之间加一个极短的延时(如 100ms),过滤掉用户在同一瞬间的连续误按。
更好的方式是引入状态机,而不是简单地按键对应动作。用枚举定义状态,按键触发的是状态迁移的请求,是否执行由当前状态决定。这在设备控制类软件中尤其重要。
6.4 输入过程中的异常与强制退出设计
还有一个容易被忽略的点:异常退出。程序在等待ReadLine或者ReadKey时如果被强制结束线程,资源可能来不及释放。比如扫码头、串口连接还没关闭就没了,下次启动时串口被占用直接打不开。
成熟的工业工具,至少会在退出路径上做两件小事:
- 用
try-finally确保串口、网络连接、文件句柄等资源被释放; - 在进入阻塞输入之前,提示用户"D 键进入调试模式,Q 键安全退出",把逃逸通道放在用户眼皮子底下。
Console.WriteLine("按 Q 随时安全退出,按其它键继续..."); ConsoleKeyInfo key = Console.ReadKey(true); if (key.Key == ConsoleKey.Q) { // 安全退出流程:关闭串口、保存配置、释放资源 _serialPort?.Close(); SaveConfig(); return; }这个模式我在多个项目里使用了,很实用。相比让用户用 Ctrl+C 强杀进程,这种软退出能把该保存的数据保存下来,避免设备状态不一致。
7. 根据自己的经验做一次选型总结
讲到这里,核心原理、实操代码和踩坑记录都覆盖了。最后分享几条我个人的选型习惯,算是给读者的一份可直接用的参照表:
| 场景 | 推荐输入方式 | 原因 |
|---|---|---|
| 参数录入,需要用户确认 | Console.ReadLine() | 有回车确认,输入边界清晰 |
| 菜单选择,想快速响应 | Console.ReadKey(true) | 按下即响应,不需要回车 |
| 按键密码/访问码输入 | Console.ReadKey(true) | 拦截回显,隐藏按键内容 |
| 扫码枪(键盘模拟模式) | KeyPress/KeyDown 事件 + 缓冲 | 逐键捕获,遇到回车就是一整条 |
| 串口数据读取 | DataReceived 事件 + 组帧协议 | 数据是流式的,不能按"行"盲读 |
| UI 数据刷新 | 子线程读数据 + BeginInvoke 更新 | 避免 UI 卡顿和跨线程异常 |
选择输入方式的核心逻辑,就是回答一个问题:用户给程序的每一份信息,边界是怎么划分的?
- 如果边界靠回车键划分,用
ReadLine()。 - 如果边界靠"单个动作"划分,用
ReadKey()。 - 如果边界靠协议中的帧头帧尾划分,就靠事件接收器去解析。
这件看起来很小的事情,其实直接影响用户体验、程序稳定性和代码可维护性。很多上了生产环境的工具出现奇怪卡顿、逻辑错乱,追查到最后往往就是输入方式选错了、缓冲区没清理。
如果你正在学习 C# 基础,希望这篇文章能帮你把ReadLine和ReadKey的区别刻在脑子里。如果你已经在做上位机或者工业软件,希望这些真实的坑能帮你少走一段弯路。输入处理是每个程序都绕不开的环节,值得花时间彻底搞清楚。