在C#代码中匹配F#类型模式

4
假设有一个 F# 的定义:
type Either<'a,'b> = | Left of 'a | Right of 'b

let f (i : int) : Either<int, string> =
    if i > 0
        then Left i
        else Right "nothing"

在C#代码中使用了函数f:

var a = Library.f(5);

如何对数据构造函数进行模式匹配来匹配结果值a?类似于:

/*
(if a is Left x)
    do something with x
(if a is Right y)
    do something with y
*/
3个回答

7
使用F#判别联合在C#中有些不太优雅,因为它们是如何编译的。
我认为最好的方法是定义一些成员(在F#方面),以简化从C#使用这些类型。有多种选择,但我更喜欢定义TryLeftTryRight方法,其行为类似于Int32.TryParse(因此它们应该对于使用您的F# API的C#开发人员来说是熟悉的)。
open System.Runtime.InteropServices

type Either<'a,'b> = 
  | Left of 'a 
  | Right of 'b
  member x.TryLeft([<Out>] a:byref<'a>) =
    match x with Left v -> a <- v; true | _ -> false
  member x.TryRight([<Out>] b:byref<'b>) =
    match x with Right v -> b <- v; true | _ -> false

然后,您可以如下使用C#中的类型:
int a;
string s;
if (v.TryLeft(out a)) Console.WriteLine("Number: {0}", a);
else if (v.TryRight(out s)) Console.WriteLine("String: {0}", s);

通过这样做,您会失去一些F#的安全性,但在没有模式匹配的语言中,这是可以预料的。但好的是,任何熟悉.NET的人都应该能够使用在F#中实现的API。

另一种选择是定义成员Match,它接受Func<'a>Func<'b>委托,并使用左/右case所携带的值调用正确的委托。从函数式的角度来看,这更加优雅,但对于C#调用者来说可能不太明显。


6
我会为每种情况定义一个Match成员,其中包含要执行的委托。在F#中,可以这样做(如果需要,也可以在C#扩展方法中实现类似功能):
type Either<'a,'b> = | Left of 'a | Right of 'b
with
    member this.Match<'t>(ifLeft:System.Func<'a,'t>, ifRight:System.Func<'b,'t>) =
        match this with
        | Left a -> ifLeft.Invoke a
        | Right b -> ifRight.Invoke b

现在你应该能够在C#中做出类似以下的操作:
var result = a.Match(ifLeft: x => x + 1, ifRight: y => 2 * y);

有一些对于该解决方案的限制。例如,您不能这样做 a.Match(ifLeft: x => x + 1, ifRight: _ => return null); - ДМИТРИЙ МАЛИКОВ

1
3.0规范中:

8.5.4 用于其他CLI语言的联合类型编译形式

编译后的联合类型U具有以下特点:

  1. 对于每个空联合情况C,有一个CLI静态getter属性U.C。该属性获取表示每种情况的单例对象。
  2. 对于每个非空联合情况C,有一个CLI嵌套类型U.C。该类型具有每个联合情况的字段Item1、Item2等实例属性,或者如果只有一个字段,则具有单个实例属性Item。 但是,仅有一个情况的编译联合类型没有嵌套类型。相反,联合类型本身扮演情况类型的角色。

  3. 对于每个非空联合情况C,有一个CLI静态方法U.NewC。该方法构造该情况的对象。

  4. 对于每种情况C,有一个CLI实例属性U.IsC。该属性返回该情况的true或false。
  5. 对于每种情况C,有一个CLI实例属性U.Tag。该属性获取或计算与情况相对应的整数标记。
  6. 如果U有多个情况,则有一个CLI嵌套类型U.Tags。U.Tags类型包含每种情况的一个整数字面量,从零开始按递增顺序排列。

  7. 编译后的联合类型具有实现其自动生成接口所需的方法,以及任何用户定义的属性或方法。

这些方法和属性不能直接从F#中使用。但是,这些类型具有面向用户的List.Empty、List.Cons、Option.None和Option.Some属性和/或方法。

由于至少有一个程序集私有构造函数且没有公共构造函数,因此编译后的联合类型不能用作另一种CLI语言的基类型。

如果您无法更改F# API,可以按照上述2和4的方法进行操作,类似于以下内容:
C#
class Program
{
    static void Main(string[] args)
    {
        PrintToConsole("5");
        PrintToConsole("test");
    }

    static void PrintToConsole(string value)
    {
        var result = test.getResult(value);
        if (result.IsIntValue) Console.WriteLine("Is Int: " + ((test.DUForCSharp.IntValue)result).Item);
        else Console.WriteLine("Is Not Int: " + ((test.DUForCSharp.StringValue)result).Item);
    }
}

F#
namespace Library1

module test =

    open System

    type DUForCSharp =
    | IntValue of int
    | StringValue of string

    let getResult x =
        match Int32.TryParse x with
        | true, value -> IntValue(value)
        | _ -> StringValue(x)

这个解决方案很方便,因为它通过为元组中的每个项目创建一个新属性来处理元组DU情况。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接