在C#的类型体系中,值类型和引用类型是最基础的两类类型划分,两者的核心差异体现在内存存储、赋值逻辑、参数传递等多个层面,理解这些差异对编写正确的C#程序至关重要。

一、内存分配位置不同
值类型的实例通常存储在栈(Stack)上,或者作为引用类型对象的字段时存储在堆(Heap)上。而引用类型的实例本身存储在堆上,变量仅保存指向堆中实例的引用地址,该引用地址存储在栈上。
常见的int、double、struct都属于值类型,class、string、array都属于引用类型。
二、赋值行为的差异
值类型赋值时会复制整个实例的内容,赋值后两个变量互不影响。引用类型赋值时会复制引用地址,赋值后两个变量指向同一个堆中的实例,修改其中一个会影响另一个。
下面通过代码示例展示两者的赋值差异:
// 值类型赋值示例
int a = 10;
int b = a; // 复制a的值给b
b = 20;
Console.WriteLine(a); // 输出10,a的值不受b修改影响
Console.WriteLine(b); // 输出20
// 引用类型赋值示例
class Person
{
public string Name { get; set; }
}
Person p1 = new Person { Name = "张三" };
Person p2 = p1; // 复制p1的引用地址给p2
p2.Name = "李四";
Console.WriteLine(p1.Name); // 输出李四,p1指向的实例被p2修改了
Console.WriteLine(p2.Name); // 输出李四
三、参数传递的差异
默认情况下,值类型作为参数传递时会传递值的副本,方法内部修改参数不会影响原变量。引用类型作为参数传递时会传递引用的副本,方法内部修改实例的成员会影响原变量,但修改参数本身指向新实例不会影响原变量。
若要值类型按引用传递,需要使用ref或out关键字;引用类型本身已经是引用传递,不需要额外加关键字,除非要修改参数指向的实例。
// 值类型参数传递示例
void ModifyValue(int num)
{
num = 100;
}
int x = 5;
ModifyValue(x);
Console.WriteLine(x); // 输出5,原变量未改变
// 引用类型参数传递示例
void ModifyPerson(Person person)
{
person.Name = "王五"; // 修改实例成员,影响原变量
person = new Person { Name = "赵六" }; // 修改参数指向新实例,不影响原变量
}
Person p = new Person { Name = "小明" };
ModifyPerson(p);
Console.WriteLine(p.Name); // 输出王五
四、装箱拆箱的影响
值类型转换为object类型或它实现的接口类型时会发生装箱操作,装箱会将值类型实例复制到堆上,产生额外的内存开销。从object类型转换回值类型时会发生拆箱操作,拆箱需要类型匹配,否则会抛出异常,同样有性能损耗。
引用类型不存在装箱拆箱操作,因此如果频繁进行值类型和引用类型的转换,会影响程序的运行性能。
int val = 20; object obj = val; // 装箱操作,val的值被复制到堆上 int newVal = (int)obj; // 拆箱操作,将堆上的值复制回栈上 string str = "test"; object obj2 = str; // 没有装箱,引用类型的赋值 string newStr = (string)obj2; // 没有拆箱
五、默认值不同
值类型的默认值是其所有字段的默认值,数值类型为0,bool类型为false,struct的每个字段按规则初始化。引用类型的默认值为null,表示不指向任何堆中的实例。
| 类型分类 | 示例类型 | 默认值 |
|---|---|---|
| 值类型 | int | 0 |
| 值类型 | bool | false |
| 引用类型 | string | null |
| 引用类型 | 自定义class | null |
六、总结对比
- 内存分配:值类型多在栈,引用类型实例在堆
- 赋值行为:值类型复制内容,引用类型复制引用
- 参数传递:默认值类型传副本,引用类型传引用副本
- 性能影响:值类型无装箱拆箱,引用类型转换值类型会有额外开销
- 默认值:值类型有具体初始值,引用类型为null