F# - C#方法传递的参数 - 它们是元组还是其他什么?

21

我已经多次阅读到:

使用F#或其他.NET语言生成的程序集(几乎)无法区分。

我当时在.NET 4(beta2)上尝试使用F#和C#互操作。 我创建了一个新解决方案和一个C#项目,并编写了以下类:

public class MyClass {
    public static int Add(int a, int b) { return a + b; }
}

然后,在一个F#项目中引用了C#项目之后,我尝试了以下操作:

MyClsas.Add(4, 5) |> printfn "%d" // prints 9 (no kidding!)
到目前为止,一切都很顺利。然后,我想起了另一个我已经多次读到的句子(或许在不同的书本上):
当从其他 .NET 库传递参数给函数时,你使用的语法是 ".MethodName(parm1, parm2)",也就是说,参数作为 Tuple 传递。
再加上我曾经在这里(指 StackOverflow)读过一些东西(但无法找到链接),有人在一个问题中尝试创建像 [4, 5, 6] 这样的 using(当他的意思是 [4; 5; 6]):
“逗号是 'tuple creating operator',对于其他所有情况,请使用分号。”
然后我将我的类修改为以下内容:
public class MyClass {
    public static int Add(int a, int b) { return a + b; }
    public static int Add(Tuple<int, int> a) { return a.Item1; }
}

现在我尝试在F#上使用它:

MyClass.Add(4, 5) |> printf "%d" // prints ... (keep reading!)
因此,综合以上三个引用,可以得出以下结论:
- F#在看到(4,5)时将创建一个元组。 - 然后它将调用重载的Add(Tuple<int,int>)。 - 所以它会打印4。
让我惊讶的是,它打印了9。这不是很有趣吗?
这里实际上正在发生什么?上述引用和这些实际观察似乎相互矛盾。你能证明F#的“推理”,并可能指向一些MSDN文档吗?
谢谢!
编辑
(添加更多信息(来自Blindy的答案))
如果您执行:
MyClass.Add((4, 5)) |> printfn "%d" // prints 9

F# 调用了 Add(Tuple<int, int>) 重载。

然而,如果你创建了另一个 F# 项目(即另一个程序集),并使用以下代码:

namespace MyFSharpNamespace
type MyFShapClass = class
    static member Add x y = x + y
    end

您可以在C#中像这样使用它

public static void Main(string[] args) {
    MyFSharpNamespace.MyFSharpClass.Add(4, 5);
}

现在,到此为止都很好。但是,当你尝试从F#中使用它(来自另一个项目,另一个程序集)时,你需要执行以下操作:

MyFSharpNamespace.MyFSharpClass.Add 4 5 |> printfn "%d"
如果您将参数传递为(4,5),F#将无法编译,因为Addint -> int -> int,而不是(int * int) -> int
发生了什么?
4个回答

20
当从其他.NET库传递参数到函数时,您使用的语法类似于“MethodName(parm1,parm2)”,也就是说,参数被作为元组传递。
比那更丑陋。请参阅来自language spec的方法重载分辨率描述。
基本上,它所说的是方法调用中的参数实际上不是元组。它是一个语法元组,意思是一些用逗号分隔的内容,但是括号是方法调用语法的一部分,逗号也是。这就是为什么例如o.M(a=1, b=2)不是具有两个布尔值元组的方法调用,而是两个命名参数。
因此,通常,每个逗号分隔的组件都映射到一个不同的参数。这就是为什么Add(1, 2)调用Add(int, int)重载,而Add((1, 2))调用Add(Tuple<int, int>)的原因。这里没有歧义。
然而,对于您的特定情况,触发的一个特殊情况是:
如果没有命名的实际参数,并且M中只有一个候选方法,接受一个非可选参数,则忽略arg的元组形式的分解,并且有一个命名的实际arg,即arg本身。
所以,当你移除除了元组重载之外的所有重载时,括号内的整个内容就被视为一个元组构造函数的调用。但是如果你有两个重载,例如 Add(int)Add(Tuple<int,int>),那么形如 Add(1,2) 的调用将无法解析。

3

我现在没有安装F#,但是我认为

MyClass.Add(4, 5) |> printf "%d"

会打印9,而

MyClass.Add((4, 5)) |> printf "%d"

会打印出4,注意有双括号,内部一对表示元组,外部一对表示函数调用。


是的,我在提问之前进行了测试。但是如果考虑到这3个引号,它仍然很奇怪,不是吗?如果Add是由F#创建的一个类的静态方法,它需要2个参数(并且将从C#中使用.Add(4, 5)),那么F#将无法编译.Add(4, 5) - Bruno Reis
中间的引号看起来有点奇怪,但最后一个双括号的部分很适合。你试过删除 Add(int,int) 声明,看看它是否会解析为元组版本吗? - Blindy
@Blindy:是的,如果我移除 Add(int, int),那么 F# 就会愉快地将 .Add(4, 5) 转发到 C# 的 Add(Tuple<int, int>) - Bruno Reis

3
这只是编译器的魔法。
let add a b = a+b 

add编译成add(a,b),因此很容易从C#中调用。然而,由于IL中的属性,在F#程序中它仍然被视为add a b

在F#中调用C#函数时,可以将其视为只有一个参数 - 一个元组,其元素确定正确的重载。因此,您可以编写:

// MyClass.Add(5,3) = 8
let eight = (5,3) |> MyClass.Add

1
多神奇?“魔法”?还是“更多魔法”?对我来说是“更多魔法”。 - Bruno Reis

0

我不是F#专家,所以我的猜测可能有点偏差,但我认为F#元组的概念与BCL的 System.Tuple 类型没有关系。元组是F#的核心原则并内置于语言中,但是C#,VB.NET和大多数其他.NET语言不支持元组。由于元组在这些语言中可能很有用,因此该库正在获得支持。

我认为,F#元组在内存中表示方式与在C#等语言中传递给方法的参数非常相似。也就是说,它们本质上是其组成部分的值数组。当将此值数组推送到方法调用的堆栈上时,它将具有与从C#调用该方法时将其各个组成部分推送到堆栈上相同的效果。

因此,您的第二个示例创建了一个F#元组,将其推送到堆栈上,然后调用包含元组中包含的类型的 Add 重载。

这只是我的猜测。你可能比我更多地使用了 F#,因此你可能有更深入的见解。你还可以通过查看生成的代码Reflector来获得更多线索。


嗯,非常有趣。我将检查在许多不同的 F# 调用 C# 方法和 C# 调用 F# 方法的组合中生成的中间语言(IL)。谢谢! - Bruno Reis
4
这是错误的。F#元组确实是System.Tuple。其余部分是基于错误前提的不正确的推测。 - Pavel Minaev
@Pavel Minaev:我现在正在进行一些测试,我有一些实验观察结果似乎与你的观点相矛盾。等我测试完成后会再发帖更新。 - Bruno Reis
2
只需执行 printf "%A" ((1, 2).GetType()) 即可检查。当然,这取决于 .NET 版本 - 在 .NET 4 之前没有 System.Tuple,因此在运行版本为 3.5 及以下的 F# 中必须使用自己的类型。但是,由于问题在 C# 代码中提到了 Tuple,因此显然这只能是 .NET 4。 - Pavel Minaev

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