C#是一门强类型语言,任何一个变量在使用前都必须明确它的数据类型。类型系统决定了变量占用多少内存、能存什么值、支持哪些运算。初学者往往只记得int、string这些常见写法,却说不清C#到底有几大类类型、它们在内存中如何分配、互相之间怎样转换。这篇文章就把C#的类型体系完整梳理一遍,帮你建立起清晰的知识框架。

一、C#数据类型的整体分类:值类型与引用类型
C#的数据类型从大的方向上分为两大类:值类型和引用类型。这个划分依据的不是类型的功能,而是变量在内存中的存储方式,理解了这一点,后面很多行为差异就都能解释通了。
值类型直接把数据本身存储在栈上(准确地说,是存储在变量声明的位置)。当你把一个值类型变量赋值给另一个变量时,系统会在内存中完整复制一份数据,两个变量从此互不干扰。而引用类型变量存储的是对象在堆上的地址,赋值时复制的只是这个地址,两个变量指向同一个对象,修改其中一个,另一个也能看到变化。
值类型包括:简单类型(整型、浮点型、布尔型、字符型)、结构体struct、枚举enum,以及可空值类型如int?。引用类型包括:类class、接口interface、数组、字符串string、委托delegate、对象object和动态dynamic。可以简单记为:继承了System.ValueType的都是值类型,其余的基本都是引用类型。
二、常用的值类型详解
整型家族是使用频率最高的值类型。C#提供了多种整数类型,区别在于占用字节数和是否有符号。最常用的是int(32位有符号整数,范围约-21亿到21亿),其次是long(64位)、short(16位)、sbyte(8位有符号)以及对应的无符号版本uint、ulong、ushort、byte。选择的原则是够用就好,盲目用long会浪费内存。
浮点类型有三种:float(单精度,约7位有效数字)、double(双精度,约15到16位有效数字)和decimal(高精度十进制类型,28到29位有效数字)。这里有个容易踩坑的地方:float和double采用二进制浮点表示,0.1这样的十进制小数无法精确存储,做金额计算会出现误差。涉及钱的场景务必用decimal,字面量要加m后缀。
float price = 3.14f; // 单精度,必须加f后缀 double pi = 3.1415926; // 默认浮点类型 decimal money = 199.99m; // 金额计算必须用decimal,加m后缀 char letter = 'A'; // 字符类型,单引号,16位Unicode bool isVip = true; // 布尔类型,只有true和false byte age = 25; // 0到255的无符号小整数
除了这些简单类型,结构体和枚举也是值类型。结构体适合定义轻量的、不可分割的数据,比如DateTime、Guid、Point都是.NET内置的结构体。枚举则用来定义一组命名常量,让代码更具可读性,比如把星期几定义成枚举,比用裸数字0到6清晰得多。
三、常用的引用类型详解
引用类型中用得最多的是string和class。string虽然写起来像值类型,但它是货真价实的引用类型,只不过具有不可变性——任何对字符串的修改操作都会产生新的字符串对象。频繁拼接字符串时应使用StringBuilder,避免内存浪费。
数组、接口、委托也都属于引用类型。数组一旦创建,长度固定不可变;委托则是对方法的封装,是事件机制的基础。还有一个特殊的object类型,它是所有类型的最终基类,任何值类型赋值给object时会发生装箱操作。
string name = "张三";
string greeting = $"你好, {name}"; // 字符串插值
int[] numbers = new int[] { 1, 2, 3 }; // 数组是引用类型
Person p1 = new Person { Age = 20 };
Person p2 = p1; // 复制的是引用
p2.Age = 30; // p1.Age也变成30,因为指向同一个对象
struct PointStruct { public int X; }
PointStruct a = new PointStruct { X = 10 };
PointStruct b = a; // 值类型,完整复制
b.X = 99; // a.X仍然是10引用类型变量默认可以为null,而值类型不能直接等于null。如果希望值类型也能表示空值,比如数据库中一个 nullable 的整数字段,可以使用可空类型int?,编译器会把它处理成Nullable<int>结构。
四、类型转换与装箱拆箱
类型之间的转换分隐式和显式两种。隐式转换由编译器自动完成,方向是从小范围到大范围,安全不丢数据,比如int转long、int转double。显式转换需要强制转换语法,从大范围到小范围,可能丢失精度或溢出,需要开发者自己承担风险。
装箱和拆箱是值类型与引用类型之间的桥梁。装箱指把值类型包装成堆上的object对象,拆箱则是反向提取。这个过程涉及内存分配和数据复制,性能开销不小,在循环中频繁装箱会明显拖慢程序。典型的高频场景是把值类型放进非泛型集合(如ArrayList)里,用泛型集合List<int>替代就能彻底避免装箱。
int num = 100; object boxed = num; // 装箱:值类型转到堆上 int back = (int)boxed; // 拆箱:必须显式转换,类型必须完全匹配 double d = 3.9; int truncated = (int)d; // 显式转换,结果为3,直接截断小数 long big = 1000000L; int overflow = checked((int)big); // checked可开启溢出检查
另外补充一点:var关键字并不是一种新类型,它只是让编译器根据右侧表达式推断出实际类型,推断完成后类型就固定了,仍然属于强类型范畴。合理使用var可以简化冗长的泛型声明,但过度使用会降低代码可读性。
总结一下,C#的数据类型可以先用值类型和引用类型这条主线串起来,再往下细分简单类型、结构体、枚举和类、接口、数组、字符串、委托。判断标准始终是内存分配方式:值类型存数据本身,引用类型存地址。把存储方式、转换规则和装箱机制这三块搞明白,C#的类型系统就算真正掌握了。