类型安全集合(C#):NonEmptyList 如何消除运行时异常
Source: Dev.to
请提供您希望翻译的具体文本内容,我将为您翻译成简体中文并保留原始的格式、Markdown 语法和技术术语。
传统防御式编程
public async Task CalculateAverageOrderValueAsync(int customerId)
{
var orders = await _repository.GetOrdersAsync(customerId);
return orders.Average(o => o.Total); // Throws if orders is empty
}
if (orders.Any())
{
return orders.Average(o => o.Total);
}
return 0m; // What does zero mean here? No orders? An error?
缺点
| 问题 | 为什么是个问题 |
|---|---|
| 静默失败 | 返回默认值会掩盖底层问题 |
| 代码重复 | 空检查散布在代码库的各处 |
| 语义模糊 | 0 是有效的平均值还是错误状态? |
Source: …
类型安全的解决方案:NonEmptyList
如果我们能够在类型签名中直接表达 “此集合必须至少包含一个元素”,该多好?这正是 NonEmptyList 所提供的功能。
public async Task CalculateAverageOrderValueAsync(
NonEmptyList<Order> orders) // 编译时保证非空
{
return orders.Average(o => o.Total); // 永远安全
}
编译器现在会强制执行我们的业务规则。调用方必须提供非空集合,从而把验证从运行时移到编译时。
dotnet add package Masterly.NonEmptyList
兼容 .NET 6.0 和 .NET 8.0。
NonEmptyList 的灵感来源于函数式语言(例如 Scala 的 List)。它提供了三个基本的遍历属性:
| Property | Type | Description(描述) |
|---|---|---|
| Head | T | 第一个元素(保证存在) |
| Tail | NonEmptyList? | 剩余元素,若只有一个元素则为 null |
| Init | NonEmptyList? | 除最后一个元素之外的所有元素,若只有一个元素则为 null |
var numbers = new NonEmptyList(1, 2, 3, 4, 5);
var head = numbers.Head; // 1
var tail = numbers.Tail; // NonEmptyList { 2, 3, 4, 5 }
var init = numbers.Init; // NonEmptyList { 1, 2, 3, 4 }
var last = numbers.Last; // 5
模式匹配
var (first, rest) = numbers;
// first = 1, rest = NonEmptyList { 2, 3, 4, 5 }
在保持非空保证的前提下进行转换
var orders = new NonEmptyList(order1, order2, order3);
// 映射为 DTO
NonEmptyList<OrderDto> dtos = orders.Map(o => new OrderDto(o));
// 展平嵌套集合
NonEmptyList<Item> allItems = orders.FlatMap(o => o.Items);
聚合
传统 LINQ 需要提供种子值,因为集合可能为空:
// 空集合会抛异常
decimal total = orders.Aggregate(0m, (sum, o) => sum + o.Total);
使用 NonEmptyList 可以在不提供种子的情况下使用 Reduce:
// 永远安全——集合已保证非空
decimal total = orders.Reduce((sum, o) => sum + o.Total);
当结果类型与元素类型不同:
var summary = orders.Fold(
new OrderSummary(),
(summary, order) => summary.AddOrder(order)
);
分区与分组
var (highValue, standard) = orders.Partition(o => o.Total > 1000m);
var byStatus = orders.GroupByNonEmpty(o => o.Status);
// 结果:Dictionary<Status, NonEmptyList<Order>>
对单元素与多元素情况的显式处理
string ProcessOrders(NonEmptyList<Order> orders) =>
orders.Match(
single: order => $"Processing single order: {order.Id}",
multiple: (first, remaining) =>
$"Processing batch: {first.Id} and {remaining.Count} more"
);
这种模式在递归算法和批处理逻辑中尤为出色。
异步支持
NonEmptyList 提供顺序和并行的异步操作:
var enrichedOrders = await orders.MapAsync(
async order => await _enrichmentService.EnrichAsync(order)
);
var results = await orders.MapParallelAsync(
async order => await _processor.ProcessAsync(order),
maxDegreeOfParallelism: 4
);
var totalRevenue = await orders.FoldAsync(
0m,
async (sum, order) => sum + await _calculator.CalculateRevenueAsync(order)
);
不可变性
var immutable = new ImmutableNonEmptyList(1, 2, 3);
// 所有操作都会返回新实例
var appended = immutable.Append(4); // [1, 2, 3, 4]
var prepended = immutable.Prepend(0); // [0, 1, 2, 3]
// 原始实例保持不变
Console.WriteLine(immutable.Count); // 3
在可变和不可变变体之间的转换:
var immutable = mutableList.ToImmutable();
var mutable = immutableList.ToMutable();
序列化
内置的 System.Text.Json 转换器会自动处理(反)序列化:
var orders = new NonEmptyList(order1, order2);
string json = JsonSerializer.Serialize(orders);
var restored = JsonSerializer.Deserialize<NonEmptyList<Order>>(json);
反序列化会强制执行非空约束:
// 抛出带有描述性信息的 JsonException
JsonSerializer.Deserialize<NonEmptyList<Order>>("[]");
Entity Framework Core 集成
一个专用包提供数据库持久化支持:
dotnet add package Masterly.NonEmptyList.EntityFrameworkCore
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Store as JSON column
modelBuilder.Entity<Order>()
.Property(p => p.Tags)
.HasNonEmptyListConversion();
// Configure relationships with non‑empty constraint
modelBuilder.Entity<Order>()
.HasNonEmptyManyWithOne(
o => o.LineItems,
li => li.Order,
li => li.OrderId
);
}
使用该类型的示例服务
public class OrderProcessingService
{
public async Task<ProcessingResult> ProcessBatchAsync(
NonEmptyList<Order> orders,
CancellationToken cancellationToken = default)
{
// Validate all orders in parallel
var validationResults = await orders.MapParallelAsync(
async order => await _validator.ValidateAsync(order, cancellationToken),
maxDegreeOfParallelism: 8,
cancellationToken);
// Continue only if every order is valid
if (validationResults.Any(r => !r.IsSuccess))
return ProcessingResult.Failure("One or more orders are invalid.");
// Process the batch
var processingResults = await orders.MapParallelAsync(
async order => await _processor.ProcessAsync(order, cancellationToken),
maxDegreeOfParallelism: 8,
cancellationToken);
return ProcessingResult.Success(processingResults);
}
}
摘要
- 编译时保证集合永不为空。
- 消除一整类运行时异常(
InvalidOperationException、NullReferenceException)。 - 丰富的函数式 API(
Map、FlatMap、Reduce、Fold、Partition、异步变体等)。 - 不可变和可变两种变体,转换无缝。
- 一等支持JSON 序列化和 EF Core 持久化。
添加此包,在需要非空保证的地方替换普通 IEnumerable/List,让编译器强制执行你的业务规则。 🚀
代码示例
elAsync(
order => _validator.ValidateAsync(order, cancellationToken),
maxDegreeOfParallelism: 8
);
// Partition by validation result
var (valid, invalid) = orders
.Zip(validationResults)
.Partition(pair => pair.Item2.IsValid);
if (invalid.Any())
{
_logger.LogWarning("Found {Count} invalid orders", invalid.Count);
}
// Process valid orders
var processed = valid switch
{
null => new ProcessingResult(0, 0m),
var validOrders => await ProcessValidOrdersAsync(
validOrders.Map(pair => pair.Item1),
cancellationToken
)
};
return processed;
}
private async Task ProcessValidOrdersAsync(
NonEmptyList<Order> orders,
CancellationToken cancellationToken)
{
// Calculate totals – Reduce is safe, no empty check needed
var totalRevenue = orders.Reduce((sum, o) => sum + o.Total);
// Process with pattern matching
await orders.Match(
single: async order =>
await _processor.ProcessSingleAsync(order, cancellationToken),
multiple: async (priority, remaining) =>
{
await _processor.ProcessPriorityAsync(priority, cancellationToken);
await remaining.ForEachAsync(
order => _processor.ProcessAsync(order, cancellationToken)
);
}
);
return new ProcessingResult(orders.Count, totalRevenue);
}
理想的使用场景
- 空集合无效的领域模型(例如,订单必须包含行项目)
- 需要至少一个元素的 API 合约
- 空输入视为错误的聚合操作
- 函数式编程模式(head/tail 递归)
在以下情况下考虑替代方案
- 空集合在你的领域中是有效的。
- 你需要从空状态逐步构建集合。
- 性能至关重要,分配开销很关键。
资源
- GitHub Repository
- Documentation Wiki
- NuGet Package
- EF Core Package
摘要
NonEmptyList 将一种成熟的函数式编程模式引入 C#,使开发者能够直接在类型系统中表达域约束。通过将验证从运行时转移到编译时,我们消除了整类错误,同时让代码更具表现力且自我文档化。
如果此库对您的项目有帮助,请考虑在 GitHub 上给它点一个 ⭐️。