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

编译器直接支持的数据类型成为基元类型。基元类型直接映射到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 引用类型)
- 值类型 直接存储在 栈(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;创建了独立副本,p1和p2是两个不同的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;复制的是引用,p1和p2指向同一对象。- 修改
p1.X会影响p2.X,因为它们都指向同一个对象。
3. 图示(值类型 vs 引用类型)
(1)值类型存储(栈上直接存储)
✅ p2 = p1; 创建独立副本,p2.X 不会受 p1.X 变化影响。
(2)引用类型存储(指向堆对象)
❌ 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
is 和 as 关键字(安全转换)
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 图示(溢出检查)
4. 装箱与拆箱(Boxing & Unboxing)
在对对象进行拆箱是,只能转换为最初未装箱的值类型
装箱(Boxing):值类型 → 引用类型(性能损耗)
拆箱(Unboxing):引用类型 → 值类型(需显式转换)
示例
int num = 42;
object obj = num; // 装箱:值类型 → 引用类型
object obj2 = 42;
int num2 = (int)obj2; // 拆箱:引用类型 → 值类型
为什么装箱和拆箱贵影响性能
- 装箱会导致 堆分配(Heap Allocation),增加 GC 压力
装箱过程:- 在堆(Heap)中分配内存,存储
num的值 (额外开销)。 - 复制
num的值 到堆对象 (额外复制)。 obj现在是object类型,指向堆上的数据。
- 在堆(Heap)中分配内存,存储
- 拆箱需要 两次开销:类型检查 + 复制
拆箱过程:- 类型检查:确保
obj真的是int(额外开销)。 - 从堆中复制数据 到
num(额外复制)。
- 类型检查:确保
Mermaid 图示(装箱 & 拆箱)
如何避免装箱和拆箱
✅ (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 进行如下步骤:
- 计算对象所需的内存大小
- 在堆上分配内存
- 初始化对象头(类型对象指针 & 同步块索引)
- 调用构造函数(Constructor)
- 返回对象引用
class Demo { }
Demo obj = new Demo();
Mermaid 图示(new 关键字)
[[5.6 Equal & HashCode]]
[[5.7 Dynamic]]
面试题 & 解析
1.
值类型和引用类型的主要区别是什么?
✅ 解析
- 值类型(
struct):存储在 栈 上,赋值时 复制数据,不受 GC 影响。 - 引用类型(
class):存储在 堆 上,赋值时 复制引用,由 GC 回收。
2.
装箱和拆箱的性能影响是什么?
✅ 解析
- 装箱(Boxing):值类型 → 堆分配(有性能开销)
- 拆箱(Unboxing):堆分配 → 值类型(需要显式转换,可能抛出异常)
- 优化建议:避免 频繁装箱 & 拆箱,可使用
List<int>代替ArrayList。
3.
checked和unchecked的作用是什么?
✅ 解析
checked:检测溢出,抛出异常(OverflowException)。unchecked:忽略溢出,数值循环(不抛异常)。- 默认行为:默认
unchecked,除非编译器或checked指定。
4.
new关键字在 CLR 运行时执行了哪些步骤?
✅ 解析
- 计算所需内存大小
- 在 堆 上分配内存
- 初始化对象头(类型指针 & 同步块索引)
- 调用构造函数(
.ctor) - 返回对象引用(存储在栈上)
总结
- 值类型 vs 引用类型:栈 vs 堆,复制值 vs 复制引用。
- 类型转换:隐式(安全) vs 显式(可能丢失数据)。
- 溢出检查:
checked抛异常,unchecked忽略溢出。 - 装箱与拆箱:避免频繁装箱,影响性能。
new关键字:对象创建流程,涉及 GC 及内存管理。
作者:世纪末的魔术师
出处:https://www.cnblogs.com/Firepad-magic/
Unity最受欢迎插件推荐:点击查看
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号