五、基本类型、引用类型和值类型

CLR #引用类型 #值类型

第五章:基本类型、引用类型和值类型(CLR 类型系统)

莫奈花海山.png

编译器直接支持的数据类型成为基元类型基元类型直接映射到Framework类库中的存在的类型
C#中的int始终映射到System.Int32,long映射到System.Int64
Decimal在很多语言包括C#中基元类型,CLR中却不是。CLR中没有起IL指令,编译器会生成代码调用其成员,因此梳理速度慢于CLR中其他基元类型。

1. C# 类型系统概览

CLR 将所有类型分为 值类型(Value Types)引用类型(Reference Types)

类型类别 存储位置 赋值方式 是否 GC 管理 示例
值类型 栈(Stack) 复制值 ❌ 否 int, float, struct
引用类型 堆(Heap) 复制引用 ✅ 是 class, string, object

Mermaid 图示(值类型 vs 引用类型)

graph TD subgraph 栈["📌 栈 (Stack)"] v1["🟢 int a = 10"] v2["🟢 Point p1 (struct)"] end subgraph 堆["📦 堆 (Heap)"] r1["🟠 string s = 'hello'"] r2["🟠 Person p = new Person()"] end r2 -->|引用存储在栈| 堆
  • 值类型 直接存储在 栈(Stack),赋值时 复制数据
  • 引用类型 存储在 堆(Heap),赋值时 复制引用,多个变量可能指向同一个对象。

示例 1:值类型赋值(复制值)

struct Point {
    public int X;
}

Point p1 = new Point();
p1.X = 10;

Point p2 = p1; // 复制值
p1.X = 20;

Console.WriteLine(p2.X); // 仍然是 10

`

解释

  • p2 = p1; 创建了独立副本p1p2 是两个不同的 Point 结构体。
  • 修改 p1.X 不会影响 p2.X

示例 2:引用类型赋值(复制引用)

class Point {
    public int X;
}

Point p1 = new Point();
p1.X = 10;

Point p2 = p1; // 复制引用
p1.X = 20;

Console.WriteLine(p2.X); // 结果是 20

解释

  • p2 = p1; 复制的是引用p1p2 指向同一对象
  • 修改 p1.X 会影响 p2.X,因为它们都指向同一个对象。

3. 图示(值类型 vs 引用类型)

(1)值类型存储(栈上直接存储)

graph TD subgraph Stack["📌 栈 (Stack)"] v1["🟢 Point p1 (X=10)"] v2["🟢 Point p2 (X=10)"] end

p2 = p1; 创建独立副本p2.X 不会受 p1.X 变化影响


(2)引用类型存储(指向堆对象)

graph TD subgraph Stack["📌 栈 (Stack)"] p1["🟠 Point p1 (引用)"] p2["🟠 Point p2 (引用)"] end subgraph Heap["📦 堆 (Heap)"] obj["Point 对象 (X=10)"] end p1 --> obj p2 --> obj

p2 = p1; 复制的是引用,两个变量都指向同一个对象,p1.X 改变会影响 p2.X

ref struct存储在栈上的特殊结构体,如 Span<T>不能装箱不能存入堆不能作为 object,适用于高性能计算


4. 何时使用值类型 vs 引用类型

使用场景 适合值类型(struct) 适合引用类型(class)
数据存储 小型数据(坐标、颜色、时间) 大型数据(图片、数据库连接)
频繁复制 适合(独立副本) 不适合(复制引用)
性能要求 性能高(无 GC 开销) 灵活性高(支持继承、多态)

2. 类型转换(Casting)

隐式转换(安全,无需额外代码)

int i = 10;
long l = i;  // int -> long (安全转换)
float f = i; // int -> float (安全转换)

显式转换(需要手动转换,可能丢失数据)

double d = 9.8;
int x = (int)d; // 数据截断,x = 9

isas 关键字(安全转换)

object obj = "hello";
if (obj is string str) { // is 关键字
    Console.WriteLine(str.Length);
}

object obj2 = 123;
string str2 = obj2 as string; // as 关键字,失败返回 null

3. 溢出检查(Checked & Unchecked Operations)

C# 默认不检测溢出,但 checked 关键字可以启用溢出检查:
[[byte类型]]

checked {
    byte b = 200;
    b += 100;  // 触发 OverflowException
}

unchecked {
    byte b = 200;
    b += 100;  // 不抛出异常,b 溢出变小
}

Mermaid 图示(溢出检查)

graph TD A["🌡️ checked { byte b = 200; b += 100; }"] -->|❌ 抛出异常| B["OverflowException"] C["🌡️ unchecked { byte b = 200; b += 100; }"] -->|✅ 不抛异常| D["b 溢出成 44"]

4. 装箱与拆箱(Boxing & Unboxing)

在对对象进行拆箱是,只能转换为最初未装箱的值类型

装箱(Boxing):值类型 → 引用类型(性能损耗)
拆箱(Unboxing):引用类型 → 值类型(需显式转换)

示例

int num = 42;
object obj = num;  // 装箱:值类型 → 引用类型

object obj2 = 42;
int num2 = (int)obj2;  // 拆箱:引用类型 → 值类型

为什么装箱和拆箱贵影响性能

  1. 装箱会导致 堆分配(Heap Allocation),增加 GC 压力
    装箱过程
    1. 在堆(Heap)中分配内存,存储 num 的值 (额外开销)
    2. 复制 num 的值 到堆对象 (额外复制)
    3. obj 现在是 object 类型,指向堆上的数据。
  2. 拆箱需要 两次开销:类型检查 + 复制
    拆箱过程
    1. 类型检查:确保 obj 真的是 int (额外开销)
    2. 从堆中复制数据num (额外复制)

Mermaid 图示(装箱 & 拆箱)

graph LR ValueType["🟢 int 42 值类型"] -->|装箱 Boxing| HeapObject["📦 object 42 堆存储"] HeapObject -->|拆箱 Unboxing| ValueType

如何避免装箱和拆箱

(1)使用 List<T> 代替 ArrayList

// ❌ 使用 ArrayList(内部存 object,会产生装箱) 
ArrayList list = new ArrayList(); 
list.Add(42);  // 装箱 
int value = (int)list[0];  // 拆箱  
// ✅ 使用 List<T>(无装箱) 
List<int> list2 = new List<int>(); 
list2.Add(42);  // 无装箱 
int value2 = list2[0];  // 直接读取`

(2)使用 struct + ref 传递,避免装箱

struct MyStruct {
    public int Value;
}

void ProcessStruct(ref MyStruct s) {
    s.Value++;
}

MyStruct ms = new MyStruct();
ProcessStruct(ref ms);  // ✅ 传递结构体引用,避免装箱

(3)避免装箱示例

int num1 = 42;
Console.WriteLine(num1);  // 🚨 发生装箱 (Boxing)

int num2= 42;
Console.WriteLine(num2.ToString());  // ✅ 无装箱
//`num.ToString()` **返回 `string` 类型**,而 `Console.WriteLine(string)` 是特化版本,不会发生装箱

int num3 = 42;
Console.WriteLine($"Number: {num3}");  // ✅ 无装箱
//**字符串插值** `($"Number: {num}")` **底层会调用 `num.ToString()`**

方法 是否装箱 推荐级别 适用场景
Console.WriteLine(num); 🚨 会装箱 不推荐 低性能代码
Console.WriteLine(num.ToString()); 无装箱 👍 推荐 一般应用
Console.WriteLine($"Number: {num}"); 无装箱 强烈推荐 C# 6.0+
ConsoleHelper.WriteLine<T>(T value); 无装箱 ⭐⭐ 更推荐 C# 7.0+
Span<char> + TryFormat() 无装箱 🚀 最佳性能 C# 8.0+ 高性能

5. new 关键字的内部执行过程

new 关键字用于创建对象时,CLR 进行如下步骤:

  1. 计算对象所需的内存大小
  2. 在堆上分配内存
  3. 初始化对象头(类型对象指针 & 同步块索引)
  4. 调用构造函数(Constructor)
  5. 返回对象引用
class Demo { }
Demo obj = new Demo();

Mermaid 图示(new 关键字)

sequenceDiagram participant App as 应用程序代码 participant CLR as 公共语言运行时 (CLR) participant Heap as 托管堆 (Managed Heap) App->>CLR: new Demo() CLR->>CLR: 计算内存大小 CLR->>Heap: 分配堆内存 Heap-->>CLR: 返回对象地址 CLR->>CLR: 初始化对象头 CLR->>CLR: 调用构造函数 CLR-->>App: 返回对象引用

[[5.6 Equal & HashCode]]

[[5.7 Dynamic]]


面试题 & 解析

1. 值类型引用类型 的主要区别是什么?

解析

  • 值类型struct):存储在 上,赋值时 复制数据,不受 GC 影响。
  • 引用类型class):存储在 上,赋值时 复制引用,由 GC 回收。

2. 装箱拆箱 的性能影响是什么?

解析

  • 装箱(Boxing)值类型 → 堆分配(有性能开销)
  • 拆箱(Unboxing)堆分配 → 值类型(需要显式转换,可能抛出异常)
  • 优化建议:避免 频繁装箱 & 拆箱,可使用 List<int> 代替 ArrayList

3. checkedunchecked 的作用是什么?

解析

  • checked:检测溢出,抛出异常OverflowException)。
  • unchecked:忽略溢出,数值循环(不抛异常)。
  • 默认行为:默认 unchecked,除非编译器或 checked 指定。

4. new 关键字在 CLR 运行时执行了哪些步骤?

解析

  1. 计算所需内存大小
  2. 上分配内存
  3. 初始化对象头(类型指针 & 同步块索引)
  4. 调用构造函数(.ctor
  5. 返回对象引用(存储在栈上)

总结

  • 值类型 vs 引用类型:栈 vs 堆,复制值 vs 复制引用。
  • 类型转换:隐式(安全) vs 显式(可能丢失数据)。
  • 溢出检查checked 抛异常,unchecked 忽略溢出。
  • 装箱与拆箱:避免频繁装箱,影响性能。
  • new 关键字:对象创建流程,涉及 GC 及内存管理。
posted @ 2025-08-26 10:06  世纪末の魔术师  阅读(7)  评论(0)    收藏  举报