C# 变量的输入输出与交换:从储物柜到数据搬运工
开篇:程序的「储物柜管理系统」
想象你经营一家储物柜仓库。每个柜子有一个编号(变量名),里面可以存放物品(值)。有些柜子很小,只能放一张卡片(int);有些柜子很大,能放下一整箱书(string);还有些柜子是「虚拟柜」——你拿到的是一张取物凭证,真正的货物存放在另一个大仓库里(引用类型)。
C# 的变量系统就是这套储物柜管理体系。输入是把外面的物品搬进柜子,输出是把柜子里的物品展示给别人看,交换则是把两个柜子里的物品互换位置。掌握这套体系,你的程序才能与外界「对话」,数据才能灵活流动。
一、变量基础:储物柜的「规格说明书」
1.1 什么是变量?
变量是内存中一块命名的存储区域,用于存放程序运行时的数据。就像储物柜有编号和容量一样,变量有名称和类型。
// 声明并初始化变量
int age = 25; // int 类型:4 字节整数,范围约 ±21 亿
string name = "张三"; // string 类型:引用类型,存储文本
double price = 9.99; // double 类型:8 字节浮点数
bool isActive = true; // bool 类型:1 字节,true 或 false
// 先声明,后赋值
int count; // 声明一个 int 柜子,暂时为空
count = 100; // 往柜子里放 1001.2 值类型 vs 引用类型:储物柜的「两种形态」
这是理解 C# 变量的核心分野。
值类型(Value Types) 引用类型(Reference Types)
┌─────────────────┐ ┌─────────────────┐
│ 储物柜本身 │ │ 取物凭证 │
│ 里面直接放物品 │ │ 物品在大仓库 │
│ int, double │ │ string, object │
│ struct, enum │ │ class, array │
└─────────────────┘ └─────────────────┘// 值类型:直接存储数据
int a = 10;
int b = a; // 复制一份数据,b 是独立的柜子
b = 20; // 修改 b 不影响 a
Console.WriteLine(a); // 输出:10
Console.WriteLine(b); // 输出:20
// 引用类型:存储的是「取物凭证」(内存地址)
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // 复制的是凭证,两个变量指向同一个数组
arr2[0] = 999; // 通过 arr2 修改,arr1 也会变!
Console.WriteLine(arr1[0]); // 输出:999(不是 1!)关键差异:
| 特性 | 值类型 | 引用类型 |
|---|---|---|
| 存储内容 | 数据本身 | 数据的内存地址(引用) |
| 赋值行为 | 复制数据 | 复制引用(共享数据) |
| 内存位置 | 栈(Stack) | 堆(Heap)+ 栈(引用) |
| 默认值 | 0、false、\0 | null |
| 常见类型 | int、double、struct | string、class、array |
二、控制台输入:把用户的话「搬进」变量
控制台输入是程序与用户的「对话窗口」。C# 提供三种输入方式,各有适用场景。
2.1 Console.ReadLine():读取一整行
生活类比:像听别人把一整句话说完,再回应。
// 基础用法:读取用户输入的一整行文本
Console.Write("请输入你的名字:");
string name = Console.ReadLine(); // 等待用户输入,按回车结束
Console.WriteLine($"你好,{name}!");
// 输入:张三
// 输出:你好,张三!底层差异:ReadLine() 读取直到换行符的所有字符,返回 string 类型。
2.2 Console.Read():读取单个字符
生活类比:像电报员,一个字一个字地接收。
// 基础用法:读取单个字符的 Unicode 编码
Console.Write("请输入一个字符:");
int charCode = Console.Read(); // 返回 int,表示字符的编码
Console.WriteLine($"你输入的编码是:{charCode},对应字符:{(char)charCode}");
// 输入:A
// 输出:你输入的编码是:65,对应字符:A常见坑点:Read() 返回的是 int(Unicode 编码),不是 char。而且不会自动消费换行符。
// ❌ 坑:Read() 后残留换行符,影响下次读取
int first = Console.Read(); // 用户输入 "A\n"(A + 回车)
int second = Console.Read(); // 读到的是 '\n'(编码 10),不是用户预期的字符
// ✅ 解决:额外读取一次,消费掉换行符
int first2 = Console.Read();
Console.Read(); // 消费换行符
int second2 = Console.Read(); // 现在正常读取下一个字符2.3 Console.ReadKey():读取按键(无需回车)
生活类比:像游戏手柄,按下立即响应。
// 基础用法:读取用户按下的键,立即返回,无需回车
Console.WriteLine("按任意键继续...");
ConsoleKeyInfo keyInfo = Console.ReadKey(); // 按下即触发
Console.WriteLine($"\n你按下了:{keyInfo.KeyChar},键码:{keyInfo.Key}");
// 进阶:隐藏输入(密码场景)
Console.Write("请输入密码:");
string password = "";
while (true)
{
ConsoleKeyInfo key = Console.ReadKey(intercept: true); // intercept: 不显示按键
if (key.Key == ConsoleKey.Enter)
break;
password += key.KeyChar;
Console.Write("*"); // 显示星号掩码
}
Console.WriteLine($"\n密码长度:{password.Length}");三种输入方式对比:
| 方法 | 返回类型 | 是否需回车 | 适用场景 |
|---|---|---|---|
ReadLine() | string | 是 | 文本输入、菜单选择 |
Read() | int | 是 | 单字符读取、协议解析 |
ReadKey() | ConsoleKeyInfo | 否 | 密码输入、游戏控制、快捷键 |
2.4 字符串转数值:异常处理实战
用户输入的是字符串,需要转换为数值类型。转换可能失败,必须处理异常。
// 场景:读取用户年龄
Console.Write("请输入你的年龄:");
string input = Console.ReadLine();
// ❌ 危险:直接转换,输入 "abc" 会崩溃
// int age = int.Parse(input); // FormatException!
// ✅ 方案1:TryParse(推荐,无异常开销)
if (int.TryParse(input, out int age))
{
Console.WriteLine($"你的年龄是:{age}岁");
}
else
{
Console.WriteLine("输入格式错误,请输入数字!");
}
// ✅ 方案2:try-catch(适合需要精确异常类型的场景)
try
{
int age2 = int.Parse(input);
// 检查范围
if (age2 < 0 || age2 > 150)
throw new ArgumentOutOfRangeException("年龄超出合理范围");
Console.WriteLine($"年龄验证通过:{age2}");
}
catch (FormatException)
{
Console.WriteLine("格式错误:请输入纯数字,如 25");
}
catch (OverflowException)
{
Console.WriteLine("数值过大:int 最大支持 2147483647");
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine($"范围错误:{ex.Message}");
}TryParse vs Parse 的选择:
// TryParse:返回 bool,不抛异常,性能更好
bool success = int.TryParse("123", out int result); // true, result = 123
// Parse:成功返回数值,失败抛异常
int value = int.Parse("123"); // 直接返回 123
// 通用原则:用户输入用 TryParse,程序内部确定的数据用 Parse三、控制台输出:把数据「展示」给用户
输出是程序的「表达」。C# 提供多种格式化方式,让数据展示更专业。
3.1 基础输出:Write 与 WriteLine
// Write:输出后不换行
Console.Write("Hello");
Console.Write("World"); // 输出:HelloWorld
// WriteLine:输出后换行
Console.WriteLine("Hello");
Console.WriteLine("World"); // 输出:Hello\nWorld\n3.2 字符串插值 $:最直观的拼接
string name = "张三";
int age = 25;
double height = 175.5;
// 字符串插值:用 {} 嵌入变量
Console.WriteLine($"姓名:{name},年龄:{age},身高:{height}cm");
// 输出:姓名:张三,年龄:25,身高:175.5cm
// 支持表达式和格式化
Console.WriteLine($"明年年龄:{age + 1},身高保留1位:{height:F1}");
// 输出:明年年龄:26,身高保留1位:175.53.3 复合格式化:{0}、{1} 占位符
// 复合格式化:按索引引用参数
Console.WriteLine("姓名:{0},年龄:{1},姓名重复:{0}", name, age);
// 输出:姓名:张三,年龄:25,姓名重复:张三
// 格式化字符串
Console.WriteLine("价格:{0:C},日期:{1:yyyy-MM-dd}", 99.9, DateTime.Now);
// 输出:价格:¥99.90,日期:2025-01-153.4 逐字插值 $@ 与 @$:多行文本
// $@:插值 + 逐字(不转义 \)
string path = "C:\\Users\\Admin";
Console.WriteLine($@"文件路径:{path}
第二行直接换行
缩进保留");
// 输出:
// 文件路径:C:\Users\Admin
// 第二行直接换行
// 缩进保留
// @$:逐字 + 插值(C# 8.0 前必须 $@,8.0 后 $@ 和 @$ 均可)
string sql = @$"SELECT * FROM Users
WHERE Name = '{name}'
AND Age > {age}";3.5 控制台美化:光标定位与颜色
// 设置控制台标题
Console.Title = "学生管理系统";
// 设置文本颜色
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("操作成功!");
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("警告信息!");
Console.ResetColor(); // 恢复默认
// 背景色
Console.BackgroundColor = ConsoleColor.DarkBlue;
Console.WriteLine("高亮显示");
Console.ResetColor();
// 光标定位
Console.SetCursorPosition(10, 5); // 列10,行5
Console.Write("在这里输出");
// 清屏
Console.Clear();
// 窗口大小(Windows 有效,Linux/macOS 可能受限)
// Console.SetWindowSize(80, 25);3.6 跨平台兼容注意事项
// .NET Core / .NET 5+ 跨平台差异
// 1. 控制台颜色:Linux/macOS 支持,但某些终端可能显示异常
// 2. 窗口大小:Windows 支持设置,Linux 通常由终端控制
// 3. 按键检测:Console.ReadKey() 在 Linux 重定向输入时可能抛出 InvalidOperationException
// 跨平台安全的输入检测
if (!Console.IsInputRedirected)
{
Console.WriteLine("按任意键继续...");
Console.ReadKey();
}
else
{
Console.WriteLine("输入被重定向,跳过按键检测");
Console.ReadLine(); // 使用 ReadLine 替代
}
// 跨平台安全的颜色设置
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
Console.ForegroundColor = ConsoleColor.Green;
}
else
{
// Linux/macOS:使用 ANSI 转义序列
Console.Write("\u001b[32m"); // 绿色
}
Console.WriteLine("跨平台颜色输出");
Console.ResetColor();四、变量交换:两种「搬运策略」
变量交换是编程中的经典问题。根据变量类型和场景,有四种主流方案。
4.1 临时变量法:最稳妥的「中转站」
生活类比:搬家时用一辆卡车中转——先把 A 的东西搬上车,B 搬到 A,再把车上的搬到 B。
int a = 10, b = 20;
Console.WriteLine($"交换前:a={a}, b={b}");
// 三步交换
int temp = a; // 第一步:a 搬到卡车
a = b; // 第二步:b 搬到 a 的位置
b = temp; // 第三步:卡车上的搬到 b 的位置
Console.WriteLine($"交换后:a={a}, b={b}");
// 输出:交换前:a=10, b=20 → 交换后:a=20, b=10内存图解:
交换前: 第一步: 第二步: 第三步:
a: [10] a: [10] a: [20] a: [20]
b: [20] b: [20] b: [20] b: [10]
temp: [] temp: [10] temp: [10] temp: [10]优点:逻辑清晰,适用于所有类型。缺点:需要额外内存空间。
4.2 加减法交换:不用临时变量的「数学魔术」
生活类比:两个人换座位,不借助第三把椅子——A 先站到 B 的位置,B 再站到 A 原来的位置。
int a = 10, b = 20;
// 加减法交换(仅限数值类型)
a = a + b; // a = 10 + 20 = 30
b = a - b; // b = 30 - 20 = 10(原来的 a)
a = a - b; // a = 30 - 10 = 20(原来的 b)
Console.WriteLine($"a={a}, b={b}"); // a=20, b=10风险:数值溢出!
// ❌ 危险:大数相加溢出
int big1 = int.MaxValue; // 2147483647
int big2 = 1;
// big1 = big1 + big2; // 溢出!结果为 -2147483648
// ✅ 解决:先检查范围,或用更大的类型
long safe1 = int.MaxValue;
long safe2 = 1;
safe1 = safe1 + safe2; // long 不溢出4.3 异或交换:二进制的「位魔术」
生活类比:用密码锁的异或特性——同一个密码锁,按两次就恢复原状。
int a = 10; // 二进制:1010
int b = 20; // 二进制:10100
// 异或交换(仅限整数类型)
a = a ^ b; // a = 1010 ^ 10100 = 11110
b = a ^ b; // b = 11110 ^ 10100 = 1010 = 10(原来的 a)
a = a ^ b; // a = 11110 ^ 1010 = 10100 = 20(原来的 b)
Console.WriteLine($"a={a}, b={b}"); // a=20, b=10原理:x ^ x = 0,x ^ 0 = x。异或满足交换律和结合律。
限制:仅适用于整数类型(int、long 等),不适用于浮点数和引用类型。
4.4 解构语法:C# 7.0 的「一行流」
生活类比:搬家公司的一次性打包服务——把 A 和 B 的东西同时装箱,一次性互换位置。
int a = 10, b = 20;
// 解构交换:C# 7.0+ 语法糖,底层仍使用临时变量,但代码极简
(a, b) = (b, a);
Console.WriteLine($"a={a}, b={b}"); // a=20, b=10
// 适用于任何类型
string s1 = "Hello", s2 = "World";
(s1, s2) = (s2, s1);
Console.WriteLine($"s1={s1}, s2={s2}"); // s1=World, s2=Hello4.5 引用类型的深层交换:凭证交换 vs 物品交换
这是最容易混淆的场景。引用类型的「交换」有两层含义:
// 场景:交换两个数组的内容
int[] arr1 = { 1, 2, 3 };
int[] arr2 = { 10, 20, 30 };
// ❌ 浅交换:只交换引用(凭证),不交换内容
int[] temp = arr1;
arr1 = arr2;
arr2 = temp;
// 现在 arr1 指向 {10,20,30},arr2 指向 {1,2,3}
// 但数组本身的内容没变,只是变量指向变了
// ✅ 深交换:交换数组内部的内容
void SwapArrayContents(int[] x, int[] y)
{
if (x.Length != y.Length)
throw new ArgumentException("数组长度必须相同");
for (int i = 0; i < x.Length; i++)
{
(x[i], y[i]) = (y[i], x[i]); // 逐个元素交换
}
}
int[] data1 = { 1, 2, 3 };
int[] data2 = { 10, 20, 30 };
SwapArrayContents(data1, data2);
// data1 = {10, 20, 30}, data2 = {1, 2, 3}(内容真正交换了)内存图解:
浅交换(引用交换): 深交换(内容交换):
arr1 ──→ [1, 2, 3] arr1 ──→ [10, 20, 30]
↑ 交换引用 ↑ 内容互换
arr2 ──→ [10, 20, 30] arr2 ──→ [1, 2, 3]
两个数组对象本身没变, 两个数组的内容真正改变了,
只是变量的指向变了。 变量指向不变。值类型 vs 引用类型交换的本质差异:
| 对比项 | 值类型交换 | 引用类型交换 |
|---|---|---|
| 交换内容 | 栈上的数据本身 | 栈上的引用地址 |
| 是否影响原数据 | 是(数据被替换) | 取决于是否修改堆数据 |
| 方法内交换 | 需要 ref 参数 | 引用交换需 ref,内容交换不需要 |
// 值类型的方法交换:必须 ref
void SwapValue(ref int x, ref int y)
{
(x, y) = (y, x);
}
int m = 1, n = 2;
SwapValue(ref m, ref n);
Console.WriteLine($"m={m}, n={n}"); // m=2, n=1
// 引用类型的方法交换:交换引用需要 ref,交换内容不需要
void SwapReference(ref int[] x, ref int[] y)
{
(x, y) = (y, x); // 交换引用
}
void SwapContent(int[] x, int[] y)
{
for (int i = 0; i < x.Length; i++)
(x[i], y[i]) = (y[i], x[i]); // 交换内容
}五、知识点总结
输入输出速查
| 操作 | 方法 | 关键点 |
|---|---|---|
| 读一行 | Console.ReadLine() | 返回 string,需回车 |
| 读单字符 | Console.Read() | 返回 int(编码),注意残留换行符 |
| 读按键 | Console.ReadKey() | 无需回车,可隐藏输入 |
| 字符串转数值 | int.TryParse() | 推荐,无异常 |
| 格式化输出 | $"{var:F2}" | 字符串插值 + 格式说明符 |
| 多行输出 | $@"..." 或 @$"..." | 逐字插值,保留换行和缩进 |
| 跨平台兼容 | Console.IsInputRedirected | 检测输入重定向 |
变量交换速查
| 方案 | 代码 | 适用类型 | 优点 | 缺点 |
|---|---|---|---|---|
| 临时变量 | temp=a; a=b; b=temp; | 所有类型 | 安全通用 | 需额外空间 |
| 加减法 | a=a+b; b=a-b; a=a-b; | 数值类型 | 省空间 | 溢出风险 |
| 异或 | a^=b; b^=a; a^=b; | 整数类型 | 省空间 | 可读性差 |
| 解构 | (a,b)=(b,a) | 所有类型 | 极简安全 | C# 7.0+ |
六、常见面试题解析
面试题 1:以下代码输出什么?
int a = 5;
int b = a++;
Console.WriteLine($"a={a}, b={b}");点击查看解析
解析:
a++是后缀自增:先取a的值5赋给b,然后a自增为6- 输出:
a=6, b=5
关键:后缀模式「先用后变」,前缀模式「先变后用」。
面试题 2:以下代码能否交换成功?为什么?
void Swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int a = 1, b = 2;
Swap(a, b);
Console.WriteLine($"a={a}, b={b}");点击查看解析
解析:
- 不能交换成功。
int是值类型,方法参数默认是值传递——方法内操作的是副本。 - 输出:
a=1, b=2(未交换)
修正:使用 ref 参数
void Swap(ref int x, ref int y)
{
(x, y) = (y, x);
}
Swap(ref a, ref b); // 现在 a=2, b=1面试题 3:如何安全地读取用户输入的数字?
点击查看解析
推荐方案:
int ReadInt(string prompt)
{
while (true)
{
Console.Write(prompt);
string input = Console.ReadLine();
if (int.TryParse(input, out int result))
return result;
Console.WriteLine("输入无效,请输入整数!");
}
}
// 使用
int age = ReadInt("请输入年龄:");要点:循环 + TryParse + 友好提示,避免程序崩溃。
面试题 4:以下代码输出什么?解释原因。
string s1 = "Hello";
string s2 = s1;
s2 = "World";
Console.WriteLine(s1);点击查看解析
解析:
- 输出:
Hello - 原因:
string是引用类型,但具有不可变性(Immutable)。s2 = "World"会创建新字符串,而不是修改原字符串。 - 这与数组等可变引用类型不同:
int[] arr2 = arr1; arr2[0] = 99;会修改共享数据。
关键区分:不可变引用类型 vs 可变引用类型。
面试题 5:异或交换法的数学原理是什么?
点击查看解析
原理:
异或运算满足以下性质:
a ^ a = 0(自反性)a ^ 0 = a(单位元)a ^ b = b ^ a(交换律)(a ^ b) ^ c = a ^ (b ^ c)(结合律)
推导:
初始:a = A, b = B
第一步:a = a ^ b = A ^ B
第二步:b = a ^ b = (A ^ B) ^ B = A ^ (B ^ B) = A ^ 0 = A
第三步:a = a ^ b = (A ^ B) ^ A = (A ^ A) ^ B = 0 ^ B = B
结果:a = B, b = A ✓限制:仅适用于整数类型,不适用于浮点数和引用类型。
七、调试验证方法
7.1 控制台调试技巧
// 方法1:输出中间值
int a = 10, b = 20;
Console.WriteLine($"[DEBUG] 交换前:a={a}, b={b}");
(a, b) = (b, a);
Console.WriteLine($"[DEBUG] 交换后:a={a}, b={b}");
// 方法2:使用 Debug.WriteLine(仅 Debug 模式输出)
System.Diagnostics.Debug.WriteLine($"调试信息:{a}");
// 方法3:条件编译
#if DEBUG
Console.WriteLine($"[DEBUG] 当前值:{a}");
#endif7.2 单元测试验证
// 使用 xUnit / NUnit / MSTest 验证交换逻辑
[Test]
public void TestSwap()
{
int a = 5, b = 10;
(a, b) = (b, a);
Assert.Equal(10, a);
Assert.Equal(5, b);
}7.3 内存查看(进阶)
// 使用 unsafe 代码查看变量内存地址
unsafe
{
int x = 42;
int* ptr = &x;
Console.WriteLine($"x 的值:{x},地址:{(IntPtr)ptr}");
}结语
变量的输入输出是程序与世界的「对话窗口」,变量交换是数据流动的「交通枢纽」。从 Console.ReadLine() 的耐心等待,到 (a, b) = (b, a) 的一行优雅,C# 提供了从基础到高级的完整工具链。
记住:输入要防御(异常处理),输出要清晰(格式化),交换要安全(优先用解构语法)。掌握这些,你的程序就能稳健地处理数据,优雅地与用户交互。