差别
这里会显示出您选择的修订版和当前版本之间的差别。
| 两侧同时换到之前的修订记录 前一修订版 | |||
| csharp:元组 [2025/11/26 15:09] – [1. 为什么使用元组?] 张叶安 | csharp:元组 [2025/11/26 15:13] (当前版本) – 张叶安 | ||
|---|---|---|---|
| 行 157: | 行 157: | ||
| } | } | ||
| </ | </ | ||
| + | |||
| + | ====== C# 进阶:元组 (Tuple) vs 结构体 (Struct) 的选择指南 ====== | ||
| + | |||
| + | 在 C# 开发中,`ValueTuple`(元组)和 `struct`(结构体)都是**值类型**,它们在内存分配(栈上分配)和性能上非常相似。 | ||
| + | |||
| + | 很多开发者会困惑:**既然元组可以命名元素,为什么还需要定义结构体?** 或者 **什么时候应该停止使用元组并转而定义结构体?** | ||
| + | |||
| + | 本指南将帮助您做出正确的架构决策。 | ||
| + | |||
| + | ===== 1. 核心区别概览 ===== | ||
| + | |||
| + | ^ 特性 ^ 元组 (ValueTuple) ^ 结构体 (struct / record struct) ^ | ||
| + | | **语义 (Semantics)** | **临时组合**:仅仅是一堆数据的集合,没有特定的“类型名称”。 | **领域模型**:代表一个具体的概念(如坐标、复数、货币)。 | | ||
| + | | **生命周期** | **短暂**:通常用于方法内部或方法之间的数据传递。 | **长久**:通常贯穿整个应用程序的生命周期。 | | ||
| + | | **逻辑封装** | **无**:不能包含方法、事件或验证逻辑。 | **有**:可以包含构造函数、方法、属性验证。 | | ||
| + | | **序列化** | **困难**:JSON 序列化通常会丢失字段名(变成 Item1...),除非特殊处理。 | **友好**:非常适合作为 DTO (数据传输对象) 进行序列化。 | | ||
| + | | **重构影响** | **高**:修改元组结构可能导致所有调用处都需要修改解构逻辑。 | **低**:添加新属性通常不会破坏现有的 API 调用。 | | ||
| + | |||
| + | ===== 2. 什么时候使用元组 (Tuple)? ===== | ||
| + | |||
| + | 元组的最佳应用场景是**“用完即走”**的临时数据分组。 | ||
| + | |||
| + | ==== 2.1 私有方法或内部工具方法的返回值 ==== | ||
| + | 当你需要从一个 `private` 或 `internal` 方法返回多个值,且这些值只在调用它的地方立即使用时。 | ||
| + | |||
| + | <code csharp> | ||
| + | // ✅ 推荐:这是一个内部辅助方法,不需要专门定义一个类型 | ||
| + | private (int min, int max) GetRange(IEnumerable< | ||
| + | { | ||
| + | return (numbers.Min(), | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== 2.2 简单的解构操作 ==== | ||
| + | 当你需要同时处理几个变量,或者交换变量值时。 | ||
| + | |||
| + | <code csharp> | ||
| + | // ✅ 推荐:解析字符串并立即拆分 | ||
| + | var (isValid, value) = int.TryParse(" | ||
| + | </ | ||
| + | |||
| + | ==== 2.3 LINQ 查询中的中间结果 ==== | ||
| + | 在 LINQ 链式调用中,需要临时携带额外数据到下一个阶段。 | ||
| + | |||
| + | <code csharp> | ||
| + | // ✅ 推荐:在投影中使用元组 | ||
| + | var stats = people | ||
| + | .Select(p => (p.Name, AgeGroup: p.Age / 10)) // 临时组合 | ||
| + | .GroupBy(x => x.AgeGroup); | ||
| + | </ | ||
| + | |||
| + | ===== 3. 什么时候使用结构体 (Struct)? ===== | ||
| + | |||
| + | 当数据具有**特定的业务含义**,或者需要**数据完整性**时,应该使用 `struct`(或 `class`)。 | ||
| + | |||
| + | ==== 3.1 数据代表一个核心领域概念 ==== | ||
| + | 如果这组数据代表了一个具体的“东西”,比如 `Point`(点)、`Complex`(复数)、`Color`(颜色)。 | ||
| + | |||
| + | <code csharp> | ||
| + | // ✅ 推荐:这是一个具体的数学概念,不仅仅是两个 double | ||
| + | public struct Point2D | ||
| + | { | ||
| + | public double X { get; } | ||
| + | public double Y { get; } | ||
| + | | ||
| + | // 结构体可以包含逻辑 | ||
| + | public double DistanceTo(Point2D other) => Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2)); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== 3.2 需要作为公共 API (Public API) 的返回值 ==== | ||
| + | 如果你正在编写一个供他人使用的库(Library)或公共服务。元组作为公共 API 往往显得随意且缺乏文档表现力(虽然 C# 支持,但一旦元组结构变化,会破坏客户端代码)。 | ||
| + | |||
| + | ==== 3.3 需要数据验证 ==== | ||
| + | 元组无法阻止你赋予它非法的值。结构体可以通过构造函数或属性设置器进行拦截。 | ||
| + | |||
| + | <code csharp> | ||
| + | // ✅ 推荐:结构体保证了数据的合法性 | ||
| + | public struct DateRange | ||
| + | { | ||
| + | public DateTime Start { get; } | ||
| + | public DateTime End { get; } | ||
| + | |||
| + | public DateRange(DateTime start, DateTime end) | ||
| + | { | ||
| + | if (end < start) throw new ArgumentException(" | ||
| + | Start = start; | ||
| + | End = end; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== 3.4 需要序列化 (JSON/XML) ==== | ||
| + | 如果你需要将数据返回给前端 API 或保存到文件。元组在序列化时经常出现问题(例如字段名丢失,变成 `Item1`),而 `struct` 或 `record struct` 则非常清晰。 | ||
| + | |||
| + | ===== 4. 决策流程图 (文字版) ===== | ||
| + | |||
| + | - **你需要给这组数据添加行为(方法)吗?** | ||
| + | - 是 -> **Struct** (或 Class) | ||
| + | - 否 -> 继续 | ||
| + | - **你需要验证数据(例如 Age 不能为负数)吗?** | ||
| + | - 是 -> **Struct** (或 Class) | ||
| + | - 否 -> 继续 | ||
| + | - **这组数据会跨越多个类层级传递,或者作为 Public API 吗?** | ||
| + | - 是 -> **Struct** (或 Record Struct) 推荐,为了可维护性。 | ||
| + | - 否 -> 继续 | ||
| + | - **这仅仅是为了从一个私有函数返回两个值吗?** | ||
| + | - 是 -> **Tuple** | ||
| + | - **这仅仅是 LINQ 查询中的临时投影吗?** | ||
| + | - 是 -> **Tuple** | ||
| + | |||
| + | ===== 5. 现代 C# 的折中方案:Record Struct ===== | ||
| + | |||
| + | C# 10 引入了 `record struct`,它结合了元组的简洁性和结构体的强类型特性。如果你觉得定义 `struct` 太麻烦,但又想要具体的类型名称,可以使用它。 | ||
| + | |||
| + | <code csharp> | ||
| + | // 定义非常简洁,但它是一个真正的类型,不是匿名元组 | ||
| + | public record struct UserInfo(string Name, int Age); | ||
| + | |||
| + | // 使用 | ||
| + | public UserInfo GetUser() | ||
| + | { | ||
| + | return new UserInfo(" | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== 6. 总结 ===== | ||
| + | |||
| + | * | ||
| + | * | ||