25

我试图了解 C# 和 F# 可以如何协同工作。我从F# for Fun & Profit 博客中获取了一些代码,该博客执行基本验证,返回一个有区别的联合类型:

type Result<'TSuccess,'TFailure> = 
    | Success of 'TSuccess
    | Failure of 'TFailure

type Request = {name:string; email:string}

let TestValidate input =
    if input.name = "" then Failure "Name must not be blank"
    else Success input

当试图在 C# 中使用它时;我能找到访问成功和失败值的唯一方法(失败是一个字符串,成功是再次请求)是使用大的讨厌的强制转换(这是很多打字,并且需要输入我期望的实际类型在元数据中推断或可用):

var req = new DannyTest.Request("Danny", "fsfs");
var res = FSharpLib.DannyTest.TestValidate(req);

if (res.IsSuccess)
{
    Console.WriteLine("Success");
    var result = ((DannyTest.Result<DannyTest.Request, string>.Success)res).Item;
    // Result is the Request (as returned for Success)
    Console.WriteLine(result.email);
    Console.WriteLine(result.name);
}

if (res.IsFailure)
{
    Console.WriteLine("Failure");
    var result = ((DannyTest.Result<DannyTest.Request, string>.Failure)res).Item;
    // Result is a string (as returned for Failure)
    Console.WriteLine(result);
}

有没有更好的方法来做到这一点?即使我必须手动转换(可能会出现运行时错误),我也希望至少缩短对类型 ( DannyTest.Result<DannyTest.Request, string>.Failure) 的访问。有没有更好的办法?

4

7 回答 7

23

在不支持模式匹配的语言中,使用可区分联合永远不会那么简单。但是,您的Result<'TSuccess, 'TFailure>类型很简单,应该有一些从 C# 中使用它的好方法(如果类型更复杂,比如表达式树,那么我可能会建议使用访问者模式)。

其他人已经提到了一些选项 - 如何直接访问值以及如何定义Match方法(如 Mauricio 的博客文章中所述)。对于简单的 DU,我最喜欢的方法是定义TryGetXyz遵循相同样式的方法Int32.TryParse- 这也保证了 C# 开发人员将熟悉该模式。F# 定义如下所示:

open System.Runtime.InteropServices

type Result<'TSuccess,'TFailure> = 
    | Success of 'TSuccess
    | Failure of 'TFailure

type Result<'TSuccess, 'TFailure> with
  member x.TryGetSuccess([<Out>] success:byref<'TSuccess>) =
    match x with
    | Success value -> success <- value; true
    | _ -> false
  member x.TryGetFailure([<Out>] failure:byref<'TFailure>) =
    match x with
    | Failure value -> failure <- value; true
    | _ -> false

这只是添加扩展TryGetSuccess,并在值与 case 匹配时TryGetFailure返回,并通过参数true返回可区分联合 case 的(所有)out参数。对于曾经使用过的任何人来说,C# 的使用都非常简单TryParse

  int succ;
  string fail;

  if (res.TryGetSuccess(out succ)) {
    Console.WriteLine("Success: {0}", succ);
  }
  else if (res.TryGetFailure(out fail)) {
    Console.WriteLine("Failuere: {0}", fail);
  }

我认为熟悉这种模式是最重要的好处。当您使用 F# 并将其类型公开给 C# 开发人员时,您应该以最直接的方式公开它们(C# 用户不应认为 F# 中定义的类型无论如何都是非标准的)。

此外,这为您提供了合理的保证(当正确使用时),您将仅访问当 DU 匹配特定情况时实际可用的值。

于 2013-06-23T20:01:31.357 回答
8

使用 C# 7.0 执行此操作的一个非常好的方法是使用 switch 模式匹配,它最像 F# 匹配:

var result = someFSharpClass.SomeFSharpResultReturningMethod()

switch (result)
{
    case var checkResult when checkResult.IsOk:
       HandleOk(checkResult.ResultValue);
       break;
    case var checkResult when checkResult.IsError:
       HandleError(checkResult.ErrorValue);
       break;
}

编辑:C# 8.0 即将到来,它带来了 switch 表达式,所以虽然我还没有尝试过,但我希望我们能够做这样的事情:

var returnValue = result switch 
{
    var checkResult when checkResult.IsOk:     => HandleOk(checkResult.ResultValue),
    var checkResult when checkResult.IsError   => HandleError(checkResult.ErrorValue),
    _                                          => throw new UnknownResultException()
};

有关详细信息,请参阅https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/

于 2018-03-23T14:39:11.277 回答
3

这个怎么样?它的灵感来自上述@Mauricio Scheffer 的评论和 FSharpx 中的CSharpCompat代码。

C#:

MyUnion u = CallIntoFSharpCode();
string s = u.Match(
  ifFoo: () => "Foo!",
  ifBar: (b) => $"Bar {b}!");

F#:

  type MyUnion =
    | Foo
    | Bar of int
  with
    member x.Match (ifFoo: System.Func<_>, ifBar: System.Func<_,_>) =
      match x with
      | Foo -> ifFoo.Invoke()
      | Bar b -> ifBar.Invoke(b)

我最喜欢的是它消除了运行时错误的可能性。您不再有一个虚假的默认大小写来编写代码,并且当 F# 类型更改(例如添加一个大小写)时,C# 代码将无法编译。

于 2019-08-20T22:57:59.140 回答
2

可能,实现这一点的最简单方法之一是创建一组扩展方法:

public static Result<Request, string>.Success AsSuccess(this Result<Request, string> res) {
    return (Result<Request, string>.Success)res;
}

// And then use it
var successData = res.AsSuccess().Item;

这篇文章包含了一个很好的见解。引用:

这种方法的优点是 2 倍:

  • 消除了在代码中显式命名类型的需要,从而恢复了类型推断的优势;
  • 我现在可以使用.任何值并让 Intellisense 帮助我找到合适的方法来使用;

这里唯一的缺点是更改的接口需要重构扩展方法。

如果您的项目中有太多此类类,请考虑使用 ReSharper 等工具,因为为此设置代码生成似乎并不难。

于 2013-06-22T21:02:42.640 回答
2

我对 Result 类型也有同样的问题。我创建了一种新的类型ResultInterop<'TSuccess, 'TFailure>和一个辅助方法来水合类型

type ResultInterop<'TSuccess, 'TFailure> = {
    IsSuccess : bool
    Success : 'TSuccess
    Failure : 'TFailure
}

let toResultInterop result =
    match result with
    | Success s -> { IsSuccess=true; Success=s; Failure=Unchecked.defaultof<_> }
    | Failure f -> { IsSuccess=false; Success=Unchecked.defaultof<_>; Failure=f }

现在我可以选择toResultInterop在 F# 边界处通过管道或在 C# 代码中这样做。

在 F# 边界处

module MyFSharpModule =
    let validate request = 
        if request.isValid then
            Success "Woot"
        else
            Failure "request not valid"
        
    let handleUpdateRequest request = 
        request
        |> validate
        |> toResultInterop

public string Get(Request request)
{
    var result = MyFSharpModule.handleUpdateRequest(request);
    if (result.IsSuccess)
        return result.Success;
    else
        throw new Exception(result.Failure);
}

在 Csharp 中的互操作之后

module MyFSharpModule =
    let validate request = 
        if request.isValid then
            Success "Woot"
        else
            Failure "request not valid"
        
    let handleUpdateRequest request = request |> validate

public string Get(Request request)
{
    var response = MyFSharpModule.handleUpdateRequest(request);
    var result = Interop.toResultInterop(response);
    if (result.IsSuccess)
        return result.Success;
    else
        throw new Exception(result.Failure);
}
于 2015-04-30T13:03:39.547 回答
1

您可以使用 C# 类型别名来简化引用 C# 文件中的 DU 类型。

using DanyTestResult = DannyTest.Result<DannyTest.Request, string>;

由于 C# 8.0 及更高版本具有结构模式匹配,因此很容易执行以下操作:

switch (res) {
    case DanyTestResult.Success {Item: var req}:
        Console.WriteLine(req.email);
        Console.WriteLine(req.name);
        break;
    case DanyTestResult.Failure {Item: var msg}:
        Console.WriteLine("Failure");
        Console.WriteLine(msg);
        break;
}

此策略是最简单的,因为它无需修改即可与引用类型 F# DU 一起使用。

如果 F# 将Deconstruct 方法添加到 interop 的 codegen 中,则可以进一步减少语法 C# 语法。DanyTestResult.Success(var req)

如果您的 F# DU 是 struct 样式,您只需要在没有 type 的 Tag 属性上进行模式匹配。{Tag:DanyTestResult.Tag.Success, SuccessValue:var req}

于 2020-09-09T21:11:52.757 回答
0

我正在使用下一个方法将联合从 F# 库互操作到 C# 主机。由于反射的使用,这可能会增加一些执行时间,并且可能需要通过单元测试进行检查,以便为每个联合案例处理正确的泛型类型。

  1. 在 F# 端
type Command = 
     | First of FirstCommand
     | Second of SecondCommand * int

module Extentions =
    let private getFromUnionObj value =
        match value.GetType() with 
        | x when FSharpType.IsUnion x -> 
            let (_, objects) = FSharpValue.GetUnionFields(value, x)
            objects                        
        | _ -> failwithf "Can't parse union"

    let getFromUnion<'r> value =    
        let x = value |> getFromUnionObj
        (x.[0] :?> 'r)

    let getFromUnion2<'r1,'r2> value =    
        let x = value |> getFromUnionObj
        (x.[0] :?> 'r1, x.[1] :? 'r2)
  1. 在 C# 方面
        public static void Handle(Command command)
        {
            switch (command)
            {
                case var c when c.IsFirstCommand:
                    var data = Extentions.getFromUnion<FirstCommand>(change);
                    // Handler for case
                    break;
                case var c when c.IsSecondCommand:
                    var data2 = Extentions.getFromUnion2<SecondCommand, int>(change);
                    // Handler for case
                    break;
            }
        }
于 2018-05-05T10:42:43.470 回答