在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
这只是添加了扩展TryGetSuccess
和TryGetFailure
,当值匹配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-interop-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类型。
- 在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)
- 在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; } }
- 通过TCP / .NET SSLStream发送文件很慢/不起作用
- 如何删除存在于某些文本中的任何UTF-8 BOM,而不是在某些文本的开头
- 在方法签名中使用async关键字在Web Api端点中返回Task
- 如何使用WebResponse下载.wmv文件
- ASP.NET Web窗体和标识:将IdentityModels.cs移动到另一个项目
- 向MailMessage添加附件时流关闭错误
- 使用DocumentPaginator进行打印时如何打印预览?
- 使用Logging框架有什么意义?
- Environment.CurrentDirectory和Directory.GetCurrentDirectory有什么区别?