在C#中访问F#区分联合类型数据的最简单方法是什么?

我试图了解C#和F#可以一起玩的程度。 我从F#for Fun&Profit博客中获取了一些代码,该博客执行基本validation,返回一个有区别的联合类型:

type Result = | 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.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.Failure)res).Item; // Result is a string (as returned for Failure) Console.WriteLine(result); } 

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

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

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

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

这只是添加了扩展TryGetSuccessTryGetFailure ,当值匹配case并通过out参数返回被区分的union case的(所有)参数时返回true 。 对于曾经使用过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与特定案例匹配时实际可用的值。

Mauricio Scheffer为C#/ F#interop做了一些优秀的post,并使用了有和没有核心F#库(或Fsharpx库)的技术,以便能够使用这些概念(在F#中简单化) C#。

http://bugsquash.blogspot.co.uk/2012/03/algebraic-data-type-in​​terop-fc.html

http://bugsquash.blogspot.co.uk/2012/01/encoding-algebraic-data-types-in-c.html

这也许有用: 如何在C#中复制F#区别联合类型?

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

 public static Result.Success AsSuccess(this Result res) { return (Result.Success)res; } // And then use it var successData = res.AsSuccess().Item; 

本文包含了很好的见解。 引用:

这种方法的优点是2倍:

  • 消除了在代码中明确命名类型的需要,从而获得了类型推断的优点;
  • 我现在可以用了. 在任何值上让Intellisense帮我找到合适的方法;

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

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

我对结果类型有同样的问题。 我创建了一种新类型的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 } 

现在我可以选择在F#边界通过toResultInterop进行管道toResultInterop ,或者在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); } 

使用C#7.0做一个非常好的方法是使用开关模式匹配,它就像F#match一样:

 var someResult = someFSharpClass.SomeResultReturningMethod() switch (validationResult) { case var checkResult when checkResult.IsOk: HandleValidationSuccess(checkResult.ResultValue); break; case var checkResult when checkResult.IsError: HandleValidationError(checkResult.ErrorValue); break; } 

我正在使用接下来的方法将F#库中的联合交互到C#主机。 这可能会因reflection使用而增加一些执行时间,并且可能需要通过unit testing来检查,以便为每个工会案例处理正确的generics类型。

  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(change); // Handler for case break; case var c when c.IsSecondCommand: var data2 = Extentions.getFromUnion2(change); // Handler for case break; } }