目 录CONTENT

文章目录

CSharp(三十七) 结构体在方法中的使用

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;
}

判断标准

  1. 数据量小(< 16 字节)
  2. 它逻辑上表示一个"值"而不是一个"实体"
  3. 不需要继承
  4. 创建和销毁频繁(不需要 GC)
  5. 它是不可变的或者生命周期短

用类 ✅ —— 下面情况用类

// 典型的类的使用场景
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     // 文件流——有复杂的资源管理
{
    // 有析构函数,需要释放资源
}

判断标准

  1. 数据量大
  2. 代表一个"实体",有标识和生命周期
  3. 需要继承体系
  4. 需要共享修改(多处引用同一个对象)
  5. 需要析构函数释放资源

快速决策流程图

你的数据类型:
│
├─ 数据很小(< 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   │
                    ├──────┼───────┼───────┼───────┤
                    │ 复印  │ 原件   │ 空表   │ 只看   │
                    │ 件给  │ 给你   │ 给你   │ 不给   │
                    │ 你看  │ 随便改 │ 你填   │ 你改   │
                    └──────┴───────┴───────┴───────┘
0
博主关闭了当前页面的评论