Skip to content

C# 变量的输入输出与交换:从储物柜到数据搬运工 ​

开篇:程序的「储物柜管理系统」 ​

想象你经营一家储物柜仓库。每个柜子有一个编号(变量名),里面可以存放物品(值)。有些柜子很小,只能放一张卡片(int);有些柜子很大,能放下一整箱书(string);还有些柜子是「虚拟柜」——你拿到的是一张取物凭证,真正的货物存放在另一个大仓库里(引用类型)。

C# 的变量系统就是这套储物柜管理体系。输入是把外面的物品搬进柜子,输出是把柜子里的物品展示给别人看,交换则是把两个柜子里的物品互换位置。掌握这套体系,你的程序才能与外界「对话」,数据才能灵活流动。


一、变量基础:储物柜的「规格说明书」 ​

1.1 什么是变量? ​

变量是内存中一块命名的存储区域,用于存放程序运行时的数据。就像储物柜有编号和容量一样,变量有名称和类型。

csharp
// 声明并初始化变量
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;            // 往柜子里放 100

1.2 值类型 vs 引用类型:储物柜的「两种形态」 ​

这是理解 C# 变量的核心分野。

值类型(Value Types)          引用类型(Reference Types)
┌─────────────────┐            ┌─────────────────┐
│   储物柜本身      │            │   取物凭证        │
│   里面直接放物品   │            │   物品在大仓库      │
│   int, double    │            │   string, object │
│   struct, enum   │            │   class, array   │
└─────────────────┘            └─────────────────┘
csharp
// 值类型:直接存储数据
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、\0null
常见类型int、double、structstring、class、array

二、控制台输入:把用户的话「搬进」变量 ​

控制台输入是程序与用户的「对话窗口」。C# 提供三种输入方式,各有适用场景。

2.1 Console.ReadLine():读取一整行 ​

生活类比:像听别人把一整句话说完,再回应。

csharp
// 基础用法:读取用户输入的一整行文本
Console.Write("请输入你的名字:");
string name = Console.ReadLine();  // 等待用户输入,按回车结束
Console.WriteLine($"你好,{name}!");

// 输入:张三
// 输出:你好,张三!

底层差异:ReadLine() 读取直到换行符的所有字符,返回 string 类型。

2.2 Console.Read():读取单个字符 ​

生活类比:像电报员,一个字一个字地接收。

csharp
// 基础用法:读取单个字符的 Unicode 编码
Console.Write("请输入一个字符:");
int charCode = Console.Read();  // 返回 int,表示字符的编码
Console.WriteLine($"你输入的编码是:{charCode},对应字符:{(char)charCode}");

// 输入:A
// 输出:你输入的编码是:65,对应字符:A

常见坑点:Read() 返回的是 int(Unicode 编码),不是 char。而且不会自动消费换行符。

csharp
// ❌ 坑: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():读取按键(无需回车) ​

生活类比:像游戏手柄,按下立即响应。

csharp
// 基础用法:读取用户按下的键,立即返回,无需回车
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 字符串转数值:异常处理实战 ​

用户输入的是字符串,需要转换为数值类型。转换可能失败,必须处理异常。

csharp
// 场景:读取用户年龄
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 的选择:

csharp
// 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 ​

csharp
// Write:输出后不换行
Console.Write("Hello");
Console.Write("World");  // 输出:HelloWorld

// WriteLine:输出后换行
Console.WriteLine("Hello");
Console.WriteLine("World");  // 输出:Hello\nWorld\n

3.2 字符串插值 $:最直观的拼接 ​

csharp
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.5

3.3 复合格式化:{0}、{1} 占位符 ​

csharp
// 复合格式化:按索引引用参数
Console.WriteLine("姓名:{0},年龄:{1},姓名重复:{0}", name, age);
// 输出:姓名:张三,年龄:25,姓名重复:张三

// 格式化字符串
Console.WriteLine("价格:{0:C},日期:{1:yyyy-MM-dd}", 99.9, DateTime.Now);
// 输出:价格:¥99.90,日期:2025-01-15

3.4 逐字插值 $@ 与 @$:多行文本 ​

csharp
// $@:插值 + 逐字(不转义 \)
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 控制台美化:光标定位与颜色 ​

csharp
// 设置控制台标题
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 跨平台兼容注意事项 ​

csharp
// .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。

csharp
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 原来的位置。

csharp
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

风险:数值溢出!

csharp
// ❌ 危险:大数相加溢出
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 异或交换:二进制的「位魔术」 ​

生活类比:用密码锁的异或特性——同一个密码锁,按两次就恢复原状。

csharp
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 的东西同时装箱,一次性互换位置。

csharp
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=Hello

4.5 引用类型的深层交换:凭证交换 vs 物品交换 ​

这是最容易混淆的场景。引用类型的「交换」有两层含义:

csharp
// 场景:交换两个数组的内容
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,内容交换不需要
csharp
// 值类型的方法交换:必须 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:以下代码输出什么? ​

csharp
int a = 5;
int b = a++;
Console.WriteLine($"a={a}, b={b}");
点击查看解析

解析:

  • a++ 是后缀自增:先取 a 的值 5 赋给 b,然后 a 自增为 6
  • 输出:a=6, b=5

关键:后缀模式「先用后变」,前缀模式「先变后用」。

面试题 2:以下代码能否交换成功?为什么? ​

csharp
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 参数

csharp
void Swap(ref int x, ref int y)
{
    (x, y) = (y, x);
}
Swap(ref a, ref b);  // 现在 a=2, b=1

面试题 3:如何安全地读取用户输入的数字? ​

点击查看解析

推荐方案:

csharp
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:以下代码输出什么?解释原因。 ​

csharp
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 控制台调试技巧 ​

csharp
// 方法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}");
#endif

7.2 单元测试验证 ​

csharp
// 使用 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 内存查看(进阶) ​

csharp
// 使用 unsafe 代码查看变量内存地址
unsafe
{
    int x = 42;
    int* ptr = &x;
    Console.WriteLine($"x 的值:{x},地址:{(IntPtr)ptr}");
}

结语 ​

变量的输入输出是程序与世界的「对话窗口」,变量交换是数据流动的「交通枢纽」。从 Console.ReadLine() 的耐心等待,到 (a, b) = (b, a) 的一行优雅,C# 提供了从基础到高级的完整工具链。

记住:输入要防御(异常处理),输出要清晰(格式化),交换要安全(优先用解构语法)。掌握这些,你的程序就能稳健地处理数据,优雅地与用户交互。