在 C# 中,类型系统主要分为值类型和引用类型,它们在内存中的存储方式和管理机制有显著区别。
值类型的变量直接包含数据本身,并且通常分配在托管栈中。当声明一个值类型变量时,无论是否立即赋值,编译器都会为其分配内存空间。这类变量在其作用域结束(如方法执行完毕)后会自动释放。
常见的值类型包括:数值类型(如 byte、short、int、long、float、double、decimal)、char、bool,以及用户自定义的 struct 结构体、枚举(enum)和可空类型(T?)。其中,可空类型是 System.Nullable<T> 的别名,用于表示可以为 null 的值类型。
char name='C'
当实例化它的方法结束时,name变量在栈上占用的内存就会自动释放,
// C#的所有值类型均隐式派生自System.ValueType。
数值类型进一步可分为整型和浮点型:
- 整型:
- sbyte(System.SByte)— 8 位有符号整数
- short(System.Int16)— 16 位有符号整数
- int(System.Int32)— 32 位有符号整数
- long(System.Int64)— 64 位有符号整数
- byte(System.Byte)— 8 位无符号整数
- ushort(System.UInt16)— 16 位无符号整数
- uint(System.UInt32)— 32 位无符号整数
- ulong(System.UInt64)— 64 位无符号整数
- char(System.Char)— 16 位 Unicode 字符
- 浮点型:
- float(System.Single)— 32 位单精度浮点数
- double(System.Double)— 64 位双精度浮点数
- decimal(System.Decimal)— 128 位高精度十进制数,常用于财务计算
结构类型通过 struct 关键字定义,直接继承自 System.ValueType。有些资料将结构划分为简单类型(如内置数值类型)和用户自定义结构体。
int x = 5; // 注意这里是在堆栈,因为int是值类型
CLR 的内存管理区域主要包括堆栈(简称“栈”)和托管堆。对于值类型,CLR 会在栈中为其分配内存;而对于引用类型,则会在栈中存放引用地址,实际对象则创建于托管堆中。
例如,在语句 string s = "abc"; 中,过程分为两步:
string s;— 在栈上分配一个引用变量 s,此时不指向任何堆中的对象;s = "abc";— 在托管堆中创建字符串对象 "abc",并将 s 指向该对象的内存地址。
引用类型变量并不直接存储数据,而是保存对堆中对象实例的引用(即内存地址)。这类类型包括:类(class)、接口(interface)、委托(delegate)、数组、object、string 以及 null 类型。
由于引用类型的数据位于托管堆中,其内存不由栈帧控制,因此不会在方法退出时立即释放,而是由 CLR 的垃圾回收机制(GC)在适当时机自动回收。
class MyClass
{
public int Value { get; set; }
}
void MyMethod()
{
MyClass obj = new MyClass(); // 在堆上创建 MyClass 对象
obj.Value = 10; // 设置对象的值
string str = "Hello"; // 在堆上创建 string 对象
// ... 其他代码 ...
}
// MyClass obj = new MyClass();:
// new MyClass() 在堆上分配内存,创建一个 MyClass 对象。
// obj 变量在栈上分配内存,用于存储堆上 MyClass 对象的内存地址
// string str = "Hello";
// "Hello" 在堆上分配内存,创建一个 string 对象。
// str 变量在栈上分配内存,用于存储堆上 string 对象的内存地址
类(Class)
类是 C# 中最基本的引用类型,使用 class 关键字定义,用于封装属性和方法,构建可实例化的对象模型。
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
接口(Interface)
接口定义了一组成员规范(如方法、属性、事件等),但不提供具体实现。一个类可以实现多个接口,从而实现多态性与契约式编程。
委托(Delegate)
委托派生于 System.Delegate,是一种类型安全的函数指针,能够引用方法并将其作为参数传递,广泛应用于事件处理和回调机制中。
delegate int MyDelegate(int x, int y);
基类 object
所有类型都直接或间接继承自 object 类型,它是整个类型系统的根。这意味着任何类型的值都可以赋给 object 变量。
object obj = 123;
obj = "Hello";
字符串(String)
string 类型用于表示 Unicode 字符序列,属于引用类型,但具有不可变性特征 —— 一旦字符串对象被创建,其内容就不能被修改。即使重新赋值,也是生成新的字符串对象,原对象仍保留在内存中等待 GC 回收。
常见误区:虽然 string 是引用类型,但由于其不可变特性,每次修改其实都是新建对象。这也是为何看似“修改”了字符串,实际上是改变了引用所指向的新实例。
string message = "Hello, world!";
数组(Array)
数组是一种引用类型,用于存储固定大小的同类型元素集合,支持一维、多维及锯齿数组。数组对象本身分配在托管堆中,栈中仅保存对其的引用。
int[] numbers = { 1, 2, 3, 4, 5 };
总结一下关键点:
- 值类型直接存储数据,默认分配在栈上(特殊情况如下文所述);
- 引用类型存储的是对象的引用,真实数据位于托管堆中;
- 栈区主要用于存放局部变量、参数、返回值等,由编译器自动管理生命周期;
- 堆区用于存放引用类型的实例,由 CLR 垃圾回收器统一管理;
- 尽管值类型通常位于栈中,但在以下情况也可能出现在堆上:
- 作为引用类型的字段存在时;
- 发生装箱操作时(boxing);
- 在闭包或迭代器中被捕获的局部变量。
成员变量中的值类型字段,若未显式初始化,会被赋予默认值(如 0、false 等),而引用类型字段默认为 null。
int类型的默认值是0
String类型的默认值是null
double类型的默认值是0.0d
Integer类型的默认值是null
Long类型的默认值是null
long类型的默认值是0L
float类型的默认值是0.0f
char类型的默认值是\u0000
byte类型的默认值是(byte)0
short类型的默认值是(short)0
eunm枚举默认值是0,不是null
bool默认值是false
通过以下示例有助于理解两者差异:
// 值类型,保存在栈中
int num = 100;
// 引用类型,保存在堆中
int[] nums = {1,2,3,4,5};
// 接下来,我们输出一下
Console.WriteLine(num);
Console.WriteLine(nums);
100
System.Int32[]
在这个例子中,变量 num 是值类型,输出时直接显示其值;而 nums 是引用类型,若未重写 ToString() 方法,则默认输出其类型名称,表现为“引用”的形式而非实际内容。
补充说明:
- 堆:在 C 语言中称为“堆”,在 C# 中特指“托管堆”,由 CLR 统一管理;
- 栈:也称“堆栈”,为避免与“堆”混淆,通常简称为“栈”;
尽管 string 是引用类型,但其行为在很多方面却类似于值类型,这主要归因于它的“不可变性”特性。
不可变性的核心概念
一旦一个字符串对象被创建,例如通过代码 string str = "Hello",该字符串的内容“Hello”就会被存放在内存的特定区域中,并且从此无法再被修改。
这意味着字符串本身是固定不变的——任何看似对字符串内容的“更改”,实际上都不会影响原始对象。
string
重新赋值的真实过程
当你执行如下代码:
str = "World";
系统并不会去修改原有的“Hello”字符串,而是会在内存中创建一个全新的字符串对象“World”,然后将变量
str
的引用指向这个新对象。
原先的“Hello”依然存在于内存之中,只是当前变量不再指向它。若此后没有任何其他引用指向该字符串,它将在适当的时机被垃圾回收机制清理。
为何要将 string 设计为不可变?
1. 支持字符串池(String Interning)
C# 中存在一个称为字符串池的机制,用于存储和共享相同的字符串字面量。如果字符串是可变的,那么一旦某个引用修改了其中的内容,所有共享该字符串的其他引用也会受到影响,从而引发严重的逻辑错误。
而由于字符串的不可变性,多个变量可以安全地引用同一个字符串实例,极大减少了内存占用,提升了效率。
2. 保证线程安全
不可变对象天然具备线程安全性。因为没有线程能够修改字符串的内容,所以即使多个线程同时读取同一个字符串,也不会出现数据竞争或不一致的问题,无需额外的同步措施。
3. 提升缓存与运行性能
由于字符串内容不会改变,运行时可以预先计算并缓存其哈希码等信息,避免重复计算。这种优化显著提高了在字典查找、集合操作等场景下的性能表现。
4. 编程模型更简洁直观
字符串的不可变性使其在使用上更接近值类型的语义。比如变量赋值时的行为更容易理解,降低了出错概率,使代码更具可预测性。
重新赋值 vs 内容修改:本质区别
- 重新赋值:生成一个新的字符串实例,并更新变量引用,原字符串保持不变。
- 内容修改:直接改动原有对象的数据内容——这是
string类型所不允许的操作。
string
因此,string 不支持真正的“修改”操作,所有变更都必须通过创建新对象完成。
总结
虽然
string
属于引用类型,但由于其不可变性,其使用体验更接近于值类型。
每次重新赋值实际上是创建新的字符串对象,而非修改原有实例。相比之下,真正的值类型如 int 在赋值时会直接覆盖内存中的旧值,而不产生新对象。
字符串的不可变设计带来了诸多优势,包括:
- 内存优化(借助字符串池实现共享)
- 线程安全(多线程环境下无需加锁)
- 性能提升(支持哈希缓存、减少复制开销)


雷达卡


京公网安备 11010802022788号







