You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#使用Language-Ext实现类似Rust?运算符的简化错误传播

问题

在C#中使用Language-Ext库开展函数式编程时,是否存在可以简化错误处理的方案,实现与Rust语言的错误传播运算符相近的效果?

举例来说,我希望简化下方Create方法中的错误处理逻辑:

using LanguageExt;
using LanguageExt.Common;

using static LanguageExt.Prelude;

public class ProductAggregate
{
    public string Name { get; private set; } = string.Empty;

    public static Fin<ProductAggregate> Create(string name)
    {
        var product = new ProductAggregate();

        // 我认为这段写法过于冗余,希望可以简化:
        if (product.SetName(name) is { IsFail: true } errFin)
        {
            return (Error)errFin;
        }

        return product;
    }

    public Fin<Unit> SetName(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            return Error.New("Product name cannot be null or empty.");
        }

        Name = name;

        return unit;
    }
}

我非常认可Rust的错误处理实现方式:

fn get_current_date() -> Result<String, reqwest::Error> {
  let url = "https://postman-echo.com/time/object";
  let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
  let date = res["years"].to_string();
  Ok(date)
}

上述代码中的?运算符可链式调用后续步骤,其核心逻辑是检查左侧的调用返回值是否为错误类型的Result,如果是则立刻提前返回该错误,要求错误类型与外层函数返回值的错误类型(reqwest::Error)匹配。

是否有办法在C#中实现和这种优雅度相近的错误传播写法?

编辑注:已采纳Mark Seemann的建议,补充提供了最小可复现示例。


解答

完全不需要手动编写IsFail分支判断处理错误传播,LanguageExt内置的Fin<T>本身就是支持错误自动短路传播的Monad类型,直接使用内置的组合子就能实现和Rust?运算符几乎一致的体验,常用写法有两种:

1. 链式调用Bind/Map组合子

Bind和Map是函数式错误处理的核心方法,行为逻辑和Rust的?完全对齐:

  • 如果前序操作返回Fail,直接终止后续逻辑,把错误向上返回
  • 如果前序操作返回Succ,把成功值传入后续逻辑继续执行

你给出的单步SetName场景,Create方法可以直接精简为3行代码,完全不需要手动判断错误:

public static Fin<ProductAggregate> Create(string name)
{
    var product = new ProductAggregate();
    return product.SetName(name).Map(_ => product);
}

如果是多步初始化场景(比如还要校验价格、分类等),可以用Bind串起所有操作:

public static Fin<ProductAggregate> Create(string name, decimal price, int categoryId)
{
    var product = new ProductAggregate();
    return product.SetName(name)
        .Bind(_ => product.SetPrice(price))
        .Bind(_ => product.SetCategory(categoryId))
        .Map(_ => product);
}

2. LINQ查询语法(更接近线性书写体验)

Fin<T>原生实现了C#的LINQ查询模式,用from子句串联操作的写法和Rust连续写?的逻辑几乎一一对应,可读性更强,不需要嵌套Lambda:

public static Fin<ProductAggregate> Create(string name, decimal price, int categoryId)
{
    var product = new ProductAggregate();
    return from _ in product.SetName(name)
           from _ in product.SetPrice(price)
           from _ in product.SetCategory(categoryId)
           select product;
}

每一行from就等价于Rust里一次带?的调用,任何一步返回错误,整个表达式会立刻短路返回对应错误,不会执行后续逻辑。

你给出的Rust请求示例,用LanguageExt改写后逻辑完全对等,没有任何手动错误判断:

public static Fin<string> GetCurrentDate()
{
    const string url = "https://postman-echo.com/time/object";
    return from res in BlockingGet(url).Bind(resp => resp.Json<HashMap<string, int>>())
           select res["years"].ToString();
}

补充:错误类型适配

和Rust里需要为错误类型实现From trait来做类型转换一样,如果不同步骤返回的错误类型不匹配,可以在链式调用中加MapErr方法做错误类型转换,适配外层函数的返回值要求。

注:C#语法层面不支持自定义后缀运算符,因此无法实现和Rust?完全一致的符号级语法,但上述两种写法已经完全消除了冗余的手动错误判断,错误传播的简洁度和Rust实现没有本质差异。


内容的提问来源于stack exchange,提问作者Paul-Sebastian Manole

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 20:51:20