C#类型与封装(值类型与引用类型)

26 阅读5分钟

一、内存中的栈和堆

1.栈(stack)

  • 特点:极快、空间小、自动清理(方法执行完立即释放)。
  • 用途:存储正在执行的方法的参数、局部变量。
  • 比喻:就像在办公桌上,谁手拿放,处理完当前任务就清空。

2.堆(heap)

  • 特点:较慢、空间大、由垃圾回收期(GC)管理。
  • 用途:存储长期存在的对象(实例)。
  • 比喻:就像仓库,存放大量物品,需要特定索引地址去找,不用了由管理员定期清理。

二、值类型和引用类型

已知int是值类型,class是引用类型。如果它们混合使用会发生什么?

1. 简单记忆

  • 值类型:变量直接持有数据。
  • 引用类型:变量持有的是指向堆中数据的地址(指针)。

2.嵌套场景

Q:如果一个类(引用类型)里面有一个int(值类型),这个int存在哪?

A:堆上。

  • 局部变量中的值类型:存在栈上。
  • 类成员变量中的值类型:跟随对象存在堆上。
public class Player
{
    public int HP;
}
​
void Game() 
{
    int localScore = 100;   // localScore存在栈上
    Player p = new Player();    // p这个引用在栈上,指向 堆里的player对象
    p.PH = 50;              //  HP作为 player的一部分,存储在 堆上。
}

3.参数传递规则

当把变量传入方法时,默认都是按值传递,但表现不同:

  • 传递值类型:复制一份数据的副本。方法内修改不影响外面。
  • 传递引用类型:复制一份地址的副本。方法内修改对象属性,外面会变;但如果在方法内把参数指向new的新对象,外面不变。
public void Test()
{
    int a = 10;
    User u = new User {Name = "Jack"};
    
    Modify(a, u);
    
    Console.WriteLine(a);           // 10(没变)
    Console.WriteLine(u.Name);      // "Rose" (变了,因为改的是同一个仓库里的东西)
}
​
public void Modify(int num, User user)
{
    num = 999;                      // 修改的是副本
    user.Name = "Rose";             // 修改的是地址指向的对象
    
    user = new User();              // 这里把user变量指向了新仓库,断开了和外面的联系
    user.Name = "Tom";              // 这只会影响新对象,不影响外面的 u
}

三、属性的完全体

属性不仅仅是get; set; 它是 C# 对数据访问的集大成者。

1.属性进化史

  • C#1.0/2.0 手动模式:必须手写私有字段。

  • C#3.0 自动属性:public int Age {get; set;}

  • C#6.0表达式主体

    // 只有get的简写
    public string FullName => &"{FirstName} {LastName}";
    
  • C# 9.0 Init属性

    // 只能在 new Person {Id = 1}时赋值,之后只读
    public int Id {get; init;}
    

2.为什么结构体最好做成不可变

这是值类型和属性结合时的最大陷阱。

下面是一个可变的结构体属性:

// 定义一个可变结构体
struct Point {public int X; public int Y;}
​
class Shape
{
    public Point Center {get; set;}
}
​
var shape = new Shape();
shape.Center.X = 10;    // 编译报错

原因分析:

  1. shape.Center访问属性的get方法。
  2. 因为Point是值类型,get返回的是Point的副本。
  3. X = 10修改的是这个临时的副本的X。
  4. 原来的shape对象里的Point根本没变。
  5. 编译器发现修改了一个马上会被丢弃的临时变量,通常会报错提醒。

最佳做法:所有 struct 都应该设计为不可变。如果想修改,必须替换整个结构体。

shape.Center = new Point {X = 10; Y = 0};

三、高级概念和性能优化

1.装箱和拆箱

这是值类型在堆和栈之间转换的过程,是性能杀手。

  • 装箱:把值类型丢到堆里(变成object或接口)。
  • 拆箱:把堆里的对象还原成栈上的变量
int num = 5;
object obj = num;   // 装箱:在堆上分配内存,把 5 复制过去,慢!
int n2 = (int)obj;  // 拆箱:检查类型,把值复制出来。

尽量避免这种做法:

  • 使用泛型
  • List< int> 优于 ArrayList (ArrayList 存的是object,全是装箱)。

2.可空值类型

值类型(如int,bool)默认不能为null,但在数据库场景中,整个字段可能是空的。

C#提供了 Nullable< T>。,简写为 ?。

int? score = null; //合法
int normalScore = 0;
​
if (score.HasValue)
{
    normalScore = score.Value;
}
​
normalScore = score ?? 0; // 如果score 是null,就用 0

底层原理:int?实际上是一个 struct,内部维护了一个bool hasValue 和 T value。

总结

场景做法原因
定义实体模型 (User, Order)Class(引用类型)数据量大,需要继承,需要被多处引用修改。
定义小型数据块 (Point, Color)Struct (值类型)轻量,无需继承,逻辑上代表“一个值”。
公开数据成员Property (属性)封装性,数据绑定支持,未来扩展性。
内部数据存储Field (私有字段)仅供类内部使用。
防止外部修改{ get; private set; }保持数据一致性。
完全不可变对象{ get; init; }线程安全,代码逻辑更清晰。
特性值类型引用类型
代表事物具体的数据本身指向数据的地址 (指针)
内存位置通常在 栈 (Stack) 上数据在 堆 (Heap),变量在栈上存地址
赋值行为复制数据(克隆)复制引用(给同一个房间一把新钥匙)
典型代表int, float, bool, struct, enumclass, string, array, interface
默认值0, false, 或结构体的零值null
public class Order
{
    // 1.自动属性,最常用
    public string OrderId { get; set; } = Guid.NewGuid().ToString();

    // 2.带验证属性:保护数据
    private decimal _amount;
    public decimal Amount
    {
        get => _amount;
        set
        {
            if (value < 0) throw new ArgumentException("金额不能为负");
            _amount = value;
        }
    }

    // 3.计算属性:不存数据,实时计算
    public decimal Tax => Amount * 0.1m;

    // 4.Init属性:创建后不可变
    public DateTime CreatedAt { get; init; } = DateTime.Now;

    // 5.引用类型行为演示
    public void UpdateAmount(Order otherOrder)
    {
        this.Amount += otherOrder.Amount;
    }
}