C# 在C语言中访问F#判别联合类型数据的最简单方法是什么?

C# 在C语言中访问F#判别联合类型数据的最简单方法是什么?,c#,.net,f#,functional-programming,C#,.net,F#,Functional Programming,我想知道C和F能在一起玩得多好。我从中获取了一些代码,这些代码执行基本验证,返回一个有区别的联合类型: 类型结果= |"成功" |失败 类型请求={name:string;email:string} 让TestValidate验证输入= 如果input.name=“”,则失败“名称不得为空” 其他成功输入 当你试图用C#来消费它时;我能找到的针对成功和失败(失败是一个字符串,成功又是一个请求)访问值的唯一方法是使用严重的强制转换(这需要大量键入,并且需要键入我期望在元数据中推断或可用的实际类型

我想知道C和F能在一起玩得多好。我从中获取了一些代码,这些代码执行基本验证,返回一个有区别的联合类型:

类型结果=
|"成功"
|失败
类型请求={name:string;email:string}
让TestValidate验证输入=
如果input.name=“”,则失败“名称不得为空”
其他成功输入
当你试图用C#来消费它时;我能找到的针对成功和失败(失败是一个字符串,成功又是一个请求)访问值的唯一方法是使用严重的强制转换(这需要大量键入,并且需要键入我期望在元数据中推断或可用的实际类型):

var req=new DannyTest.Request(“Danny”、“fsfs”);
var res=FSharpLib.DannyTest.TestValidate(req);
如果(解决问题成功)
{
Console.WriteLine(“成功”);
var结果=((DannyTest.result.Success)res)项;
//结果是请求(作为成功返回)
Console.WriteLine(result.email);
Console.WriteLine(result.name);
}
if(res.IsFailure)
{
控制台写入线(“失败”);
var结果=((DannyTest.result.Failure)res)项;
//结果是一个字符串(失败时返回)
控制台写入线(结果);
}

有更好的方法吗?即使我必须手动强制转换(可能出现运行时错误),我也希望至少缩短对类型的访问(
DannyTest.Result.Failure
)。有更好的方法吗?

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

公共静态结果。成功关联(此结果为res){
返回(Result.Success)res;
}
//然后使用它
var successData=res.AsSuccess().Item;
包含良好的洞察力。引述:

这种方法的优点有两个方面:

  • 消除了在代码中显式命名类型的需要,从而恢复了类型推断的优点
  • 我现在可以对任何值使用
    ,并让Intellisense帮助我找到合适的方法来使用
这里唯一的缺点是,更改的接口需要重构扩展方法


如果你的项目中有太多这样的类,考虑使用像RESHARPER这样的工具,因为它看起来不是很难为这个代码建立一个代码生成。

与区分的联盟一起工作在一个不支持模式匹配的语言中是不那么直接的。但是,您的

结果
类型非常简单,应该有一些很好的方法从C#使用它(如果类型更复杂,比如表达式树,那么我可能建议使用访问者模式)

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

open System.Runtime.InteropServices
类型结果=
|"成功"
|失败
使用键入结果
成员x.TryGetSuccess([]成功:byref)=
将x与
|失败值->失败错误
这只需添加扩展名
TryGetSuccess
TryGetFailure
,当值与大小写匹配时,这些扩展名返回
true
,并通过
out
参数返回(所有)已识别联合大小写的参数。对于任何曾经使用过
TryParse
的人来说,C#的使用都非常简单:

  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与特定情况匹配时实际可用的值。

我对结果类型也有同样的问题。我创建了一个新类型的
ResultInterop
,并创建了一个助手方法来创建该类型

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 }

在Csharp中的互操作之后

使用C#7.0实现这一点的一个非常好的方法是使用开关模式匹配,它与F#匹配非常相似:

编辑:C#8.0即将面世,它带来了切换表达式,因此尽管我还没有尝试过,但我希望我们能够做到以下几点:

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

有关更多信息,请参阅。

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

  • 在F侧
  • type命令=
    |一把手
    |第二个命令*int
    模块扩展=
    让private获取unionobj值=
    将value.GetType()与
    |当FSharpType.IsUnion x->
    let(u,objects)=FSharpValue.GetUnionFields(值,x)
    物体
    |->failwithf“无法分析联合”
    让getFromUnion getFromUnionObj
    (x[0]:?>'r1,x[1]:?'r2)
    
  • 在C侧
  • 公共静态无效句柄(命令)
    {
    开关(命令)
    {
    c.IsFirstCommand时的case var c:
    var data=extensions.getFromUnion(变更);
    //案件处理人
    打破
    当c.IsSecondCommand时的情况变量c:
    var data2=扩展。getFromUnion2(更改);
    //案件处理人
    打破
    
    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);
    }
    
    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);
    }
    
    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;
    }
    
    var returnValue = result switch 
    {
        var checkResult when checkResult.IsOk:     => HandleOk(checkResult.ResultValue),
        var checkResult when checkResult.IsError   => HandleError(checkResult.ErrorValue),
        _                                          => throw new UnknownResultException()
    };
    
    MyUnion u = CallIntoFSharpCode();
    string s = u.Match(
      ifFoo: () => "Foo!",
      ifBar: (b) => $"Bar {b}!");
    
      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)
    
    using DanyTestResult = DannyTest.Result<DannyTest.Request, string>;
    
    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;
    }