Skip to content

C# 输入与输出:从控制台到文件的全链路指南 ​

开篇:程序的「嘴巴」和「耳朵」 ​

想象你正在经营一家餐厅。顾客点菜(输入)、厨房做菜(处理)、服务员上菜(输出)——这是一个完整的流程。如果服务员听不懂顾客说什么,或者上错了菜,餐厅就无法运转。

程序也一样。输入是程序从外部世界获取数据(用户键盘、文件、网络),输出是程序把处理结果呈现给外部(屏幕、文件、数据库)。C# 提供了从「最简陋的控制台对话」到「高效的文件管道」一整套工具,理解它们的使用场景,能让你的程序真正「活」起来。


一、Console 控制台:最基础的对话窗口 ​

Console 类是 C# 中最简单的 IO 工具,适合学习调试、小型工具、演示程序。它就像餐厅的「前台对讲机」——功能简单,但必不可少。

1.1 输出:Write 与 WriteLine 的区别 ​

csharp
// WriteLine:输出后自动换行(像说完一句话按回车)
Console.WriteLine("Hello, World!");  // 输出后光标移到下一行
Console.WriteLine("第二行");           // 在新行输出

// Write:输出后不换行(像说话说一半,等着继续)
Console.Write("姓名:");
Console.Write("张三");               // 两句连在一起输出
Console.WriteLine();                 // 单独换行

// 格式化输出的三种方式
string name = "李四";
int age = 25;

// 方式一:字符串拼接(最原始)
Console.WriteLine("姓名:" + name + ",年龄:" + age);

// 方式二:占位符(C# 经典方式)
Console.WriteLine("姓名:{0},年龄:{1}", name, age);

// 方式三:字符串插值(C# 6.0+,推荐)
Console.WriteLine($"姓名:{name},年龄:{age}");

// 格式化控制
double price = 19.99;
Console.WriteLine($"价格:{price:C}");      // 货币格式:¥19.99
Console.WriteLine($"价格:{price:F2}");     // 两位小数:19.99
Console.WriteLine($"年龄:{age:D3}");       // 三位补零:025

易错提醒:WriteLine 的占位符索引从 0 开始,{0} 对应第一个参数,{1} 对应第二个。索引越界会抛出 FormatException。

1.2 输入:Read、ReadLine、ReadKey 的适用场景 ​

csharp
// ReadLine:读取一整行输入(用户按回车结束)
Console.Write("请输入你的名字:");
string? name = Console.ReadLine();  // 返回 string?,可能为 null
Console.WriteLine($"你好,{name ?? "陌生人"}");

// Read:读取单个字符(返回 int,表示字符的 Unicode 编码)
Console.Write("按任意键继续...");
int keyCode = Console.Read();       // 阻塞等待,返回字符编码
Console.ReadKey();                  // 清除缓冲区中的回车符

// ReadKey:读取按键,无需回车,支持功能键
Console.Write("按 Y 确认,N 取消:");
ConsoleKeyInfo key = Console.ReadKey();
if (key.Key == ConsoleKey.Y)
    Console.WriteLine("\n已确认");
else if (key.Key == ConsoleKey.N)
    Console.WriteLine("\n已取消");

方法对比:

方法返回值结束方式适用场景
ReadLine()string?按回车读取文本输入
Read()int按回车逐字符读取(少见)
ReadKey()ConsoleKeyInfo任意键快捷键、密码输入

易错提醒:ReadLine() 返回的是 string?(可空字符串),用户直接按回车会得到空字符串 "",而非 null。处理输入时务必检查空值。

1.3 类型转换异常:最常见的崩溃来源 ​

csharp
// 危险代码:用户输入非数字时直接崩溃
Console.Write("请输入年龄:");
string input = Console.ReadLine() ?? "";
int age = int.Parse(input);  // 输入 "abc" 时抛出 FormatException!

// 安全方案一:int.TryParse(推荐)
Console.Write("请输入年龄:");
if (int.TryParse(Console.ReadLine(), out int safeAge))
{
    Console.WriteLine($"你输入的年龄是:{safeAge}");
}
else
{
    Console.WriteLine("输入无效,请输入数字!");
}

// 安全方案二:循环验证直到输入正确
int validAge;
while (true)
{
    Console.Write("请输入年龄(1-150):");
    if (int.TryParse(Console.ReadLine(), out validAge) && validAge >= 1 && validAge <= 150)
        break;
    Console.WriteLine("输入无效,请重新输入!");
}

二、文件 IO 流:程序的数据仓库 ​

控制台适合调试,但真实程序需要持久化存储。文件 IO 就像餐厅的「食材仓库」——需要分类存放(不同文件类)、按需取用(读取方式)、及时关门(释放资源)。

2.1 File vs FileInfo:静态工具与对象实例 ​

csharp
// File:静态类,适合一次性操作
string path = "test.txt";

// 写入(覆盖)
File.WriteAllText(path, "Hello, File!");

// 追加
File.AppendAllText(path, "\n第二行");

// 读取
string content = File.ReadAllText(path);
string[] lines = File.ReadAllLines(path);

// 存在性检查
if (File.Exists(path))
{
    Console.WriteLine($"文件大小:{new FileInfo(path).Length} 字节");
}

// 删除
File.Delete(path);

// FileInfo:实例类,适合多次操作同一文件
FileInfo fileInfo = new FileInfo("data.txt");
if (!fileInfo.Exists)
{
    using StreamWriter writer = fileInfo.CreateText();
    writer.WriteLine("创建文件");
}

Console.WriteLine($"文件名:{fileInfo.Name}");
Console.WriteLine($"完整路径:{fileInfo.FullName}");
Console.WriteLine($"扩展名:{fileInfo.Extension}");
Console.WriteLine($"创建时间:{fileInfo.CreationTime}");

选择建议:

  • 一次性操作(读一次、写一次)→ File
  • 多次操作同一文件(检查属性、多次读写)→ FileInfo
  • 需要目录信息 → Directory / DirectoryInfo

2.2 StreamReader / StreamWriter:文本流的精细控制 ​

csharp
// StreamWriter:逐行写入,支持编码设置
string logPath = "app.log";

// using 语句确保资源释放(关键!)
using (StreamWriter writer = new StreamWriter(logPath, append: true, Encoding.UTF8))
{
    writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 程序启动");
    writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 用户登录");
} // 这里自动调用 Dispose(),关闭文件流

// StreamReader:逐行读取,内存友好
using (StreamReader reader = new StreamReader(logPath, Encoding.UTF8))
{
    string? line;
    while ((line = reader.ReadLine()) != null)  // 逐行读取,不一次性加载全部
    {
        Console.WriteLine(line);
    }
}

易错提醒:忘记使用 using 或手动调用 Dispose() 会导致文件被占用,其他程序无法访问。这是新手最常见的文件 IO 错误!

csharp
// 错误示例:文件被锁定
StreamWriter badWriter = new StreamWriter("locked.txt");
badWriter.WriteLine("内容");
// 忘记关闭!此时文件被占用,其他进程无法读写
// 程序异常退出后,文件句柄可能仍被系统持有

// 正确做法:using 语句或 try-finally
using StreamWriter goodWriter = new StreamWriter("unlocked.txt");
goodWriter.WriteLine("内容");
// 离开作用域自动释放

2.3 BinaryReader / BinaryWriter:二进制数据的精确读写 ​

csharp
// 写入二进制数据(结构紧凑,适合自定义格式)
string binPath = "data.bin";

using (BinaryWriter writer = new BinaryWriter(File.Open(binPath, FileMode.Create)))
{
    writer.Write(42);                    // int:4 字节
    writer.Write(3.14159);               // double:8 字节
    writer.Write("Hello");               // string:长度前缀 + UTF8 字节
    writer.Write(true);                  // bool:1 字节
    writer.Write(new byte[] { 0xFF, 0x00, 0xAA }); // 原始字节
}

// 读取二进制数据(顺序必须与写入一致!)
using (BinaryReader reader = new BinaryReader(File.Open(binPath, FileMode.Open)))
{
    int intValue = reader.ReadInt32();       // 42
    double doubleValue = reader.ReadDouble(); // 3.14159
    string stringValue = reader.ReadString(); // "Hello"
    bool boolValue = reader.ReadBoolean();   // true
    byte[] bytes = reader.ReadBytes(3);      // { 0xFF, 0x00, 0xAA }

    Console.WriteLine($"读取:{intValue}, {doubleValue}, {stringValue}, {boolValue}");
}

适用场景:自定义文件格式(如游戏存档、图片元数据)、网络协议数据包、高性能序列化。

2.4 大文件分块读取:避免内存爆炸 ​

csharp
// 危险:一次性读取 2GB 文件到内存
// string hugeContent = File.ReadAllText("huge.log"); // OutOfMemoryException!

// 正确:分块读取
string hugePath = "huge.log";
byte[] buffer = new byte[4096]; // 4KB 缓冲区

using FileStream fs = new FileStream(hugePath, FileMode.Open, FileAccess.Read);
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
    // 处理当前块(如统计、搜索、转换)
    ProcessChunk(buffer, bytesRead);

    // 显示进度
    double progress = (double)fs.Position / fs.Length * 100;
    Console.Write($"\r进度:{progress:F1}%");
}

2.5 路径处理:跨平台的陷阱 ​

csharp
// 错误:硬编码路径分隔符(Windows 用 \,Linux/macOS 用 /)
string badPath = "C:\\Users\\Admin\\file.txt"; // 在 Linux 上完全无效!

// 正确:使用 Path 类组合路径
string goodPath = Path.Combine("data", "logs", "app.log");
// Windows: data\logs\app.log
// Linux:   data/logs/app.log

// 常用路径操作
string fullPath = Path.GetFullPath("relative.txt");      // 转绝对路径
string? dirName = Path.GetDirectoryName(fullPath);       // 获取目录
string fileName = Path.GetFileName(fullPath);            // 获取文件名
string extension = Path.GetExtension(fullPath);          // 获取扩展名
string tempPath = Path.GetTempPath();                    // 系统临时目录
string randomFile = Path.GetRandomFileName();            // 随机文件名

// 特殊目录
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string myDocs = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

三、标准输入输出流的底层原理 ​

控制台 IO 的本质是标准流(Standard Streams)。理解这个抽象,能让你把程序输出重定向到文件、管道,甚至网络。

csharp
// 标准流的三件套
// stdin (0):标准输入,默认键盘
// stdout (1):标准输出,默认屏幕
// stderr (2):标准错误,默认屏幕

// 重定向输出到文件
Console.SetOut(new StreamWriter("output.txt"));
Console.WriteLine("这行会写入文件,而非屏幕!");

// 恢复控制台输出(Windows 专用,跨平台需其他方式)
var standardOutput = new StreamWriter(Console.OpenStandardOutput());
standardOutput.AutoFlush = true;
Console.SetOut(standardOutput);

// 从文件读取输入(模拟用户输入)
Console.SetIn(new StreamReader("input.txt"));
string? line = Console.ReadLine(); // 从文件读取,而非键盘

实际应用:单元测试中捕获 Console 输出、日志收集、命令行工具管道操作。


四、异步 IO:不卡界面的关键 ​

同步 IO 会阻塞线程,在 UI 程序中导致界面冻结,在服务器中浪费线程资源。异步 IO 让程序在等待磁盘/网络时去处理其他任务。

csharp
// 同步读取(阻塞线程)
string syncContent = File.ReadAllText("large.txt"); // 线程等待磁盘完成

// 异步读取(不阻塞,适合 UI 程序和高并发服务器)
async Task<string> ReadFileAsync(string path)
{
    using StreamReader reader = new StreamReader(path);
    return await reader.ReadToEndAsync(); // 线程释放,完成后回调
}

// 异步写入
async Task WriteLogAsync(string message)
{
    string logPath = "app.log";
    string logLine = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}{Environment.NewLine}";

    // 异步追加,不阻塞主线程
    await File.AppendAllTextAsync(logPath, logLine);
}

// 并行处理多个文件
async Task ProcessFilesAsync(string[] files)
{
    var tasks = files.Select(async file =>
    {
        string content = await File.ReadAllTextAsync(file);
        return Process(content);
    });

    var results = await Task.WhenAll(tasks); // 并行等待所有完成
}

五、序列化与反序列化:对象的「冷冻与解冻」 ​

序列化把内存中的对象转换为可存储/传输的格式(JSON、XML、二进制),反序列化则还原。就像把新鲜蔬菜冷冻保存,吃的时候再解冻。

csharp
using System.Text.Json;

// 定义可序列化的类
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public List<string> Courses { get; set; } = new();
}

// JSON 序列化(推荐:.NET 内置,高性能)
Student student = new Student
{
    Id = 1,
    Name = "张三",
    Age = 20,
    Courses = { "数学", "英语" }
};

// 序列化:对象 → JSON 字符串
string json = JsonSerializer.Serialize(student, new JsonSerializerOptions
{
    WriteIndented = true  // 格式化输出,便于阅读
});
Console.WriteLine(json);

// 写入文件
File.WriteAllText("student.json", json);

// 反序列化:JSON 字符串 → 对象
string readJson = File.ReadAllText("student.json");
Student? restored = JsonSerializer.Deserialize<Student>(readJson);
Console.WriteLine($"还原:{restored?.Name},{restored?.Age} 岁");

常用序列化方案对比:

方案命名空间优点缺点适用场景
System.Text.Json.NET 内置高性能、低内存功能相对基础Web API、微服务
Newtonsoft.JsonNuGet 包功能丰富、灵活性能略低复杂对象、旧项目兼容
XmlSerializer内置可读性好、标准冗长、慢配置文件、遗留系统
BinaryFormatter内置(已过时)简单不安全、跨平台差避免使用

六、综合实战案例 ​

案例 1:控制台学生信息管理系统的输入输出模块 ​

csharp
using System.Text.Json;

public class StudentManager
{
    private List<Student> _students = new();
    private const string DATA_FILE = "students.json";

    public void Run()
    {
        LoadData();

        while (true)
        {
            Console.WriteLine("\n=== 学生管理系统 ===");
            Console.WriteLine("1. 添加学生  2. 查看所有  3. 保存退出");
            Console.Write("请选择:");

            switch (Console.ReadLine())
            {
                case "1": AddStudent(); break;
                case "2": ShowAll(); break;
                case "3": SaveData(); return;
                default: Console.WriteLine("无效选项!"); break;
            }
        }
    }

    private void AddStudent()
    {
        Console.Write("姓名:");
        string name = Console.ReadLine() ?? "未知";

        Console.Write("年龄:");
        if (!int.TryParse(Console.ReadLine(), out int age) || age <= 0)
        {
            Console.WriteLine("年龄无效!");
            return;
        }

        _students.Add(new Student { Id = _students.Count + 1, Name = name, Age = age });
        Console.WriteLine("添加成功!");
    }

    private void ShowAll()
    {
        foreach (var s in _students)
            Console.WriteLine($"ID:{s.Id} 姓名:{s.Name} 年龄:{s.Age}");
    }

    private void LoadData()
    {
        if (File.Exists(DATA_FILE))
        {
            string json = File.ReadAllText(DATA_FILE);
            _students = JsonSerializer.Deserialize<List<Student>>(json) ?? new();
        }
    }

    private void SaveData()
    {
        string json = JsonSerializer.Serialize(_students, new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(DATA_FILE, json);
        Console.WriteLine("数据已保存!");
    }
}

// 测试验证方法:
// 1. 运行程序,添加 3 个学生
// 2. 选择 3 保存退出
// 3. 重新运行,选择 2 查看所有,确认数据已恢复

案例 2:本地日志文件的读写实现 ​

csharp
public class Logger : IDisposable
{
    private readonly StreamWriter _writer;
    private readonly object _lock = new();

    public Logger(string logPath)
    {
        // 确保目录存在
        Directory.CreateDirectory(Path.GetDirectoryName(logPath)!);

        // 以追加模式打开,UTF8 编码,自动刷新
        _writer = new StreamWriter(logPath, append: true, Encoding.UTF8)
        {
            AutoFlush = true  // 立即写入,避免缓存丢失
        };
    }

    public void Log(string level, string message)
    {
        lock (_lock)  // 多线程安全
        {
            string logLine = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}";
            _writer.WriteLine(logLine);
            Console.WriteLine(logLine);  // 同时输出到控制台
        }
    }

    public void Info(string msg) => Log("INFO", msg);
    public void Error(string msg) => Log("ERROR", msg);

    public void Dispose() => _writer?.Dispose();
}

// 使用示例
using var logger = new Logger("logs/app.log");
logger.Info("程序启动");
logger.Error("测试错误");

// 测试验证方法:
// 1. 运行后检查 logs/app.log 文件是否存在
// 2. 确认日志内容包含时间戳和级别
// 3. 用其他程序(如记事本)打开日志文件,确认未被占用

案例 3:自定义对象的序列化存储 ​

csharp
using System.Text.Json;
using System.Text.Json.Serialization;

public class GameSave
{
    public string PlayerName { get; set; } = "";
    public int Level { get; set; }
    public int Health { get; set; }
    public List<string> Inventory { get; set; } = new();

    [JsonIgnore]  // 不序列化此属性
    public string TempData { get; set; } = "";
}

public class SaveManager
{
    private const string SAVE_FILE = "savegame.json";
    private readonly JsonSerializerOptions _options = new() { WriteIndented = true };

    public void Save(GameSave data)
    {
        string json = JsonSerializer.Serialize(data, _options);
        File.WriteAllText(SAVE_FILE, json);
        Console.WriteLine("存档成功!");
    }

    public GameSave? Load()
    {
        if (!File.Exists(SAVE_FILE))
        {
            Console.WriteLine("无存档文件");
            return null;
        }

        try
        {
            string json = File.ReadAllText(SAVE_FILE);
            return JsonSerializer.Deserialize<GameSave>(json);
        }
        catch (JsonException ex)
        {
            Console.WriteLine($"存档损坏:{ex.Message}");
            return null;
        }
    }
}

// 测试验证方法:
// 1. 创建 GameSave 对象,设置属性,调用 Save
// 2. 检查 savegame.json 内容格式是否正确
// 3. 注释掉部分属性,测试 Load 的反序列化容错
// 4. 手动修改 JSON 制造格式错误,验证异常捕获

知识点思维导图 ​

C# 输入与输出
│
├─ Console 控制台
│  ├─ 输出:Write / WriteLine / 格式化($ 插值推荐)
│  ├─ 输入:ReadLine(行)/ Read(字符)/ ReadKey(按键)
│  ├─ 类型安全:TryParse 防崩溃,循环验证输入
│  └─ 易错点:ReadLine 返回 string?,索引从 0 开始
│
├─ 文件 IO 流
│  ├─ File vs FileInfo:静态工具 vs 实例对象
│  ├─ 文本流:StreamReader / StreamWriter
│  │  ├─ 编码设置(UTF8 推荐)
│  │  ├─ 追加模式(append: true)
│  │  └─ 逐行读取(内存友好)
│  ├─ 二进制流:BinaryReader / BinaryWriter
│  │  └─ 顺序必须一致,适合自定义格式
│  ├─ 大文件:分块读取(4KB 缓冲区)
│  ├─ 路径处理:Path.Combine 跨平台,特殊目录获取
│  └─ 资源释放:using 语句(关键!避免文件占用)
│
├─ 标准流底层
│  ├─ stdin / stdout / stderr 三流模型
│  └─ 重定向:Console.SetOut / SetIn
│
├─ 异步 IO
│  ├─ ReadAllTextAsync / WriteAllTextAsync
│  ├─ await 释放线程,提升吞吐量
│  └─ Task.WhenAll 并行处理多文件
│
└─ 序列化
   ├─ JSON:System.Text.Json(内置推荐)
   ├─ XML:XmlSerializer(可读性好)
   ├─ 特性控制:[JsonIgnore] 等
   └─ 安全:避免 BinaryFormatter(已过时)