-
结构体 struct:与类的区别、值类型语义
知识点
- 内存语义与核心价值
struct 是值类型,默认分配在线程栈上,具有副本语义(赋值、传参时复制完整数据);
class 是引用类型,分配在托管堆,传递时仅复制引用。
企业开发中,结构体是高吞吐、低 GC 场景的核心优化手段,可大幅减少垃圾回收压力。
- 与类的选型级差异
| 维度 | struct 结构体 | class 类 |
|---|---|---|
| 继承 | 不支持继承,隐式继承自System.ValueType | 支持单继承、多接口实现 |
| 构造函数 | C# 10 前无自定义无参构造,编译器始终生成默认零值构造;C# 10 + 可自定义,但无法覆盖default(T)零值行为 | 可自由定义构造函数,无默认构造限制 |
| 析构函数 | 不能定义终结器,栈内存随方法作用域自动释放 | 可定义终结器,由 GC 回收 |
| 相等性 | 默认逐字段值比较(反射实现,性能低) | 默认引用地址比较 |
| GC 影响 | 栈上分配无 GC;装箱 / 作为类字段时进入堆 | 全程受 GC 管理 |
-
企业级进阶特性
-
-
readonly struct:只读结构体,强制所有成员不可变,消除装箱时的防御性副本,是高性能开发的标准写法。
-
-
-
ref struct:栈专属结构体,无法被装箱,配合Span<T>实现内存安全的高性能缓冲区操作,用于网络报文、文件 IO 处理。
-
-
-
StructLayout特性:精确控制内存布局,用于 P/Invoke 互操作、C++ 动态库调用。
-
-
-
企业场景:高吞吐日志事件、批量计算中间结果、网络协议报文、轻量级数据快照、互操作数据载体。
示例 高吞吐业务日志系统的只读日志事件结构体
模拟百万级 QPS 的业务日志处理,使用readonly struct承载日志事件,配合Span<T>栈内存批量处理,全程零 GC 分配,满足互联网级性能要求。
/// <summary>
/// 只读日志事件结构体:高吞吐场景下的无GC数据载体
/// readonly struct:只读结构体,所有字段自动不可修改,分配在栈上,减少堆内存GC压力
/// IEquatable<LogEvent>:实现值类型专用相等比较接口,避免装箱,提升比较性能
/// </summary>
public readonly struct LogEvent : IEquatable<LogEvent>
{
// 只读属性,readonly struct内属性不能在构造之外修改
public int EventId { get; } // 日志事件编号
public LogLevel Level { get; } // 日志级别
public DateTimeOffset Timestamp { get; } // 日志时间戳(推荐DateTimeOffset,带时区,日志系统常用)
public Guid TraceId { get; } // 链路追踪Id,用于分布式追踪,定位一次请求全链路日志
public string MessageTemplate { get; } // 日志消息模板(Serilog风格模板字符串)
/// <summary>
/// 构造函数,初始化只读结构体的全部属性
/// </summary>
/// <param name="eventId">事件编号</param>
/// <param name="level">日志级别</param>
/// <param name="timestamp">日志时间</param>
/// <param name="traceId">追踪ID</param>
/// <param name="messageTemplate">日志模板,不允许null</param>
public LogEvent(int eventId, LogLevel level, DateTimeOffset timestamp, Guid traceId, string messageTemplate)
{
EventId = eventId;
Level = level;
Timestamp = timestamp;
TraceId = traceId;
// 参数校验:模板字符串不能为null,抛出参数空异常
MessageTemplate = messageTemplate ?? throw new ArgumentNullException(nameof(messageTemplate));
}
/// <summary>
/// 自定义值比较,替代默认反射实现,提升性能
/// IEquatable<T>接口方法,专门用于LogEvent之间的相等判断,**不会发生装箱**
/// 默认结构体Equals会用反射遍历字段,性能差;手动实现直接逐个字段对比
/// </summary>
/// <param name="other">待比较的另一个LogEvent</param>
/// <returns>true代表两个日志事件所有字段完全一致</returns>
public bool Equals(LogEvent other)
{
return EventId == other.EventId
&& Level == other.Level
&& Timestamp.Equals(other.Timestamp)
&& TraceId.Equals(other.TraceId)
// Ordinal:原生二进制字符串对比,不做文化处理,速度最快,日志场景首选
&& string.Equals(MessageTemplate, other.MessageTemplate, StringComparison.Ordinal);
}
/// <summary>
/// 重写object.Equals,处理object类型比较(会装箱,尽量少用)
/// 模式匹配:判断obj是不是LogEvent,如果是则调用上面高性能Equals
/// </summary>
public override bool Equals(object? obj) => obj is LogEvent other && Equals(other);
/// <summary>
/// 重写哈希码,用于HashSet/Dictionary等集合。
/// HashCode.Combine:把多个字段合并生成哈希值,C#内置高性能组合哈希
/// 自定义Equals必须配套重写GetHashCode,否则放入字典会出现匹配异常
/// </summary>
public override int GetHashCode() => HashCode.Combine(EventId, Level, Timestamp, TraceId, MessageTemplate);
/// <summary>
/// ==运算符重载,直接调用Equals,方便用 `a == b` 语法比较两个结构体
/// </summary>
public static bool operator ==(LogEvent left, LogEvent right) => left.Equals(right);
/// <summary>
/// !=运算符重载
/// </summary>
public static bool operator !=(LogEvent left, LogEvent right) => !left.Equals(right);
}
/// <summary>
/// 日志级别枚举,和微软标准ILogger日志级别语义对齐
/// Trace: 详细跟踪 | Debug:调试 | Information:普通信息 | Warning:警告 | Error:错误 | Critical:致命
/// </summary>
public enum LogLevel
{
Trace,
Debug,
Information,
Warning,
Error,
Critical
}
/// <summary>
/// 批量日志处理器:基于Span栈内存零GC处理
/// static class:静态类,不能实例化,里面全部是静态工具方法
/// Span<T>:内存视图类型,可以指向栈内存、堆内存,**不需要装箱**,高性能处理连续内存
/// </summary>
public static class LogEventProcessor
{
/// <summary>
/// 遍历日志集合,统计Error及以上级别的日志数量,收集对应的TraceId
/// 返回元组 (errorCount, List<Guid>):多返回值语法
/// </summary>
/// <param name="events">日志事件Span视图,可以指向stackalloc栈数组</param>
/// <returns>(错误日志总数,错误日志对应的TraceId列表)</returns>
public static (int errorCount, List<Guid> errorTraceIds) ProcessErrorLogs(Span<LogEvent> events)
{
int errorCount = 0;
var traceIds = new List<Guid>();
// ref readonly var evt:只读引用遍历Span,**不复制结构体副本**,性能极高
// 普通foreach会把struct拷贝一份;ref readonly直接引用原内存,避免拷贝开销
foreach (ref readonly var evt in events)
{
// >= Error:Error、Critical都会被统计
if (evt.Level >= LogLevel.Error)
{
errorCount++;
traceIds.Add(evt.TraceId);
}
}
return (errorCount, traceIds);
}
}
// 调用演示
public static void StructDemo()
{
// stackalloc:在【栈内存】分配数组,**不进入托管堆,不产生GC**,适合短生命周期、小容量数组
// ⚠️栈空间有限,stackalloc不能分配很大数组,一般几百以内
Span<LogEvent> logEvents = stackalloc LogEvent[100];
var baseTime = DateTimeOffset.Now;
// 填充模拟日志数据
for (int i = 0; i < logEvents.Length; i++)
{
logEvents[i] = new LogEvent(
eventId: 1000 + i,
// i%10 ==0 模拟每10条产生一条Error日志
level: i % 10 == 0 ? LogLevel.Error : LogLevel.Information,
timestamp: baseTime.AddMilliseconds(i * 10),
traceId: Guid.NewGuid(),
messageTemplate: "订单处理事件 - 订单ID:{OrderId}");
}
// 值语义验证:结构体赋值是完整拷贝,副本独立
var original = logEvents[0];
var copy = original;
// copy.EventId = 9999; // 编译报错:readonly struct,所有属性只读,实例创建后不能修改
// 调用处理器,传入Span(直接指向栈上数组,无拷贝)
var (errorCount, errorTraces) = LogEventProcessor.ProcessErrorLogs(logEvents);
Console.WriteLine($"错误日志数量:{errorCount}");
}
语法注意事项
- 装箱拆箱开销:结构体转换为
object、接口时会发生装箱,产生堆分配与 GC 压力;高频场景优先使用泛型约束where T : struct替代接口参数 。 - 默认加 readonly:业务开发中定义结构体默认加
readonly,避免成员方法隐式修改this导致的防御性副本,同时提升编译器优化空间。 - 栈分配限制:
stackalloc分配的内存不可逃逸出方法作用域;单块内存建议不超过 1KB,避免栈溢出。 - 零值合法性:结构体始终存在默认零值,即使自定义无参构造也无法覆盖
default(T),业务中需显式校验零值是否合法。 - 作为字典键必须重写哈希:默认值比较通过反射实现,性能极差,高并发场景会成为瓶颈,必须手动实现
IEquatable<T>。 - 体积控制:结构体过大时,复制开销会超过引用类型;64 位系统下建议单结构体不超过 16 字节,否则考虑改用类。
-
记录类型 record:声明与相等性(C# 9+)
知识点
- 核心定位
record(全称record class)是引用类型的不可变数据载体,
编译器自动生成值相等性逻辑、解构方法、 ToString 方法, 核心解决 “数据对象按值比较” 的痛点,是 DDD 领域驱动设计中值对象、API 层 DTO 的首选实现。
-
企业级核心特性
-
- 值相等性:两个实例只要所有属性值完全相同,就判定为相等,区别于 class 的引用相等,无需手动重写
Equals/GetHashCode。
- 值相等性:两个实例只要所有属性值完全相同,就判定为相等,区别于 class 的引用相等,无需手动重写
-
- 不可变性:默认属性为
init-only,对象创建后不可修改,保证分布式传输、缓存场景下的数据一致性。
- 不可变性:默认属性为
-
- 非破坏性修改:通过
with表达式基于现有实例创建修改后的新实例,保留不可变特性的同时简化数据变更。
- 非破坏性修改:通过
-
- 自动解构:编译器生成解构方法,支持按属性快速拆解变量。
-
-
编译器自动生成成员:
EqualityContract属性、强类型Equals、GetHashCode、相等运算符、ToString、全属性构造函数、解构器。 -
典型企业场景:REST API 请求 / 响应 DTO、事件溯源领域事件、配置项对象、业务数据快照、分布式消息载体。
示例 电商采购系统的不可变订单 DTO 与地址值对象
模拟微服务间的采购订单数据传输,用record 实现不可变的订单、地址、明细,演示值相等性、嵌套非破坏性修改、作为缓存键的企业级用法。
/// <summary>
/// 地址值对象:典型DDD值对象
/// record特性:自动实现值相等,实例不可变;适合描述没有唯一标识、靠字段内容判断相等的业务概念
/// DDD值对象核心特征:不可变;只关心内容是否相同,不关心是不是同一个对象
/// </summary>
public record Address
{
// init访问器:只能在构造函数 / 对象初始化器赋值,初始化完成后,外部不能修改,保证不可变
public string Province { get; init; }
public string City { get; init; }
public string Detail { get; init; }
public string PostalCode { get; init; }
/// <summary>
/// 构造函数:创建地址时校验业务规则,保证值对象永远合法(DDD:不允许存在非法的值对象)
/// </summary>
public Address(string province, string city, string detail, string postalCode)
{
// 业务校验:省份、城市不能为空空白字符串
if (string.IsNullOrWhiteSpace(province)) throw new ArgumentException("省份不能为空");
if (string.IsNullOrWhiteSpace(city)) throw new ArgumentException("城市不能为空");
Province = province;
City = city;
Detail = detail;
PostalCode = postalCode;
}
}
/// <summary>
/// 订单明细记录:record记录,不可变,存放单行商品信息
/// </summary>
public record OrderLineItem
{
public Guid ProductId { get; init; }
public string ProductName { get; init; }
public int Quantity { get; init; }
public decimal UnitPrice { get; init; }
/// <summary>
/// 表达式主体成员,只读计算属性,不需要存储字段
/// 不需要set,读取时实时计算:总金额 = 数量 * 单价
/// </summary>
public decimal TotalPrice => Quantity * UnitPrice;
}
/// <summary>
/// 采购订单主记录:嵌套不可变数据,DDD聚合根示例
/// record会自动生成Equals、==、GetHashCode,基于全部属性的值做比较,而不是引用地址
/// </summary>
public record PurchaseOrder
{
public Guid OrderId { get; init; }
public string SupplierCode { get; init; }
public Address ShippingAddress { get; init; }
// IReadOnlyList:只读集合,外部无法Add/Remove,防止外部修改订单明细
public IReadOnlyList<OrderLineItem> Items { get; init; }
/// <summary>
/// 订单总金额,实时计算,不持久化存储
/// </summary>
public decimal TotalAmount => Items.Sum(i => i.TotalPrice);
public DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// 订单构造,强制创建订单时传入必要数据,自动赋值创建时间
/// </summary>
public PurchaseOrder(Guid orderId, string supplierCode, Address shippingAddress, IReadOnlyList<OrderLineItem> items)
{
OrderId = orderId;
SupplierCode = supplierCode;
ShippingAddress = shippingAddress;
// 非空校验:订单明细不能为空
Items = items ?? throw new ArgumentNullException(nameof(items));
CreatedAt = DateTimeOffset.Now;
}
}
// 业务演示
public static void RecordDemo()
{
// 构造地址值对象
var address = new Address("上海市", "浦东新区", "张江高科技园区", "201203");
// 构建订单明细List
var items = new List<OrderLineItem>
{
// Record对象初始化简写:new() 省略类型名
new() { ProductId = Guid.NewGuid(), ProductName = "企业服务器", Quantity = 2, UnitPrice = 15000m },
new() { ProductId = Guid.NewGuid(), ProductName = "27寸显示器", Quantity = 5, UnitPrice = 1800m }
};
// AsReadOnly:包装List为只读视图,外部不能修改内部集合
var order1 = new PurchaseOrder(Guid.NewGuid(), "SUP001", address, items.AsReadOnly());
// ===================== 语法1:with表达式 【非破坏性修改】=====================
// with:基于原有record复制一份新实例,只修改指定属性;原有对象保持不变(不可变对象核心能力)
var newAddress = address with { Detail = "张江集电港B区" };
var order2 = order1 with { ShippingAddress = newAddress };
// ===================== 语法2:with { } 浅拷贝 =====================
// 不写任何属性,代表复制全部字段,生成一个全新副本
var order3 = order1 with { };
// Record自动实现值相等,对比所有属性的值,不是对比内存引用
Console.WriteLine($"order1 == order3:{order1 == order3}"); // True,所有字段完全一样
Console.WriteLine($"order1 == order2:{order1 == order2}"); // False,配送地址Detail不一样
// ===================== 语法3:解构 Deconstruct =====================
// Record自动生成Deconstruct解构方法,一次性把属性拆到多个变量
var (orderId, supplierCode, _, _, _) = order1;
// _ 是弃元,不需要接收该属性,直接丢弃
Console.WriteLine($"订单ID:{orderId},供应商:{supplierCode}");
// ===================== 特性4:可直接作为Dictionary的Key =====================
// 因为record自动重写Equals与GetHashCode,基于值比较,适合做缓存Key
var orderCache = new Dictionary<PurchaseOrder, string> { [order1] = "已提交审核" };
// order3是order1拷贝出来的,值完全相同,可以命中缓存
Console.WriteLine($"order3缓存状态:{orderCache[order3]}");
}
语法注意事项
- 引用类型本质:
record默认是引用类型,仍然分配在堆上,会产生 GC;不可变是语法约束,不是性能优化手段。
- 浅拷贝限制:
with表达式是浅拷贝,引用类型成员只复制引用;包含集合时建议使用不可变集合(IReadOnlyList、ImmutableList),避免共享修改。
- 值相等的嵌套坑:
如果 record 中包含自定义引用类型成员,该成员仍按引用相等比较;要保证完整值相等,所有嵌套成员都必须实现值相等(如都用 record)。
- 不可变性是编译级:
init属性仅在编译期限制修改,通过反射仍可修改,不能作为安全边界,仅用于业务约束。
- 敏感信息风险:
编译器自动生成的ToString会输出所有属性值,包含密码、手机号的 record 必须重写ToString,防止日志泄露。
- 继承相等性:
子类 record 与父类 record 比较时,会先校验类型,类型不同直接返回 false;避免跨继承层级的相等判断。
- 业务边界:
record 定位是贫血数据载体,不建议编写复杂业务逻辑,业务逻辑应放在服务类中。
-
struct 与 record class 简析(C# 9+、10+)
知识点
-
类型定位对比
-
-
record struct(C# 10+):值类型记录,结合了 struct 的值语义与 record 的语法糖(值相等、with、解构),是轻量级高性能数据载体。
-
-
-
record class:即 C# 9 的record,引用类型记录,面向数据传输与领域建模。
-
-
-
企业选型核心差异
| 维度 | record struct | record class |
|---|---|---|
| 类型本质 | 值类型,默认栈分配 | 引用类型,堆分配 |
| GC 影响 | 栈上无 GC,装箱时产生 GC | 全程受 GC 管理 |
| 默认可变性 | 属性默认可读写(值类型惯例) | 属性默认 init-only(不可变) |
| 继承 | 不支持继承,仅可实现接口 | 支持单继承 |
| 副本行为 | 赋值时全量值拷贝 | 赋值时仅拷贝引用 |
| 适用体积 | 小数据(建议≤32 字节) | 任意大小,大数据更优 |
-
进阶最优实践:
readonly record struct只读值类型记录,兼具不可变性与高性能,可完全替代手动实现IEquatable的普通 struct,是高频计算场景的首选。 -
典型企业场景
-
-
record struct:高频计算中间结果、表格单元格数据、方法多返回值结构化包装、批量数据处理元素。
-
-
-
record class:API DTO、领域值对象、事件消息、配置对象、长生命周期数据。
-
-
示例 金融行情系统汇率快照(record struct) + 客户信息 DTO(record class)
分别演示两种 record 的典型企业场景:高频行情处理用值类型降本增效,客户信息用引用类型支持继承扩展。
// ========== 场景1:高频汇率行情 - readonly record struct ==========
/// <summary>
/// 汇率快照:百万级每秒的行情数据,零GC开销
/// </summary>
public readonly record struct ExchangeRateSnapshot
{
public string CurrencyPair { get; init; } // 货币对,如USD/CNY
public decimal BidPrice { get; init; } // 买入价
public decimal AskPrice { get; init; } // 卖出价
public long TimestampUnixMs { get; init; } // 毫秒级时间戳
public decimal MidPrice => (BidPrice + AskPrice) / 2m; // 计算中间价
}
/// <summary>
/// 行情计算器:批量处理汇率快照
/// </summary>
public static class ExchangeRateCalculator
{
/// <summary>
/// 计算指定货币对的小时均价
/// </summary>
public static decimal CalculateHourlyAverage(Span<ExchangeRateSnapshot> snapshots, string currencyPair)
{
decimal sum = 0m;
int count = 0;
foreach (ref readonly var snap in snapshots)
{
if (string.Equals(snap.CurrencyPair, currencyPair, StringComparison.Ordinal))
{
sum += snap.MidPrice;
count++;
}
}
return count == 0 ? 0m : sum / count;
}
}
// ========== 场景2:客户信息DTO - record class ==========
/// <summary>
/// 基础审计DTO:供所有业务DTO继承
/// </summary>
public abstract record BaseDto
{
public Guid Id { get; init; }
public DateTimeOffset CreatedAt { get; init; }
public string? CreatedBy { get; init; }
public DateTimeOffset? UpdatedAt { get; init; }
}
/// <summary>
/// 客户信息DTO:继承基础审计字段
/// </summary>
public record CustomerDto : BaseDto
{
public string CustomerName { get; init; }
public string ContactPhone { get; init; }
public string Email { get; init; }
public int CustomerLevel { get; init; }
/// <summary>
/// 工厂方法:创建新客户DTO
/// </summary>
public static CustomerDto Create(string name, string phone, string email, string operatorName)
{
return new CustomerDto
{
Id = Guid.NewGuid(),
CreatedAt = DateTimeOffset.Now,
CreatedBy = operatorName,
CustomerName = name,
ContactPhone = phone,
Email = email,
CustomerLevel = 1
};
}
}
// 调用演示
public static void RecordStructDemo()
{
// --- record struct 演示 ---
Span<ExchangeRateSnapshot> rates = stackalloc ExchangeRateSnapshot[50];
var baseTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
for (int i = 0; i < rates.Length; i++)
{
rates[i] = new ExchangeRateSnapshot
{
CurrencyPair = i % 2 == 0 ? "USD/CNY" : "EUR/CNY",
BidPrice = 7.25m + i * 0.001m,
AskPrice = 7.26m + i * 0.001m,
TimestampUnixMs = baseTime + i * 100
};
}
// with表达式生成新快照
var updatedRate = rates[0] with { BidPrice = 7.245m, AskPrice = 7.255m };
var avgUsd = ExchangeRateCalculator.CalculateHourlyAverage(rates, "USD/CNY");
Console.WriteLine($"USD/CNY小时均价:{avgUsd:F4}");
// --- record class 演示 ---
var customer1 = CustomerDto.Create("张三", "13800138000", "zhangsan@example.com", "admin");
// 非破坏性修改:升级客户等级
var customer2 = customer1 with { CustomerLevel = 2, UpdatedAt = DateTimeOffset.Now };
Console.WriteLine($"客户等级变更前:{customer1.CustomerLevel},变更后:{customer2.CustomerLevel}");
}
语法注意事项
- 默认可变性差异:
record struct的属性默认可读写,要不可变必须加readonly关键字,或手动声明init属性。 - 拷贝开销阈值:单实例超过 32 字节时,值拷贝性能会低于
record class,需评估选型。 - 装箱行为:
record struct赋值给object、接口时会发生装箱,和普通 struct 一致;高频场景避免装箱。 - 继承限制:
record struct不能继承也不能被继承;需要继承的场景只能用record class。 - with 本质区别:
record struct的with是创建新的值副本,全字段复制;record class的with是创建新的堆对象,浅拷贝引用。 - 选型原则:纯数据、小体积、高频创建、追求零 GC → 优先
readonly record struct;需要继承、数据量大、生命周期长 → 优先record class。