如何在C#中使用Either类型?

2023-12-30

佐兰·霍瓦特 https://www.pluralsight.com/authors/zoran-horvat提议使用Either类型以避免空检查和不要忘记处理问题在执行操作期间。Either在函数式编程中很常见。

为了说明其用法,Zoran 展示了一个与此类似的示例:

void Main()
{
    var result = Operation();
    
    var str = result
        .MapLeft(failure => $"An error has ocurred {failure}")
        .Reduce(resource => resource.Data);
        
    Console.WriteLine(str);
}

Either<Failed, Resource> Operation()
{
    return new Right<Failed, Resource>(new Resource("Success"));
}

class Failed { }

class NotFound : Failed { }

class Resource
{
    public string Data { get; }

    public Resource(string data)
    {
        this.Data = data;
    }
}

public abstract class Either<TLeft, TRight>
{
    public abstract Either<TNewLeft, TRight>
        MapLeft<TNewLeft>(Func<TLeft, TNewLeft> mapping);

    public abstract Either<TLeft, TNewRight>
        MapRight<TNewRight>(Func<TRight, TNewRight> mapping);

    public abstract TLeft Reduce(Func<TRight, TLeft> mapping);
}

public class Left<TLeft, TRight> : Either<TLeft, TRight>
{
    TLeft Value { get; }

    public Left(TLeft value)
    {
        this.Value = value;
    }

    public override Either<TNewLeft, TRight> MapLeft<TNewLeft>(
        Func<TLeft, TNewLeft> mapping) =>
        new Left<TNewLeft, TRight>(mapping(this.Value));

    public override Either<TLeft, TNewRight> MapRight<TNewRight>(
        Func<TRight, TNewRight> mapping) =>
        new Left<TLeft, TNewRight>(this.Value);

    public override TLeft Reduce(Func<TRight, TLeft> mapping) =>
        this.Value;
}

public class Right<TLeft, TRight> : Either<TLeft, TRight>
{
    TRight Value { get; }

    public Right(TRight value)
    {
        this.Value = value;
    }

    public override Either<TNewLeft, TRight> MapLeft<TNewLeft>(
        Func<TLeft, TNewLeft> mapping) =>
        new Right<TNewLeft, TRight>(this.Value);

    public override Either<TLeft, TNewRight> MapRight<TNewRight>(
        Func<TRight, TNewRight> mapping) =>
        new Right<TLeft, TNewRight>(mapping(this.Value));

    public override TLeft Reduce(Func<TRight, TLeft> mapping) =>
        mapping(this.Value);
}

如您所见,Operation回报Either<Failture, Resource>稍后可以用来形成单个值,而不会忘记处理操作失败的情况。请注意,所有的失败都源于Failure类,以防有多个类。

这种方法的问题是消耗该值可能很困难。

我用一个简单的程序展示了复杂性:

void Main()
{
    var result = Evaluate();
    
    Console.WriteLine(result);
}

int Evaluate()
{
    var result = Op1() + Op2();
    
    return result;
}

int Op1()
{
    Throw.ExceptionRandomly("Op1 failed");
    
    return 1;
}


int Op2()
{
    Throw.ExceptionRandomly("Op2 failed");
    
    return 2;
}

class Throw
{
    static Random random = new Random();
    
    public static void ExceptionRandomly(string message)
    {
        if (random.Next(0, 3) == 0)
        {
            throw new InvalidOperationException(message);   
        }       
    }
}

请注意,此示例不使用Either根本无法打字,但作者本人告诉我这是可以做到的。

准确地说,我想将上面的示例转换为评估 to use Either.

换句话说,我想将我的代码转换为使用 Either 并正确使用它

NOTE

拥有一个包含有关最终错误的信息和一个失败类是有意义的Success类包含int value

Extra

这将是非常有趣的Failure可以包含评估期间可能出现的所有问题的摘要。这种行为非常适合向调用者提供有关失败的更多信息。不仅是第一次失败的操作,还有后续的失败。我在语义分析过程中想到了编译器。我不希望阶段在检测到第一个错误时就退出,而是收集所有问题以获得更好的体验。


任一类型基础知识

这两种类型都来自函数式语言,其中异常(理所当然地)被视为副作用,因此不适合传递domain错误。请注意不同类型错误之间的区别:其中一些属于领域,另一些则不属于。例如。空引用异常或索引越界与域无关 - 它们更表明存在缺陷。

Either 被定义为具有两个分支的泛型类型 - 成功和失败:Either<TResult, TError>。它可以以两种形式出现,其中包含一个对象TResult,或者它包含一个对象的地方TError. It cannot同时出现在两种状态中,或者都不出现在任何状态中。因此,如果拥有 Either 实例,它要么包含成功生成的结果,要么包含错误对象。

要么和例外

在异常代表对域很重要的事件的情况下,任一类型都会替换异常。但它并不能取代其他场景中的异常。

关于异常的故事很长,从不需要的副作用到简单的泄漏抽象。顺便说一下,泄漏抽象是使用throws随着时间的推移,关键字在 Java 语言中逐渐消失。

两者之一和副作用

当涉及到副作用时,尤其是与不可变类型结合时,它同样有趣。在任何语言中,无论是函数式语言、OOP 语言还是混合语言(包括 C#、Java、Python),程序员在以下情况下都会采取特定的行为:know某种类型是不可变的。一方面,他们有时倾向于cache结果——完全正确! - 这有助于他们避免以后进行昂贵的调用,例如涉及网络调用甚至数据库的操作。

缓存也可以是微妙的,例如在操作结束之前使用内存中的对象几次。现在,如果不可变类型具有用于域错误结果的单独通道,那么它们将达不到缓存的目的。我们拥有的对象会多次有用,还是应该在每次需要其结果时调用生成函数?这是一个棘手的问题,无知有时会导致代码缺陷。

功能性任一类型实现

这就是任一类型可以提供帮助的地方。我们可以忽略它的内部复杂性,因为它是一个库类型,而只关注它的API。最小任一类型允许:

  • 将结果映射到不同的结果或不同类型的结果 - 对于链接快乐路径转换很有用
  • 处理错误,有效地将失败转化为成功 - 在顶层很有用,例如当将成功和失败表示为 HTTP 响应时
  • 将一个错误转换为另一个错误 - 在传递层边界时很有用(一层中的一组域错误需要转换为另一层的一组域错误)

使用 Either 的最明显的好处是返回它的函数将显式地声明它们返回结果的两个通道。而且,结果将变得稳定,这意味着我们可以在需要时自由缓存它们。另一方面,仅对 Either 类型进行绑定操作有助于避免代码其余部分的污染。一方面,函数永远不会收到 Either。它们将分为对常规对象进行操作的对象(包含在 Either 的 Success 变体中),或对域错误对象进行操作的对象(包含在 Either 的 Failed 变体中)。正是对 Either 的绑定操作选择了将有效调用哪个函数。考虑这个例子:

var response = ReadUser(input) // returns Either<User, Error>
  .Map(FindProduct)            // returns Either<Product, Error>
  .Map(ReadTechnicalDetails)   // returns Either<ProductDetails, Error>
  .Map(View)                   // returns Either<HttpResponse, Error>
  .Handle(ErrorView);          // returns HttpResponse in either case

使用的所有方法的签名都是直接的,并且它们都不会收到 Either 类型。那些方法can检测到错误,允许返回 Either。那些不这样做的,只会返回一个简单的结果。

Either<User, Error> ReadUser(input);
Product FindProduct(User);
Either<ProductDetails, Error> ReadTechnicalDetails(Product);
HttpResponse View(Product);
HttpResponse ErrorView(Product);

所有这些不同的方法都可以绑定到 Either,它将选择是有效地调用它们,还是继续使用它已经包含的内容。基本上,如果调用失败,则映射操作将通过,如果调用成功,则调用该操作。

这就是让我们只编写快乐路径的原则,并在可能的时候处理错误。在大多数情况下,在到达最顶层之前不可能一直处理错误。应用程序通常会通过将错误转换为错误响应来“处理”错误。这种情况正是 Either 类型发挥作用的地方,因为没有其他代码会注意到需要处理错误。

实践中的任一类型

在某些情况下,例如表单验证,需要沿路径收集多个错误。对于这种情况,任一类型都将包含列表,而不仅仅是错误。之前提出的 Either.Map 函数在这种情况下也足够了,只需进行修改即可。常见的Either<Result, Error>.Map(f)不打电话f处于失败状态。但Either<Result, List<Error>>.Map(f),其中 f 返回Either<Result, Error>还是会选择打电话f,仅查看它是否返回错误并将该错误附加到当前列表。

经过这样的分析,很明显,Either 类型代表了一种编程原则,如果你喜欢的话,可以说是一种模式,而不是一种解决方案。如果任何应用程序有一些特定的需求,并且 Either 满足这些需求,那么实现归结为选择适当的绑定,然后应用它们通过 Either 对象来瞄准对象。使用 Either 进行编程变得声明式。呼叫者有责任declare哪些函数适用于正向和负向场景,Either 对象将决定在运行时是否调用函数以及调用哪个函数。

简单的例子

考虑计算算术表达式的问题。通过计算函数对节点进行深入评估,该函数返回Either<Value, ArithmeticError>。错误如上溢、下溢、被零除等 - 典型的域错误。实现计算器就很简单了:定义节点,可以是普通值或运算,然后实现一些Evaluate他们每个人的功能。

// Plain value node
class Value : Node
{
    private int content;
    ...
    Either<int, Error> Evaluate() => this.content;
}

// Division node
class Division : Node
{
    private Node left;
    private Node right;
    ...
    public Either<Value, ArithmeticError> Evaluate() =>
        this.left.Map(value => this.Evaluate(value));

    private Either<Value, ArithmeticError> Evaluate(int leftValue) =>
        this.right.Map(rightValue => rightValue == 0 
            ? Either.Fail(new DivideByZero())
            : Either.Success(new Value(leftValue / rightValue));
}
...
// Consuming code
Node expression = ...;
string report = expression.Evaluate()
    .Map(result => $"Result = {result}")
    .Handle(error => $"ERROR: {error}");
Console.WriteLine(report);

此示例演示了求值如何导致在任意点弹出算术错误,而系统中的所有节点都会忽略它。节点只会评估它们的快乐路径,或者自己生成错误。只有在 UI 上才会第一次考虑错误,当某物需要显示给用户。

复杂的例子

在更复杂的算术评估器中,人们可能希望看到所有错误,而不仅仅是一个错误。该问题需要对至少两个帐户进行自定义:(1) Either 必须包含错误列表,(2) 必须添加新 API 以组合两个 Either 实例。

public Either<int, ArithErrorList> Combine(
    Either<int, ArithErrorList> a,
    Either<int, ArithErrorList> b,
    Func<int, int, Either<int, ArithErrorList>> map) =>
    a.Map(aValue => Combine(aValue, b, map);

private Either<int, ArithErrorList> Combine(
    int aValue,
    Either<int, ArithErrorList> b,
    Func<int, int, Either<int, ArithErrorList>> map) =>
    b.Map(bValue => map(aValue, bValue));  // retains b error list otherwise

private Either<int, ArithErrorList> Combine(
    ArithErrorList aError,
    Either<int, ArithErrorList> b,
    Func<int, int, Either<int, ArithErrorList>> map) =>
    b.MapError(bError => aError.Concat(bError))
        .Map(_ => bError);    // Either concatenate both errors, or just keep b error
...
// Operation implementation
class Divide : Node
{
    private Node left;
    private Node right;
    ...
    public Either<int, AirthErrorList> Evaluate() =>
        helper.Combine(left.Evaluate(), right.Evaluate(), this.Evaluate);

    private Either<int, ArithErrorList> Evaluate(int a, int b) =>
        b == 0 ? (ArithErrorList)new DivideByZero() : a / b;
}

在本次实施中,公众Combinemethod 是入口点,可以连接两个 Either 实例的错误(如果两个都失败),保留一个错误列表(如果只有一个失败),或者调用映射函数(如果两个都成功)。请注意,即使最后一个场景(两个 Either 对象均为 Success)最终也可能产生 Fail 结果!

实施者注意事项

重要的是要注意Combine方法是库代码。一般规则是,必须对使用代码隐藏神秘、复杂的转换。消费者只会看到简单明了的 API。

在这方面,Combine方法可以是附加的扩展方法,例如附加到Either<TResult, List<TError>> or Either<TReuslt, ImmutableList<TError>>类型,以便在出现错误的情况下它变得可用(不引人注目!)can被合并。在所有其他情况下,当错误类型不是列表时,Combine方法将不可用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在C#中使用Either类型? 的相关文章

随机推荐

  • 如何使用 CSS 反转颜色?

    HTML div p inverted color p div CSS div background color f00 p color how to use inverted color here in relation with div
  • Rust 中的严格别名?

    我的理解是 由于所谓的 严格别名规则 以下代码在 C 中具有未定义的行为 include
  • C++ 中条件变量的常见用途是什么?

    我正在尝试了解条件变量 我想知道使用条件变量的常见情况有哪些 一个示例是在阻塞队列中 其中有两个线程访问队列 生产者线程将一个项目推入队列 而消费者线程从队列中弹出一个项目 如果队列为空 则消费者线程将等待 直到生产者线程发送信号 还有哪些
  • 如何在Python中获取方法名称

    我一直在尝试获取此方法中的方法名称 我在堆栈上发现了关于获取名称的类似问题function https stackoverflow com questions 251464 how to get a function name as a s
  • StringBuilder 附加 vs +

    这两行有什么区别 stringBuilder append Text counter more text stringBuilder append Text append counter append more text 假设 counte
  • 我无法从 vscode 访问 github 存储库

    我是 Git 新手 我尝试将其与 VSCode 集成 但出现此错误 git clone https github com vijaypatneedi DS git f VS Code DSA DS Cloning into f VS Cod
  • 如何告诉 PyTorch 不使用 GPU?

    我想在 CPU 和 GPU 之间进行一些时序比较以及一些分析 并且想知道是否有办法区分pytorch questions tagged pytorch不使用GPU而只使用CPU 我意识到我可以安装另一个仅 CPUpytorch questi
  • Django 模板/视图与轮播的问题

    好的 交易是这样的 这就是我目前正在做的事情 看到顶部的两个箭头了吗 这就是图片轮播应该在的地方 然而 这个轮播中没有图片 也就是说 直到我单击 上传 按钮 所以 我的目标是在我点击 上传 按钮之前让图片出现在第一页上 我该如何解决这个问题
  • mongo dbname --eval 'db.collection.find()' 不起作用

    为什么这有效 mongo dbname MongoDB shell version 1 8 3 connecting to nextmuni staging gt db collection find foo bar gt bye 虽然这不
  • --disable-web-security 在 Chrome 中工作吗?

    我正在尝试做一个简单的测试 而不更改任何涉及的服务器端代码跨域 https developer mozilla org en US docs Web HTTP CORS AJAX调用 https www w3schools com xml
  • 如何使用cached_network_image预加载图像?

    我刚刚实现了 Flutter包cached network image我想知道如何预加载图像 以便稍后可以立即使用它们 我从我们的服务器检索稍后将使用的所有图像网址 我已经定义了自定义缓存管理器 getter class LocalCach
  • 如何在 Linux 中针对分段错误生成核心转储?

    我的 Linux 中有一个进程出现分段错误 我怎样才能告诉它在失败时生成核心转储 这取决于您使用的 shell 如果您使用的是 bash 则 ulimit 命令控制与程序执行相关的多个设置 例如是否应该转储核心 如果您输入 ulimit c
  • 仅当属性为 null 时,如何从 lombok 构建器中排除该属性

    我有一个用户模型类 如下所示 JsonSerialize Getter Setter FieldDefaults level AccessLevel PRIVATE Builder public class User Default Str
  • 基于 Python 中较小的数据集生成较大的综合数据集

    我有一个包含 21000 行 数据样本 和 102 列 特征 的数据集 我希望根据当前数据集生成一个更大的合成数据集 例如 100000 行 这样我就可以将其用于机器学习目的 我在这篇文章中提到了 Prashant 的答案https sta
  • 替换“/”之前的任何字符串,PHP

    我想替换 之前的任何字符串 无论字符串长度如何 谢谢 让 一种方式 假设您想更改第一个 之前的字符串 str anystring the rest blah s explode str s 0 new string print r impl
  • 谷歌浏览器本地存储保存在哪里?

    某些网站显示正在使用本地存储 在 Google Chrome 中 CTRL I 资源选项卡 本地存储 例如 StackOverflow 有一个 login prefs 键和我的默认登录提供程序 所以据我所知它是一个 json 对象 这些数据
  • Spring embeddeb 数据库表已存在错误

    我正在尝试使用嵌入式数据库运行 Spring Boot 应用程序 在 bean 初始化期间 由于某种原因 我的表创建脚本被调用两次 第二次调用失败 并出现 表已存在 错误 下面是我的代码 可能是什么问题 Configuration publ
  • MatLab - 将函数应用于矩阵中的每一行

    我有一个矩阵 行数为 4 个整数 列数未指定 取决于文本文件 我想独立地将函数应用于矩阵的每一行 该函数有 4 个输入和 2 个输出 我尝试使用 arrayfun 函数来执行此操作 但每当我调用该函数时 都会收到一条错误消息 输入参数不足
  • 如何在木偶上使用参数化正则表达式(例如/${user}/)进行测试?

    我需要获取用户的主目录 我决定通过解析 getent passwd 字符串来获取它 这是一个自定义事实构建为 内容的串联 etc passwd 并借助正则表达式提取相关信息 当我测试 getent带有固定字符串 adam 提取工程 if g
  • 如何在C#中使用Either类型?

    佐兰 霍瓦特 https www pluralsight com authors zoran horvat提议使用Either类型以避免空检查和不要忘记处理问题在执行操作期间 Either在函数式编程中很常见 为了说明其用法 Zoran 展