一、内存中的栈和堆
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; // 编译报错
原因分析:
- shape.Center访问属性的get方法。
- 因为Point是值类型,get返回的是Point的副本。
- X = 10修改的是这个临时的副本的X。
- 原来的shape对象里的Point根本没变。
- 编译器发现修改了一个马上会被丢弃的临时变量,通常会报错提醒。
最佳做法:所有 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, enum | class, 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;
}
}