类型安全集合(C#):NonEmptyList 如何消除运行时异常

发布: (2026年1月2日 GMT+8 08:16)
8 min read
原文: Dev.to

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)。它提供了三个基本的遍历属性:

PropertyTypeDescription(描述)
HeadT第一个元素(保证存在)
TailNonEmptyList?剩余元素,若只有一个元素则为 null
InitNonEmptyList?除最后一个元素之外的所有元素,若只有一个元素则为 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);
    }
}

摘要

  • 编译时保证集合永不为空。
  • 消除一整类运行时异常(InvalidOperationExceptionNullReferenceException)。
  • 丰富的函数式 APIMapFlatMapReduceFoldPartition、异步变体等)。
  • 不可变和可变两种变体,转换无缝。
  • 一等支持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 上给它点一个 ⭐️。

Back to Blog

相关文章

阅读更多 »

MiniScript 2026 路线图

2026 展望 随着 2025 接近尾声,是时候展望 2026 了!MiniScript 已经八岁。许多编程语言真的进入了它们的……