我在 C# 里用 struct 定义数据放进 List,想改它的字段却怎么改都不生效、传进方法改也白改,我对着值类型的拷贝语义排查了大半天的复盘

我图省事用 struct 定义数据(以为就是轻量的 class)放进 List,改 list

我在 C# 里用 struct 定义数据放进 List,想改它的字段却怎么改都不生效、传进方法改也白改,我对着值类型的拷贝语义排查了大半天的复盘

这是一个让我对 C# 的"值类型 vs 引用类型"彻底理解透彻的故事。我定义了一个表示坐标的数据结构,图省事用了 struct(我以为 struct 和 class 差不多,就是个"轻量的类")。我把一堆 struct 放进了 List。然后,需求来了:要修改列表里某个元素的字段。我自然地写下 list[0].X = 10;——结果编译器直接报错!我换了种写法绕过去,运行起来,更诡异的事发生了:明明"改"了那个元素的字段,可再去读它,值纹丝不动、还是原来的;同样地,我把一个 struct 传进一个方法里修改,方法返回后,外面那个原始的 struct,也一点没变。我"改"了它们,可这些"修改",像泥牛入海一样,全都没生效

我顺着"改了不生效"的线索深挖,才终于揭开真相,补上了我对 C# 一个最根本的认知漏洞:问题的核心,是 struct"值类型(value type)",而它和 class(引用类型 reference type)的行为有着本质的不同。我一直想当然地以为,"struct 就是个轻量的 class,用起来差不多";可真相是:值类型(struct)和引用类型(class),在"赋值、传参、从集合取出"时,行为截然相反引用类型(class):变量里存的是"指向对象的引用(地址)";赋值/传参时,复制的是引用,多个变量指向同一个对象,所以改一个,大家都变。而值类型(struct):变量里直接存数据本身;赋值/传参时,复制的是整个数据(一份完整的拷贝),各是各的、互不相干,所以改"拷贝",不影响"原值"这就解释了我所有的困惑:list[0].X = 10 报错,是因为 list[0] 这个索引器,返回的是 struct 的一份拷贝,改这个"临时拷贝"的字段毫无意义(改完就丢),所以编译器直接禁止了这种"注定无效"的写法。传进方法修改不生效,是因为传进去的是一份拷贝,方法改的是那份拷贝,原值自然不变我这才痛彻地明白:在 C# 里,structclass,绝不是"轻重之分",而是"值语义"和"引用语义"的根本之别;你以为在"修改一个对象",实际可能只是在"修改一个转瞬即逝的拷贝"选 struct 还是 class,不是看"大小",而是看"你想要值语义,还是引用语义":需要"可变、且多处共享同一份"的,用 class;而 struct,因为它的"拷贝"特性,最好设计成"不可变(immutable)"的小数据,以避免这种"改了拷贝、原值不变"的迷惑

故障现场:改 struct 的拷贝,原值纹丝不动

我把这些"改了不生效"的现场,摊开给你看:

// ✗ 灾难: 用 struct(值类型), 改的全是拷贝, 原值不变
struct Point { public int X; public int Y; }   // struct = 值类型

// 1. 改 List 里的元素 —— 编译报错
var list = new List { new Point { X = 1, Y = 2 } };
list[0].X = 10;   // ✗ 编译错误! "无法修改返回值, 因为它不是变量"
// 原因: list[0] 索引器返回的是 Point 的"拷贝", 改拷贝无意义, 编译器禁止。

// 2. 绕过去后, 改了也不生效
var p = list[0];   // ✓ 编译过, 但 p 是拷贝!
p.X = 10;          // 改的是拷贝 p
Console.WriteLine(list[0].X);   // ✗ 还是 1! 原 list 里的没变。

// 3. 传进方法修改 —— 白改
void Move(Point pt) { pt.X = 100; }   // pt 是传进来的拷贝
var origin = new Point { X = 0 };
Move(origin);
Console.WriteLine(origin.X);    // ✗ 还是 0! 方法改的是拷贝。

// 4. 对比 class(引用类型)—— 一切正常
class PointC { public int X; public int Y; }
var listC = new List { new PointC { X = 1 } };
listC[0].X = 10;                // ✓ 生效! 索引器返回引用, 改的是同一个对象。
Console.WriteLine(listC[0].X);  // ✓ 10

// 值类型 vs 引用类型的本质:
//   class(引用类型): 变量存"引用(地址)", 赋值/传参复制引用 → 共享同一对象 → 改了都变。
//   struct(值类型):  变量存"数据本身", 赋值/传参复制整个数据 → 各是各的 → 改拷贝不影响原值。

// 哪些是值类型? int/double/bool/char、enum、struct、以及 DateTime/Guid 等。
// 哪些是引用类型? class、interface、数组、string、委托、object。

// 根因: struct 是值类型, 赋值/传参/索引器取出都是拷贝;
//   改拷贝不影响原值, 所以"改 List 元素/传方法改"都不生效。

看着这一组"改了不生效"的代码,我才算彻底想明白了根源。问题的核心,是 struct值类型,而我对它的所有"修改",改的都是拷贝list[0].X = 10 编译报错,是因为 list[0] 索引器返回的是 struct 的拷贝,改拷贝无意义、编译器直接禁止;var p = list[0]; p.X = 10 改了不生效,是因为 p 是拷贝、原 list 里的没变;Move(origin) 传进方法改也白改,是因为 传进去的是拷贝、方法改的是拷贝。而换成 class(引用类型),listC[0].X = 10正常生效了——因为索引器返回的是引用、改的是同一个对象。值类型 vs 引用类型的本质:class 变量存"引用(地址)"、复制引用 → 共享同一对象 → 改了都变;struct 变量存"数据本身"、复制整个数据 → 各是各的 → 改拷贝不影响原值(值类型有 int/struct/enum/DateTime 等;引用类型有 class/数组/string/委托等。)归根结底:struct 是值类型,赋值/传参/索引器取出都是拷贝;改拷贝不影响原值,所以"改 List 元素/传方法改"都不生效——这,就是根源。

第一件事:搞懂值类型与引用类型

定位到根源,我必须把"值类型 vs 引用类型"从根上彻底搞清楚:

值类型(struct)拷贝整个数据; 引用类型(class)拷贝引用(共享对象)

# 核心区别(在赋值/传参/取出时):
#   引用类型(class): 变量 = 指向堆上对象的"引用"。
#     - 赋值 b = a: 复制引用, a 和 b 指向同一对象 → 改一个, 两个都变。
#     - 传参: 传引用, 方法内改对象, 外面也变(改的是同一对象)。
#   值类型(struct): 变量 = 数据本身。
#     - 赋值 b = a: 完整拷贝数据, a 和 b 是两份独立的 → 改一个, 另一个不变。
#     - 传参: 传拷贝, 方法内改, 外面不变(改的是拷贝)。

# 内存位置(常见但非绝对):
#   - 值类型: 常在栈上(或作为对象的内联字段); 引用类型: 对象在堆上。
#   - 别死记"栈/堆", 关键记"值=拷贝数据, 引用=拷贝地址"。

# 那些"改了不生效"的场景, 都是因为"操作的是拷贝":
#   - list[0].X = .. : 索引器返回拷贝 → 编译禁止/无效。
#   - foreach (var s in list) s.X = .. : 循环变量是拷贝 → 改了无效(且编译错)。
#   - 传 struct 进方法改: 改的是拷贝。
#   - struct 作属性 obj.Pos.X = .. : 取出的是拷贝。

# struct 的优点(它存在是有理由的):
#   - 小、无需 GC(不在堆上)、密集存储缓存友好、无空引用。
#   - 适合: 小的、不可变的、有"值语义"的数据(坐标、颜色、金额、时间等)。

# 关键认知: 选 struct/class 看"语义", 不看"大小"。
#   - 要"可变 + 多处共享同一份" → class。
#   - 小的、不可变的、值语义的 → struct(且设计成不可变, 避免拷贝陷阱)。

# 核心: 值类型拷贝整个数据(改拷贝不影响原值)、引用类型拷贝引用(共享对象);
#   "改了不生效"都是因为操作的是拷贝; struct 优先设计成不可变, 按语义而非大小选。

原理终于清晰了。核心区别(在赋值/传参/取出时):引用类型(class),变量是指向堆上对象的引用,赋值/传参复制引用、指向同一对象,改一个两个都变;值类型(struct),变量是数据本身,赋值/传参完整拷贝数据、各是各的,改一个另一个不变(内存上值类型常在栈、引用类型对象在堆,但别死记栈/堆,关键记"值=拷贝数据、引用=拷贝地址"。)那些"改了不生效"的场景,都是因为操作的是拷贝:list[0].X=(索引器返回拷贝)、foreach 改循环变量(拷贝)、传 struct 进方法改(拷贝)、obj.Pos.X=(属性取出是拷贝)struct 存在是有理由的:小、无需 GC、密集存储缓存友好、无空引用,适合"小的、不可变的、有值语义的数据"(坐标、颜色、金额、时间)由此,我刻下一个关键认知:选 struct/class 看"语义"不看"大小"——要"可变+多处共享同一份"用 class;小的、不可变的、值语义的用 struct(且设计成不可变,避开拷贝陷阱)。归根结底:值类型拷贝整个数据(改拷贝不影响原值)、引用类型拷贝引用(共享对象);"改了不生效"都是因为操作的是拷贝;struct 优先设计成不可变,按语义而非大小选。

第二件事:正解——按语义选 class/struct,struct 设计成不可变

搞懂了原理,正解就清晰了:需要"可变、共享引用"语义就用 class;struct 优先设计成不可变;要改集合里的 struct 就整体替换或用 ref

// ✓ 正解一: 需要"可变、共享同一份"的, 用 class(引用类型)
class Point { public int X; public int Y; }   // ✓ 改它就改原对象
var list = new List { new Point { X = 1 } };
list[0].X = 10;                 // ✓ 生效! 改的是同一个对象
Move(list[0]);                  // ✓ 方法内改也生效

// ✓ 正解二: struct 设计成"不可变"(避免"改了拷贝"的迷惑)
readonly struct Point2          // ✓ readonly struct: 字段不可变
{
    public int X { get; }       // 只读属性
    public int Y { get; }
    public Point2(int x, int y) { X = x; Y = y; }
    // "修改"返回新实例, 而不是改自己(像 string 那样)
    public Point2 WithX(int x) => new Point2(x, Y);
}
// → 不可变 struct 不存在"改了不生效"的迷惑(本来就不能改, 只能造新的)。

// ✓ 正解三: 真要改集合里的 struct, 整体替换那个元素
var sl = new List { new Point2(1, 2) };
sl[0] = sl[0].WithX(10);        // ✓ 取出→造个新的→整体放回, 生效。

// ✓ 正解四: 用 ref 传递可变 struct(高级, 性能敏感场景)
void MoveRef(ref Point2 pt) { /* ... */ }   // ref 传引用而非拷贝
// 集合: 用 CollectionsMarshal.AsSpan(list)[0] 拿到 ref(.NET高级用法)

// 选型决策:
//   - 实体/有身份/可变/多处共享 → class。
//   - 小的、不可变、值语义(坐标/金额/颜色/时间)→ readonly struct。
//   - 拿不准 → 用 class(更符合直觉, 不容易踩拷贝坑)。

// 核心: 可变共享语义用 class; struct 设计成 readonly(不可变)避免拷贝迷惑;
//   要改集合里的 struct 就整体替换元素或用 ref; 拿不准就用 class。

修复的方向,核心是"按语义选对类型"正解一,需要"可变、共享同一份"的用 class:换成 class 后,list[0].X = 10 就生效了(改的是同一个对象),传方法改也生效。正解二,struct 设计成不可变:用 readonly struct + 只读属性,"修改"时返回一个新实例(像 string 那样)——不可变的 struct,根本不存在"改了不生效"的迷惑(它本来就不能改、只能造新的),这是用好 struct 的关键姿势正解三,真要改集合里的 struct,就整体替换那个元素:sl[0] = sl[0].WithX(10)(取出→造个新的→整体放回)。正解四,用 ref 传递可变 struct(高级、性能敏感场景)。选型决策:实体/有身份/可变/多处共享用 class;小的、不可变、值语义(坐标/金额/颜色/时间)用 readonly struct;拿不准就用 class(更符合直觉、不容易踩拷贝坑)归根结底:可变共享语义用 class;struct 设计成 readonly(不可变)避免拷贝迷惑;要改集合里的 struct 就整体替换元素或用 ref;拿不准就用 class。

第三件事:值类型拷贝语义引发的其他坑

这次踩坑后,我把值类型拷贝语义引发的其他容易踩的坑,也一并梳理清楚了:

// 值类型拷贝语义引发的其他坑:

// 坑1: foreach 里改不了元素
foreach (var p in structList) { p.X = 10; }   // ✗ p 是拷贝, 改无效(且编译错)
//   → 用 for + 索引, 整体替换: for(int i..) structList[i] = ...;

// 坑2: 把 struct 装进 object(装箱 boxing)
object o = myStruct;            // 装箱: struct 被拷贝到堆上的盒子里
((Point)o).X = 10;             // ✗ 拆箱又是拷贝, 改无效
//   → 频繁装箱拆箱还有性能开销; 注意把 struct 放进 ArrayList/非泛型集合/object。

// 坑3: 属性返回 struct, 改它无效
class Player { public Point Pos { get; set; } }
player.Pos.X = 10;             // ✗ Pos 返回拷贝, 改拷贝无效(编译错)
//   → player.Pos = new Point { X = 10, Y = player.Pos.Y };  整体赋值

// 坑4: 可变 struct 是"万恶之源"(普遍建议)
//   - 可变的 struct 极易引发"改了拷贝"的 bug, C# 官方都建议 struct 设计成不可变。

// 坑5: 默认值 default(struct) 不是 null
//   - struct 不能为 null(除非 Nullable); default 是"所有字段为零值"。
//   - Point p = default;  → X=0, Y=0(不是 null)。

// 坑6: struct 实现接口会装箱
//   - struct 赋给接口变量 → 装箱(变引用类型行为), 注意性能和语义变化。

// 核心: 值类型拷贝语义还会坑 foreach改元素、装箱拆箱、属性返回struct、
//   可变struct; struct 默认值是零值非null; 设计成不可变最省心。

原来值类型拷贝语义的坑,处处都是foreach 里改不了元素(循环变量是拷贝,要用 for+索引整体替换);装箱(boxing)(object o = myStruct 把 struct 拷到堆上的盒子里,拆箱又是拷贝、改无效,还有性能开销);属性返回 struct 改它无效(player.Pos.X = 10 编译错,要整体赋值 player.Pos = new Point{...});可变 struct 是"万恶之源"(C# 官方都建议 struct 设计成不可变);默认值 default(struct) 不是 null(struct 不能为 null,default 是所有字段零值);struct 实现接口会装箱它们的共同根源,都是"struct 是值类型、操作时是拷贝"。归根结底:值类型拷贝语义还会坑 foreach 改元素、装箱拆箱、属性返回 struct、可变 struct;struct 默认值是零值非 null;设计成不可变最省心。

下面这张图,是这次"改 struct 不生效"的成因与解法:

第四件事:struct vs class 速查对照

这次踩坑后,我把 struct 和 class 的关键区别,整理成一张速查表,以后定义类型时一看就知道该用哪个。

维度 struct(值类型) class(引用类型)
赋值/传参 拷贝整个数据 拷贝引用(共享对象)
改"它" 常改的是拷贝, 原值不变 改的是同一对象, 都变
能否为 null 不能(除非 Nullable<T>)
默认值 所有字段零值 null
内存/GC 常在栈, 无需GC 堆上, 受GC管理
适用 小的、不可变、值语义 可变、共享、有身份的实体

这张表,把"该用 struct 还是 class"讲清了。最该牢记的,是"改'它'"那一行:struct 改的常是拷贝、原值不变(本文的坑);class 改的是同一对象、都变——这是它们行为差异的集中体现其余维度也各有讲究:struct 不能为 null(除非 Nullable<T>)、默认值是字段零值、常在栈上无需 GC;class 能为 null、默认 null、堆上受 GC 管理适用场景的判断,核心是:小的、不可变的、有"值语义"的数据(坐标、颜色、金额)用 struct;可变的、需要共享的、有"身份"的实体(用户、订单、连接)用 class它给我的启发是:struct 和 class 的选择,本质是在选"值语义"还是"引用语义"——这是一个关乎程序正确性(改了生不生效)的根本设计决策,而非"用哪个性能好一点"的细枝末节;选错了类型,就等于选错了"这个东西被赋值、传递、修改时,到底该是拷贝还是共享"的底层行为。所以,定义任何一个类型时,都要先想清楚:"我希望它表现得像一个'值'(像数字, 拷来拷去各是各的),还是像一个'对象'(有身份, 大家引用同一个)?"——想清楚了这个,struct 还是 class,答案自然就有了

第五件事:不同语言里"值 vs 引用"的异同

这次踩坑也让我意识到,"值 vs 引用"是个跨语言的普遍主题,各语言处理方式不同。我对比梳理了一下。

语言 值类型 vs 引用类型 注意点
C# struct值 / class引用, 由你选 本文: struct 拷贝、class 共享
Java 基本类型值 / 对象全是引用 没有用户自定义值类型(record也是引用)
Go struct默认值拷贝, 用指针*共享 传 struct 是拷贝, 传 *struct 才共享(见Go切片篇)
Python 一切皆对象(引用), 但有可变/不可变之分 int/str/tuple不可变, list/dict可变
C/C++ 默认值拷贝, 用指针/引用共享 需手动管理, & 取地址 * 解引用
JS 原始类型值 / 对象引用 对象/数组传引用, 原始值传拷贝

这张表,让我看到了"值 vs 引用"这个主题贯穿了几乎所有语言,只是各家处理不同。C# 让你用 struct/class 自由选择值或引用语义(本文);Java基本类型是值、其余全是引用,没有用户自定义值类型;Go 的 struct 默认拷贝、要共享得用指针 *(和它的切片篇一脉相承);Python 一切皆对象(引用),但区分可变/不可变(int/str/tuple 不可变、list/dict 可变);C/C++ 默认拷贝、用指针/引用共享;JS 原始类型值、对象引用它给我的最大启发是:"一个变量赋值、传参时,到底是拷贝了数据,还是共享了引用",是每一门语言都必须回答、而每门语言答案又不尽相同核心问题;而这个问题,直接决定了"改一个会不会影响另一个"这件最基本、也最容易出 bug 的事。所以,每学一门新语言,一定要第一时间搞清楚它的"值/引用"模型:哪些是值(拷贝)、哪些是引用(共享)、自定义类型默认是哪种、怎么显式切换——搞懂了这个,你才算真正掌握了这门语言里"数据是如何流动的"这一最底层的脉络,也才能避开那一整类"改了不生效 / 改了意外影响别处"的经典 bug。

第六件事:定义一个数据类型时,我现在会怎么决策

现在,每当我定义一个数据类型,脑子里都会过一遍这张决策图——核心就一问:我要的是"值语义"还是"引用语义"?

这张图的灵魂,是那个必问的问题:我要的是"值语义"还是"引用语义"?第一问:它需要"可变 + 多处共享同一份"吗?——是(有身份的实体,如用户/订单),用 class不是(它就是个"值"),再追问:它小吗?能设计成不可变吗?——小且能不可变,用 readonly struct;较大或必须可变,用 class 更稳妥用了 struct 就修改靠返回新实例、集合里整体替换;用 class 则改它即改原对象。最后,一条实用兜底:拿不准,就默认用 class(更符合直觉、不容易踩拷贝坑)。这套判断,让我定义类型时,不再凭"它小不小"瞎选,而是从"值/引用语义"这个根本去想——核心始终是:它该像"值"还是像"对象"。

我立下的几条规矩

这场"改 struct 不生效"的事故,换来了我写 C# 时,刻进骨子里的几条铁律:

  1. struct 是值类型,操作的常是拷贝。赋值/传参/索引器取出都是拷贝,改拷贝不影响原值——这是理解一切的前提。
  2. 选 struct/class 看"值/引用语义",不看大小。要可变+共享用 class,小的值语义数据用 struct。
  3. struct 优先设计成 readonly(不可变)。可变 struct 是"万恶之源",不可变就没有"改了拷贝"的迷惑。
  4. 要改集合里的 struct,整体替换或用 ref。list[0].X= 无效;改成 list[0] = newVal。
  5. 警惕装箱、属性返回 struct、foreach 改元素。这些场景操作的都是拷贝,改了不生效。
  6. 拿不准就用 class。class 的引用语义更符合直觉,不容易踩值类型的拷贝坑。
  7. 学语言先搞清"值/引用"模型。哪些拷贝、哪些共享,直接决定"改一个影不影响另一个"。

附:几行代码亲眼看清 struct 和 class 的行为差异

口说无凭。下面这几段,用同样的操作分别作用在 struct 和 class 上,直观对比它们的行为差异,跑一遍胜过千言:

struct PStruct { public int X; }
class  PClass  { public int X; }

class Demo {
    static void ChangeStruct(PStruct p) { p.X = 100; }   // 拷贝
    static void ChangeClass(PClass p)  { p.X = 100; }    // 引用

    static void Main() {
        // ===== 赋值: struct 各是各的, class 共享 =====
        var s1 = new PStruct { X = 1 };
        var s2 = s1;          // 拷贝整个数据
        s2.X = 99;
        System.Console.WriteLine($"struct: s1.X={s1.X}, s2.X={s2.X}");  // 1, 99 (各自独立)

        var c1 = new PClass { X = 1 };
        var c2 = c1;          // 拷贝引用, 指向同一对象
        c2.X = 99;
        System.Console.WriteLine($"class: c1.X={c1.X}, c2.X={c2.X}");   // 99, 99 (共享!)

        // ===== 传参: struct 改不到外面, class 能 =====
        var ss = new PStruct { X = 1 };
        ChangeStruct(ss);
        System.Console.WriteLine($"struct after method: {ss.X}");   // 1 (没变, 改的是拷贝)

        var cc = new PClass { X = 1 };
        ChangeClass(cc);
        System.Console.WriteLine($"class after method: {cc.X}");    // 100 (变了, 改的是同一对象)

        // ===== 集合: struct 取出是拷贝 =====
        var sl = new System.Collections.Generic.List { new PStruct { X = 1 } };
        var got = sl[0];      // 拷贝
        got.X = 50;
        System.Console.WriteLine($"struct in list: {sl[0].X}");     // 1 (没变)
        // sl[0].X = 50;      // ✗ 这行直接编译错!
    }
}

// 核心: 一跑便知 —— struct 赋值/传参各是各的(改一个另一个不变),
//   class 共享同一对象(改一个都变); 把"值 vs 引用"的差异看得一清二楚。

这几段代码,把 struct 和 class 的行为差异,用打印结果摆得清清楚楚赋值:s2 = s1 后改 s2,s1 纹丝不动(1, 99)——struct 是拷贝、各自独立;而 c2 = c1 后改 c2,c1 也跟着变了(99, 99)——class 共享同一对象。传参:ChangeStruct(ss)ss 没变(1),ChangeClass(cc)cc 变了(100)集合:从 List 取出 struct 改它,原 list 没变,而 sl[0].X = 50 直接编译错这一组"同样的操作、截然不同的结果"的对比,胜过千言万语:struct 改一个另一个不变(值语义)、class 改一个都变(引用语义),差异一目了然这,正是我想用这几段代码,留给每一个写 C# 的人的最后一课:当你对"值 vs 引用"这种抽象、又极易混淆的概念感到困惑时,最有效的办法,就是写这样一组最小的对照实验,让同样的代码分别作用在 struct 和 class 上,把它们的行为差异,亲眼"打印"出来。一次"亲眼看到 s1 没变、c1 变了"的实验,带来的理解,远比死记"值类型拷贝、引用类型共享"这句话深刻得多;而那些"相似而不同"的概念,也唯有在这样一次次的亲手辨析中,才能真正被你分得清、记得牢

写在最后

回头看,这场由"struct 拷贝语义"引发的、改了却不生效的事故,真正教给我的,是一个比"分清 struct/class"本身更深的道理:编程语言里,有些差异是"显式的、写在脸上的"(比如不同的关键字、不同的语法),而另一些更危险的差异,是"隐式的、藏在行为里的"——它们长得几乎一样(structclass 的定义和用法看起来差不多),行为却天差地别我犯的错,正是被 structclass 那"几乎一样"的外表骗了——我只看到了它们"写起来差不多",却没意识到它们"赋值、传参、修改时,行为截然相反";这种"外表相似、内核迥异"的特性,最是暗藏杀机,因为它会让你不自觉地,用对待 A 的直觉,去对待 B。这让我深刻地领悟到:真正掌握一门语言,不能只停留在"会写它的语法",更要深入理解"每一个语法结构背后,真正的语义和行为是什么";尤其要警惕那些"看起来可以互换、实则语义不同"的概念——它们才是最容易让你"想当然"地用错的地方所以,学习时,我会更刻意地去追问那些"相似事物之间的本质区别":struct vs class== vs equals、值 vs 引用、深拷贝 vs 浅拷贝……把这些"相似而不同"的概念,一对对地辨析清楚,正是从"会用"走向"精通"的关键台阶。透过相似的外表,看清行为的本质区别——这,是我用一次"改了不生效"的事故,换来的、关于 C#、也关于"如何深入掌握一门语言"的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次定义类型时,先想清楚"我要值语义还是引用语义",那我对着那些改了不生效的 struct 熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我用 Redis 加了分布式锁防并发,结果偶尔还是有两个节点同时拿到锁、把临界区并发执行了,我对着这把"形同虚设"的锁排查了大半天的复盘

2026-6-2 4:31:50

技术教程

我图省事在一处用了 any,结果它像病毒一样扩散、把一整条链路的类型检查全废了,拼错属性名都不报错,我排查了大半天的复盘

2026-6-2 4:45:59

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索