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

