CSharp(三十七) 结构体在方法中的使用 & 结构体与类的对比
第一部分:结构体在方法中的使用
一、结构体作为方法参数
1.1 基本传参——传递的是"复印件"
把结构体传给方法时,默认传递的是一份拷贝,方法里改了不影响外面。
public struct Point
{
public int X;
public int Y;
}
// 方法接收一个 Point 参数
static void Move(Point p)
{
p.X += 10;
p.Y += 10;
Console.WriteLine($"方法内:X={p.X}, Y={p.Y}");
}
// 调用
Point point = new Point();
point.X = 5;
point.Y = 3;
Console.WriteLine($"调用前:X={point.X}, Y={point.Y}");
Move(point);
Console.WriteLine($"调用后:X={point.X}, Y={point.Y}");
输出:
调用前:X=5, Y=3
方法内:X=15, Y=13
调用后:X=5, Y=3 ← 外面没变!因为是拷贝
图解:方法拿到的是复印件,在上面画画不会影响原件。
调用前: point ──> [X=5, Y=3]
↓ 传参时复制
Move内: p ──> [X=5, Y=3] → [X=15, Y=13]
↓ 方法结束,p 销毁
调用后: point ──> [X=5, Y=3] ← 原件毫发无损
1.2 多个结构体参数
public struct Rectangle
{
public int Width;
public int Height;
}
// 接收两个结构体,计算总面积
static int SumArea(Rectangle r1, Rectangle r2)
{
return (r1.Width * r1.Height) + (r2.Width * r2.Height);
}
// 使用
Rectangle a = new Rectangle() { Width = 5, Height = 3 };
Rectangle b = new Rectangle() { Width = 4, Height = 2 };
int total = SumArea(a, b);
Console.WriteLine($"总面积: {total}"); // 输出: 23 (15+8)
二、想让方法修改原值怎么办?—— 用 ref / out / in
既然默认传的是复印件,那想直接修改原件怎么办?C# 提供了三个关键字:
2.1 ref —— 传递引用(可读可写)
public struct Point
{
public int X;
public int Y;
}
// 加了 ref,传递的就是"原件本身"而不是复印件
static void MoveByRef(ref Point p)
{
p.X += 10;
p.Y += 10;
}
// 使用(调用时也必须加 ref)
Point point = new Point() { X = 5, Y = 3 };
Console.WriteLine($"调用前:X={point.X}, Y={point.Y}");
MoveByRef(ref point);
Console.WriteLine($"调用后:X={point.X}, Y={point.Y}");
输出:
调用前:X=5, Y=3
调用后:X=15, Y=13 ← 原件被修改了!
图解:ref 就像把原件直接交给方法,而不是给复印件。
调用前: point ──> [X=5, Y=3]
↓ ref 传递,直接指向同一块内存
MoveByRef内:p ──↗(同一个东西) → [X=15, Y=13]
调用后: point ──> [X=15, Y=13] ← 原件被改了
重要规则:使用
ref时,变量必须先初始化才能传入。
2.2 out —— 纯输出(方法内必须赋值)
public struct Student
{
public string Name;
public int Age;
public void Show()
{
Console.WriteLine($"姓名: {Name}, 年龄: {Age}");
}
}
// out 参数:方法负责给它赋值,返回给调用者
static void CreateStudent(out Student s)
{
// 在方法内必须给所有字段赋值
s.Name = "张三";
s.Age = 18;
}
// 使用(调用时也必须加 out)
Student stu; // 声明即可,不需要初始化
CreateStudent(out stu);
stu.Show(); // 输出: 姓名: 张三, 年龄: 18
重要规则:使用
out时,调用前变量可以未初始化,但方法内必须赋值。
2.3 in —— 只读引用(C# 7.2+)
// in 参数:只读的引用,不能修改,但避免了拷贝开销
static double Distance(in Point p)
{
// p.X = 10; ← 编译错误!in 参数不能修改
return Math.Sqrt(p.X * p.X + p.Y * p.Y);
}
// 使用(调用时的 in 可以省略)
Point p = new Point() { X = 3, Y = 4 };
double d = Distance(in p);
Console.WriteLine($"距离: {d}"); // 输出: 5
2.4 三兄弟速查表
| 关键字 | 能读? | 能写? | 调用前要初始化? | 方法内必须赋值? | 适用场景 |
|---|---|---|---|---|---|
| 无 | 能 | 能(改的是拷贝) | 是 | 否 | 只需要读取值 |
| ref | 能 | 能 | 是 | 否 | 要读取并修改原值 |
| out | 能 | 能 | 否 | 是 | 纯粹返回一个值 |
| in | 能 | 不能 | 是 | 否 | 大数据只读,省拷贝 |
记忆口诀:ref 是"原件给我看和改",out 是"我帮你填好还给你",in 是"原件给我看看但不准改"。
三、结构体作为方法返回值
3.1 基本返回
public struct Point
{
public int X;
public int Y;
}
// 方法返回一个结构体
static Point CreatePoint(int x, int y)
{
Point p = new Point();
p.X = x;
p.Y = y;
return p; // 返回的是值拷贝
}
// 使用
Point result = CreatePoint(10, 20);
Console.WriteLine($"X={result.X}, Y={result.Y}"); // 输出: X=10, Y=20
3.2 返回多个值的常见模式——用元组 vs 用结构体
// 方式一:返回结构体(更清晰,适合复用)
public struct CalculatorResult
{
public double Sum;
public double Difference;
public double Product;
public double Quotient;
}
static CalculatorResult Calculate(double a, double b)
{
CalculatorResult r = new CalculatorResult();
r.Sum = a + b;
r.Difference = a - b;
r.Product = a * b;
r.Quotient = a / b;
return r;
}
// 使用
CalculatorResult result = Calculate(10, 3);
Console.WriteLine($"和:{result.Sum} 差:{result.Difference} "
+ $"积:{result.Product} 商:{result.Quotient:F2}");
// 输出: 和:13 差:7 积:30 商:3.33
3.3 完整示例:购物车计算
using System;
// 商品结构体
public struct Product
{
public string Name;
public double Price;
public int Quantity;
}
// 计算结果结构体
public struct OrderResult
{
public double TotalPrice; // 总价
public double Discount; // 折扣金额
public double FinalPrice; // 实付金额
public int BonusPoints; // 赠送积分
public void Show()
{
Console.WriteLine($"总价: {TotalPrice:C}");
Console.WriteLine($"折扣: {Discount:C}");
Console.WriteLine($"实付: {FinalPrice:C}");
Console.WriteLine($"积分: {BonusPoints}");
}
}
class Program
{
// 计算订单结果
static OrderResult CalculateOrder(Product[] products)
{
OrderResult result = new OrderResult();
// 计算总价
foreach (Product p in products)
{
result.TotalPrice += p.Price * p.Quantity;
}
// 满 100 打 9 折
result.Discount = result.TotalPrice >= 100 ? result.TotalPrice * 0.1 : 0;
result.FinalPrice = result.TotalPrice - result.Discount;
// 每 10 元送 1 积分
result.BonusPoints = (int)(result.FinalPrice / 10);
return result;
}
// 计算单个商品小计
static double Subtotal(Product p)
{
return p.Price * p.Quantity;
}
static void Main()
{
Product[] cart =
{
new Product() { Name = "键盘", Price = 299, Quantity = 1 },
new Product() { Name = "鼠标", Price = 89, Quantity = 2 },
new Product() { Name = "鼠标垫", Price = 25, Quantity = 3 }
};
// 显示每个商品小计
Console.WriteLine("===== 购物清单 =====");
foreach (Product p in cart)
{
Console.WriteLine($"{p.Name,-6} x{p.Quantity} 小计:{Subtotal(p)}");
}
// 计算整个订单
Console.WriteLine("\n===== 订单结算 =====");
OrderResult result = CalculateOrder(cart);
result.Show();
}
}
输出:
===== 购物清单 =====
键盘 x1 小计:299
鼠标 x2 小计:178
鼠标垫 x3 小计:75
===== 订单结算 =====
总价: ¥552.00
折扣: ¥55.20
实付: ¥496.80
积分: 49
四、结构体方法中的注意事项
4.1 结构体方法内的修改不影响外面(除非 ref)
public struct Counter
{
public int Value;
public void Increment()
{
Value++; // 改的是调用者这份数据
}
}
// 在另一个方法中使用
static void TryIncrement(Counter c)
{
c.Increment(); // 改的是拷贝!
Console.WriteLine($"方法内: {c.Value}");
}
Counter c = new Counter() { Value = 5 };
Console.WriteLine($"调用前: {c.Value}"); // 5
TryIncrement(c);
Console.WriteLine($"调用后: {c.Value}"); // 5 ← 没变!
4.2 在数组/集合中调用方法
Counter[] counters = new Counter[3];
// 数组元素可以直接调用方法 ✅
counters[0].Increment();
counters[0].Increment();
Console.WriteLine(counters[0].Value); // 输出: 2
// List 不行!❌
List<Counter> list = new List<Counter>();
list.Add(new Counter());
// list[0].Increment(); ← 编译错误!索引器返回拷贝
// 正确做法:
Counter temp = list[0];
temp.Increment();
list[0] = temp; // 放回去
第二部分:结构体与类的全方位对比
五、核心区别一览表
| 对比维度 | 结构体(struct) | 类(class) |
|---|---|---|
| 类型 | 值类型 | 引用类型 |
| 存储位置 | 通常在线程栈上 | 在托管堆上 |
| 赋值 | 复制整份数据 | 复制引用(地址) |
| 默认值 | 所有字段归零(数字=0,字符串=null...) | null |
| 继承 | ❌ 不能继承类,✅ 可实现接口 | ✅ 可以继承 |
| 无参构造函数 | 传统不支持,C# 10.0+ 放开 | ✅ 始终支持 |
| 析构函数 | ❌ 不支持 | ✅ 支持 |
| 可否为 null | 不可为 null(但 Nullable |
✅ 可以为 null |
| == 运算符 | 默认不实现(除非手动重载) | 默认比较引用 |
| 性能 | 小对象快,大对象慢(频繁拷贝) | 大对象快,小对象有 GC 开销 |
六、代码对比——每一个区别都看得见
6.1 赋值行为的对比(最核心的区别)
// ===== 结构体:赋值 = 克隆 =====
public struct PersonStruct
{
public string Name;
public int Age;
}
PersonStruct s1 = new PersonStruct();
s1.Name = "张三";
s1.Age = 20;
PersonStruct s2 = s1; // 把 s1 整个数据复制一份给 s2
s2.Name = "李四"; // 改 s2 不会影响 s1
Console.WriteLine(s1.Name); // 输出: 张三 ← 独立!
Console.WriteLine(s2.Name); // 输出: 李四
// ===== 类:赋值 = 贴标签 =====
public class PersonClass
{
public string Name;
public int Age;
}
PersonClass c1 = new PersonClass();
c1.Name = "张三";
c1.Age = 20;
PersonClass c2 = c1; // 把地址复制给 c2,c1 和 c2 指向同一个对象
c2.Name = "李四"; // 改 c2 就是改同一个对象
Console.WriteLine(c1.Name); // 输出: 李四 ← 也被改了!
Console.WriteLine(c2.Name); // 输出: 李四
通俗解释:
结构体:s1 = [张三,20] → s2 = [张三,20] 两个独立的盒子
类: c1 → [张三,20] ← c2 同一个盒子,两个名字
6.2 方法传参的对比
// 方法:修改结构体
static void ChangeStruct(PersonStruct p)
{
p.Name = "被改了";
p.Age = 999;
}
// 方法:修改类
static void ChangeClass(PersonClass p)
{
p.Name = "被改了";
p.Age = 999;
}
// 测试结构体
PersonStruct sp;
sp.Name = "原始";
sp.Age = 1;
ChangeStruct(sp);
Console.WriteLine($"结构体后: {sp.Name}, {sp.Age}"); // 原始, 1 ← 没变!
// 测试类
PersonClass cp = new PersonClass();
cp.Name = "原始";
cp.Age = 1;
ChangeClass(cp);
Console.WriteLine($"类后: {cp.Name}, {cp.Age}"); // 被改了, 999 ← 变了!
6.3 方法返回的对比
// 返回结构体
static PersonStruct CreateStruct()
{
PersonStruct p = new PersonStruct();
p.Name = "新对象";
p.Age = 1;
return p; // 返回的是值拷贝,调用方拿到独立数据
}
// 返回类
static PersonClass CreateClass()
{
PersonClass p = new PersonClass();
p.Name = "新对象";
p.Age = 1;
return p; // 返回的是引用,调用方拿到地址
}
PersonStruct s = CreateStruct();
PersonClass c = CreateClass();
s.Name = "修改结构体";
c.Name = "修改类";
// CreateStruct 内部的对象不受影响 ✅
// CreateClass 没人引用了,会被 GC 回收 ♻️
6.4 等值比较的对比
// 默认情况下,结构体也不能直接用 ==
// 但可以用 Equals(值比较)
PersonStruct a = new PersonStruct() { Name = "张三", Age = 20 };
PersonStruct b = new PersonStruct() { Name = "张三", Age = 20 };
Console.WriteLine(a.Equals(b)); // True ← 内容相同就相等
// 类:默认比较的是引用(地址)
PersonClass c1 = new PersonClass() { Name = "张三", Age = 20 };
PersonClass c2 = new PersonClass() { Name = "张三", Age = 20 };
Console.WriteLine(c1 == c2); // False ← 不是同一个对象
Console.WriteLine(c1.Equals(c2)); // False ← 默认也比引用
七、什么时候用结构体?什么时候用类?(决策指南)
用结构体 ✅ —— 满足下面多数条件时
// 典型的结构体使用场景
public struct Point // 坐标——就两个数,逻辑上是"一个值"
{
public int X;
public int Y;
}
public struct Color // 颜色——就四个 byte,很小
{
public byte R;
public byte G;
public byte B;
public byte A;
}
public struct Complex // 复数——数学上的"一个值"
{
public double Real;
public double Imaginary;
}
判断标准:
- 数据量小(< 16 字节)
- 它逻辑上表示一个"值"而不是一个"实体"
- 不需要继承
- 创建和销毁频繁(不需要 GC)
- 它是不可变的或者生命周期短
用类 ✅ —— 下面情况用类
// 典型的类的使用场景
public class Customer // 客户——有身份、有行为、有复杂关系
{
public int Id;
public string Name;
public string Address;
public List<Order> OrderHistory; // 有引用关系
}
public class Order // 订单——有生命周期,需要共享修改
{
public int OrderId;
public Customer Customer;
public List<OrderItem> Items;
}
public class FileStream // 文件流——有复杂的资源管理
{
// 有析构函数,需要释放资源
}
判断标准:
- 数据量大
- 代表一个"实体",有标识和生命周期
- 需要继承体系
- 需要共享修改(多处引用同一个对象)
- 需要析构函数释放资源
快速决策流程图
你的数据类型:
│
├─ 数据很小(< 16 字节)? ─── 否 ──→ 用 class
│ │
│ └─ 是
│ │
│ └─ 逻辑上是"一个值"(如坐标、颜色)? ─── 否 ──→ 用 class
│ │
│ └─ 是
│ │
│ └─ 不需要继承? ─── 否 ──→ 用 class
│ │
│ └─ 是
│ │
│ └─ 用 struct ✅
八、性能对比——理解内存行为
8.1 内存分配位置
栈(Stack) 堆(Heap)
┌─────────────┐ ┌─────────────────┐
│ 方法局部变量 │ │ new 出来的对象 │
│ struct 通常在这│ │ class 对象在这 │
│ 用完自动释放 │ │ GC 负责回收 │
└─────────────┘ └─────────────────┘
// 结构体——在栈上(通常),用完就扔
void DoWork()
{
Point p = new Point(); // 在栈上分配
p.X = 10;
p.Y = 20;
// 方法结束,p 自动消失,零 GC 压力!
}
// 类——在堆上,GC 要管
void DoWork2()
{
PersonClass c = new PersonClass(); // 在堆上分配
c.Name = "张三";
// 方法结束,c 变成垃圾 → 等 GC 回收
}
8.2 大结构体是性能杀手!
// ❌ 错误示范:结构体太大
public struct BigData
{
public double A;
public double B;
public double C;
public double D;
public double E;
public double F;
// ... 50 个字段,几百字节
}
// 每次传参都复制几百字节,越传越慢!
static void Process(BigData data) // 每次复制几百字节!
{
// ...
}
// ✅ 正解:这种就该用 class
public class BigDataClass
{
// 传参只传 8 字节的引用地址
}
九、综合实战对比示例
下面用同一个场景,分别用结构体和类实现,体会区别:
using System;
// ===== 结构体版本 =====
public struct PointStruct
{
public int X;
public int Y;
public void Move(int dx, int dy)
{
X += dx;
Y += dy;
}
}
// ===== 类版本 =====
public class PointClass
{
public int X;
public int Y;
public void Move(int dx, int dy)
{
X += dx;
Y += dy;
}
}
class Program
{
static void Main()
{
Console.WriteLine("====== 结构体测试 ======");
PointStruct sp1 = new PointStruct() { X = 0, Y = 0 };
PointStruct sp2 = sp1; // 拷贝
sp1.Move(5, 3); // 移动 sp1
sp2.Move(10, 20); // 移动 sp2
Console.WriteLine($"sp1: ({sp1.X}, {sp1.Y})"); // (5, 3)
Console.WriteLine($"sp2: ({sp2.X}, {sp2.Y})"); // (10, 20)
Console.WriteLine("两个互不影响 ✅");
Console.WriteLine("\n====== 类测试 ======");
PointClass cp1 = new PointClass() { X = 0, Y = 0 };
PointClass cp2 = cp1; // 引用同一个对象!
cp1.Move(5, 3); // 移动
cp2.Move(10, 20); // 移动
Console.WriteLine($"cp1: ({cp1.X}, {cp1.Y})"); // (15, 23) ← 累加了!
Console.WriteLine($"cp2: ({cp2.X}, {cp2.Y})"); // (15, 23) ← 同一个对象
Console.WriteLine("cp1 和 cp2 是同一个东西!");
}
}
输出:
====== 结构体测试 ======
sp1: (5, 3)
sp2: (10, 20)
两个互不影响 ✅
====== 类测试 ======
cp1: (15, 23)
cp2: (15, 23)
cp1 和 cp2 是同一个东西!
十、总结
结构体在方法中的使用总结
| 场景 | 写法 | 说明 |
|---|---|---|
| 默认传参 | void F(Point p) |
传拷贝,方法内修改不影响外部 |
| 要修改外部值 | void F(ref Point p) |
传引用,可以读也可以改 |
| 只输出值 | void F(out Point p) |
方法负责赋值返回 |
| 只读大结构体 | void F(in Point p) |
避免拷贝但不让改 |
| 返回结构体 | return new Point() |
返回值的拷贝 |
结构体 vs 类速查表
| 关键词 | 结构体 | 类 |
|---|---|---|
| 类型 | 值类型 | 引用类型 |
| 赋值 | 独立拷贝 | 共享引用 |
| new | 不需要(只初始化) | 必须在堆上分配 |
| 继承 | 不能继承 | 能继承 |
| 传参 | 默认拷贝 | 默认传引用 |
| 适用 | 小、简单、"是一个值" | 大、复杂、"是一个实体" |
一句话总结:结构体像"复印机",传参、赋值都复印一份,各改各的互不干扰;类像"共享文档链接",传参、赋值都是发链接,多人打开同一份文档,谁改了大家都看得见。想让结构体也能"共享编辑"?用
ref!
附录:关键字对照记忆卡
┌──────────────────────────────┐
│ 结构体传参四兄弟 │
├──────┬───────┬───────┬───────┤
│ 默认 │ ref │ out │ in │
├──────┼───────┼───────┼───────┤
│ 复印 │ 原件 │ 空表 │ 只看 │
│ 件给 │ 给你 │ 给你 │ 不给 │
│ 你看 │ 随便改 │ 你填 │ 你改 │
└──────┴───────┴───────┴───────┘