值类型和引用类型是 C# 类型系统的两大支柱。值类型变量"直接装数据",引用类型变量"装数据地址"——这看似简单的差别,却深刻影响着赋值、传参、方法返回的行为。这篇来彻底拆解。

值类型的变量直接包含类型实例,赋值时复制整个实例。
public struct MutablePoint
{
public int X;
public int Y;
public MutablePoint(int x, int y) => (X, Y) = (x, y);
public override string ToString() => $"({X}, {Y})";
}
var p1 = new MutablePoint(1, 2);
var p2 = p1; // 复制整个结构体
p2.Y = 200; // 只影响 p2
Console.WriteLine(p1); // (1, 2) ✅ p1 不受影响
Console.WriteLine(p2); // (1, 200)
方法传参也是复制:
static void MutateAndDisplay(MutablePoint p)
{
p.X = 100; // 改的是副本
Console.WriteLine(p); // (100, 200)
}
MutateAndDisplay(p2);
Console.WriteLine(p2); // (1, 200) ✅ 原值不受影响
划重点: struct 默认按值传递——方法内部对参数的修改不会影响调用方的原始变量。这与 class 的行为正好相反。
当结构体内部嵌套了引用类型(如 List<string>),复制行为就变得有意思了——值类型实例被复制,但内部的引用类型只复制了引用(地址) 。
public struct TaggedInteger
{
public int Number;
private List<string> tags;
public TaggedInteger(int n)
{
Number = n;
tags = new List<string>();
}
public void AddTag(string tag) => tags.Add(tag);
public override string ToString() => $"{Number} [{string.Join(", ", tags)}]";
}
var n1 = new TaggedInteger(0);
n1.AddTag("A");
Console.WriteLine(n1); // 0 [A]
var n2 = n1; // 复制结构体
n2.Number = 7; // ✅ 只改 n2 的 Number(值类型成员)
n2.AddTag("B"); // ⚠️ tags 是 List<string>,n1 和 n2 共享同一个 List
Console.WriteLine(n1); // 0 [A, B] ← n1 也被影响了!
Console.WriteLine(n2); // 7 [A, B]
| 操作 | n1.Number | n1.tags | n2.Number | n2.tags |
|---|---|---|---|---|
| 初始 | 0 | List["A"] | — | — |
| n2 = n1 | 0 | List["A"] | 0 | 同一个List["A"] |
| n2.Number = 7 | 0 | List["A"] | 7 | 同一个List["A"] |
| n2.AddTag("B") | 0 | List["A", "B"] | 7 | 同一个List["A", "B"] |
【提示】 官方明确建议:尽量使用不可变值类型。如果结构体包含引用类型成员,一定要清楚这种"共享引用"的语义——这往往是 bug 的来源。
C# 中的值类型有三种:
public struct Point { public int X; public int Y; }
public enum Color { Red, Green, Blue }
定义一组封闭的事例类型,值可以表示。这是 .NET 6+ 引入的特性。
int? nullableInt = null; // Nullable<int> bool? nullableBool = null; // Nullable<bool>
T? 表示 T 的所有值 + null。不能为 null 的值类型变量无法赋 null。
// struct 约束:类型参数必须是值类型(struct 或 enum)
public class MyClass<T> where T : struct { }
// System.Enum 约束:类型参数必须是枚举类型
public class MyClass<T> where T : System.Enum { }
C# 提供以下内置值类型,也称为简单类型:
| 类别 | 类型 |
|---|---|
| 整型 | byte,sbyte,short,ushort,int,uint,long,ulong,nint,nuint |
| 浮点型 | float,double,decimal |
| 布尔 | bool |
| 字符 | char |
所有简单类型都是结构类型,但与其他结构类型不同,它们允许一些额外操作:
| 操作 | 简单类型 | 自定义结构类型 |
|---|---|---|
| 文字常量 | ✅'A'、2001、12.34m | ❌ |
| const声明 | ✅const int Max = 100; | ❌ |
| 编译期求值 | ✅ 常量表达式在编译时求值 | ❌ |
// 简单类型特有的操作
const int MaxItems = 100; // ✅ struct 是简单类型
const decimal TaxRate = 0.05m; // ✅
// 以下 ❌ 编译错误
public struct Point { public int X; public int Y; }
// const Point Origin = new Point(); // 不能声明结构类型的常量
值元组(ValueTuple<T1, T2>)是值类型,但不是简单类型。
var point = (X: 10, Y: 20); // ValueTuple<int, int>
| 维度 | 值类型 | 引用类型 |
|---|---|---|
| 变量内容 | 数据本身 | 数据地址(引用) |
| 赋值行为 | 复制整个实例 | 复制引用(地址) |
| 默认传参 | 按值传递(副本) | 按值传递引用(同上仍是副本) |
| null赋值 | ❌ 不可(除非T?) | ✅ |
| 存储位置 | 栈/内联 | 堆 |
| 继承 | ❌struct不能继承 | ✅class可以 |
| 性能 | 小数据快(无 GC 压力) | 大数据好(引用传递) |
最后: 值类型的行为可以用一句话概括——值类型变量就是数据本身。赋值即复制、传参即复制、包含引用类型成员时要小心"共享引用"。这些规则虽然简单,但组合起来却影响着日常编码中的每一个细节。记住官方建议:优先用不可变值类型,代码会更可靠。