在 C# 中访问 F# 可区分联合类型的数据的最简单方法是什么?
本文关键字:数据 最简单 方法 是什么 类型 访问 可区分 | 更新日期: 2023-09-27 18:19:35
我试图了解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
(。有没有更好的方法?
在不支持模式匹配的语言中,使用可区分的联合永远不会那么简单。但是,您的Result<'TSuccess, 'TFailure>
类型非常简单,应该有一些不错的方法来从 C# 使用它(如果类型更复杂,例如表达式树,那么我可能会建议使用 Visitor 模式(。
其他人已经提到了一些选项 - 如何直接访问值以及如何定义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
和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 与特定情况匹配时实际可用的值。
C# 7.0 执行此操作的一个非常好的方法是使用开关模式匹配,它最类似于 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/。
可以使用 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# 将解构方法添加到用于互操作的代码生成,则可以进一步减少语法 C# 语法。 DanyTestResult.Success(var req)
如果 F# DU 是结构样式,则只需在 Tag 属性上进行模式匹配,而无需键入该类型。 {Tag:DanyTestResult.Tag.Success, SuccessValue:var req}
我在结果类型方面遇到了同样的问题。 我创建了一个新类型的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
或在 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);
}
这个怎么样? 它的灵感来自@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# 代码将无法编译。
也许,实现此目的的最简单方法之一是创建一组扩展方法:
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 倍:
- 无需在代码中显式命名类型,从而恢复类型推断的优势;
- 现在,我可以对任何值使用
.
,并让智能感知帮助我找到要使用的适当方法;
这里唯一的缺点是更改的接口需要重构扩展方法。
如果您的项目中有太多这样的类,请考虑使用 ReSharper 等工具,因为为此设置代码生成看起来并不困难。
我正在使用下一个方法将联合从 F# 库互操作到 C# 主机。由于反射的使用,这可能会增加一些执行时间,并且需要检查(可能通过单元测试(以处理每个联合情况的正确泛型类型。
- 在 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<FirstCommand>(change);
// Handler for case
break;
case var c when c.IsSecondCommand:
var data2 = Extentions.getFromUnion2<SecondCommand, int>(change);
// Handler for case
break;
}
}