C# 值类型与引用类型 详解

C# 值类型与引用类型 完整详解

一、核心本质区别(内存存储)

1. 内存分配位置

  • 值类型(Value Type) :变量数据直接存储在栈(Stack) 上,变量本身就是数据。
  • 引用类型(Reference Type) :实际数据存放在堆(Heap) ,栈上只存一个内存地址(引用),通过地址指向堆中的对象。

2. 赋值行为差异

  1. 值类型赋值:完整拷贝副本
    赋值时把全部数据复制一份,两个变量完全独立,修改其中一个不会影响另一个。
  2. 引用类型赋值:拷贝地址(浅拷贝)
    只复制堆地址,两个变量指向同一个堆对象,任意一个修改对象内容,两边同时变化。

3. 生命周期与回收

  • 值类型:栈自动回收,超出作用域立刻销毁,无GC开销。
  • 引用类型:靠CLR垃圾回收器(GC)管理堆内存,无任何引用指向对象后才会被GC回收。

4. 默认值

  • 值类型:必有默认值 ,不能为null(可空值类型除外)
  • 引用类型:默认值是null,代表栈上没有指向任何堆对象

二、值类型完整分类

所有值类型隐式继承 System.ValueType,而ValueType本身又继承object

1. 简单内置值类型

分类 类型 说明
整数 sbyte、byte、short、ushort、int、uint、long、ulong 固定长度数字
浮点 float、double 小数
高精度小数 decimal 财务计算专用
布尔 bool true/false
字符 char 单个Unicode字符

2. 枚举 enum

底层基于整型,属于值类型

csharp 复制代码
enum Color { Red, Green } // 值类型
Color c1 = Color.Red;
Color c2 = c1; // 拷贝独立副本
c1 = Color.Green; // c2不受影响

3. 结构体 struct

自定义值类型,可包含字段、方法、属性、构造函数

csharp 复制代码
struct Point // 值类型
{
    public int X;
    public int Y;
}

Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1; // 完整复制X、Y
p1.X = 100; 
// p2.X 仍然是 1,互不干扰

注意:C# 10+ 支持无参构造函数,结构体默认无参构造永远存在,自动赋0/默认值。

4. 可空值类型 Nullable<T> / T?

普通值类型不能为null,包装后允许空:

csharp 复制代码
int? num = null; // Nullable<int>
if(num.HasValue) {}

值类型内存图解

csharp 复制代码
int a = 10;
int b = a;
a = 20;

栈内存:

复制代码
栈:a = 10 → 修改后20
栈:b = 10 (独立副本,不受影响)

三、引用类型完整分类

所有引用类型直接/间接继承 System.Object,数据存堆,栈存引用地址。

1. 类 class(最常用)

自定义引用类型,实例分配在堆

csharp 复制代码
class Person // 引用类型
{
    public string Name;
}

Person p1 = new Person { Name = "张三" };
Person p2 = p1; // 仅复制堆地址,指向同一个对象
p1.Name = "李四";
Console.WriteLine(p2.Name); // 输出李四,同步修改

内存图解:

复制代码
栈:p1 → 0x001(堆地址)
栈:p2 → 0x001
堆0x001:{ Name="张三" } → 修改为"李四"

2. 字符串 string

特殊引用类型,不可变(immutable)

  • 属于class,存在堆;
  • 一旦创建无法修改,拼接/替换会生成全新字符串;
  • 字符串池优化:相同字面量复用地堆内存。
csharp 复制代码
string s1 = "abc";
string s2 = s1;
s1 = "xyz"; // 新建堆对象,s2仍指向"abc"

3. 数组 Array

不管元素是值类型还是引用类型,数组本身永远是引用类型

csharp 复制代码
int[] arr1 = new int[2] {1,2};
int[] arr2 = arr1;
arr1[0] = 99;
Console.WriteLine(arr2[0]); // 99,共享数组

4. 接口 interface

本身不能实例化,但接口变量是引用类型,存储实现类对象的地址。

5. 委托 delegate、事件 event

本质封装方法指针,属于引用类型。

6. 动态类型 dynamic

底层基于object,引用类型。


四、装箱与拆箱(值类型 ↔ object)

1. 装箱(Boxing):值类型 → 引用类型

把栈上的值类型数据,复制到堆中包装为object,生成引用:

csharp 复制代码
int num = 10;    // 栈
object obj = num;// 装箱:堆创建object副本,obj存堆地址

开销:分配堆内存、拷贝数据,频繁装箱影响性能。

2. 拆箱(Unboxing):object → 值类型

从堆的object中取出原始值类型数据,复制回栈,必须强制转换:

csharp 复制代码
int num2 = (int)obj; // 拆箱

错误示范:类型不匹配会抛InvalidCastException

避免装箱优化

使用泛型List<T>而非ArrayList,泛型容器不会装箱拆箱。


五、关键易混淆知识点

1. struct vs class 核心选用场景

用 struct(值类型)满足全部:
  1. 小型数据(通常实例大小<16字节)
  2. 数据轻量,很少做赋值拷贝
  3. 无需继承、多态
  4. 语义是单一数据点(坐标、颜色、尺寸)
用 class(引用类型)满足任意:
  1. 数据量大
  2. 需要频繁传递、共享对象
  3. 需要继承、多态、接口多实现
  4. 语义是业务实体(用户、订单、商品)

2. ref / out / in 参数(改变值类型传递逻辑)

默认值类型传参是值拷贝,加ref后传递栈变量地址,方法内修改会影响外部变量:

csharp 复制代码
static void Modify(ref int x)
{
    x = 999;
}
int a = 10;
Modify(ref a);
// a = 999

3. 只读结构体 readonly struct

结构体所有字段只读,拷贝时编译器可做优化,减少复制开销。

4. 字符串特殊的相等判断

  • ==:string重载,比较字符内容
  • object.ReferenceEquals(s1,s2):比较是否指向同一个堆地址(判断字符串池复用)

5. 空值区别

  • 值类型int:不能=null;int?才允许null
  • 引用类型string:默认null,代表无堆对象

六、对比总结表

对比维度 值类型(Value Type) 引用类型(Reference Type)
存储位置 栈Stack 数据堆Heap,栈存地址
赋值逻辑 完整复制数据副本 仅复制内存地址,共享对象
默认值 数字0、false、\0,不可null null(无堆对象)
内存回收 栈自动释放,无GC GC标记清除回收堆内存
继承根 System.ValueType → object 直接继承object
代表类型 struct、enum、int/bool/char等 class、string、数组、委托、接口
修改传递 副本互不干扰 一处修改全部同步
装箱拆箱 支持,有性能损耗 无需装箱

七、完整演示代码

csharp 复制代码
using System;

// 值类型:结构体
struct Point
{
    public int X;
    public int Y;
}

// 引用类型:类
class Student
{
    public string Name;
}

class Program
{
    static void Main()
    {
        // ========== 值类型演示 ==========
        Point p1 = new Point { X = 10, Y = 20 };
        Point p2 = p1;
        p1.X = 999;
        Console.WriteLine($"值类型 p2.X = {p2.X}"); // 10,不受影响

        // ========== 引用类型演示 ==========
        Student s1 = new Student { Name = "小明" };
        Student s2 = s1;
        s1.Name = "小红";
        Console.WriteLine($"引用类型 s2.Name = {s2.Name}"); // 小红,同步变化

        // ========== 装箱拆箱 ==========
        int num = 100;
        object boxObj = num; // 装箱
        int unboxNum = (int)boxObj; // 拆箱
        Console.WriteLine($"拆箱结果:{unboxNum}");
    }
}

输出:

复制代码
值类型 p2.X = 10
引用类型 s2.Name = 小红
拆箱结果:100

八、常见踩坑点

  1. 结构体作为List元素修改无效
    List<Point>取出的是结构体副本,直接修改属性不会改变集合内数据,要用索引重新赋值。
  2. 频繁new class产生大量GC
    高频循环内创建类实例会造成堆碎片,可改用结构体或对象池优化。
  3. 字符串拼接性能差
    string不可变,大量拼接用StringBuilder
  4. 拆箱强制转换类型错误抛异常
    装箱是什么类型,拆箱必须对应类型,不能隐式转换。
  5. 数组永远是引用类型
    哪怕数组元素是int值类型,数组本身传递依然共享。
相关推荐
偏爱自由 !1 小时前
8. 泛型程序设计
java·开发语言·windows
冰暮流星1 小时前
python之flask框架讲解-准备
开发语言·python·flask
ch.ju1 小时前
Java Programming Chapter 4——Class loading
java·开发语言
Huangjin007_1 小时前
【C++11篇(二)】右值引用、移动语义保姆级讲解!
开发语言·c++
孟浩浩3 小时前
JAVA SpringAI+阿里云百炼应用开发
java·开发语言·阿里云
碧蓝的水壶4 小时前
数据转换过程
java·开发语言·windows
2501_947575809 小时前
计算机毕业设计之jsp开山车行二手车交易系统
java·开发语言·hadoop·python·信息可视化·django·课程设计
骑士雄师10 小时前
java面试题 4:鉴权
java·开发语言
时间的拾荒人11 小时前
C语言字符函数与字符串函数完全指南
c语言·开发语言