F#方法重载决策不如C#聪明吗?

7
说,我有
member this.Test (x: 'a) = printfn "generic"
                           1
member this.Test (x: Object) = printfn "non generic"
                               2

如果我在C#中调用它

var a = test.Test(3);         // calls generic version
var b = test.Test((object)3); // calls non generic version
var c = test.Test<object>(3); // calls generic version

然而,在F#中

let d = test.Test(3);  // calls non generic version
let e = test.Test<int>(3); // calls generic version

所以我必须添加类型注释才能获得正确的重载方法。是这样吗?如果是这样,那么为什么F#不会自动解析正确,因为参数类型已经被推断出来了呢?(F#的重载解析顺序是什么?总是优先考虑Object而不是它继承的类吗?)
如果一个方法既有重载,其中一个以Object类型作为参数,另一个是泛型,并且两者都返回相同的类型,那么这就有点危险了(就像在这个例子中或在单元测试中的Assert.AreEqual一样),因为很可能我们会不知不觉地得到错误的重载(不会有任何编译器错误)。这会成为一个问题吗?
更新:
有人能解释一下:
- 为什么F#将Assert.AreEqual(3, 5)解析为Assert.AreEqual(Object a, Object b),而不是Assert.AreEqual(T a, T b)? - 但是F#将Array.BinarySearch([|2;3|], 2)解析为BinarySearch(T[]array, T value),而不是BinarySearch(Array array, Object value)?

你似乎漏掉了一个问号..在某个地方。 - ildjarn
@ildjarn 好的,我会添加一些。 - colinfang
你尝试过 test.Test<_>(3) 吗? - svick
1个回答

9

F#方法重载决策不如C#聪明吗?

我认为这是不正确的。方法重载使类型推断变得更加困难。F#做出了合理的权衡,使方法重载可用,并使类型推断尽可能强大。

当您将一个值传递给函数/方法时,F#编译器会自动将其向上转换为适当的类型。这在许多情况下很方便,但有时也会令人困惑。

在您的示例中,3被向上转换为obj类型。两种方法都是适用的,但选择了更简单的(非泛型)方法。

第14.4节 方法应用解析 在规范中清楚地指定了重载规则:

1) 倾向于使用不限制用户引入的泛型类型注释等于另一种类型的候选者。

2) 倾向于不使用ParamArray转换的候选者。如果两个候选者都使用具有类型pty1和pty2的ParamArray转换,并且pty1可以包含pty2,则更喜欢第二个;也就是说,使用具有更精确类型的候选者。

3) 倾向于没有ImplicitlyReturnedFormalArgs的候选者。

4) 倾向于没有ImplicitlySuppliedFormalArgs的候选者。

5) 如果两个候选者具有未命名的实际参数类型ty11 ... ty1n和ty21 ... ty2n,并且每个ty1i都

a. 包含ty2i,或

b. ty2i是System.Func类型,ty1i是其他委托类型,则更喜欢第二个候选者。也就是说,更喜欢具有更具体实际参数类型的任何候选者,并且将任何System.Func类型视为比任何其他委托类型更具体。

6) 倾向于不是扩展成员的候选者而不是扩展成员的候选者。

7) 在两个扩展成员之间进行选择时,请优先考虑最近使用open的成员。

8) 倾向于不是通用的候选者而不是通用的候选者-也就是说,更喜欢具有空ActualArgTypes的候选者。

我认为用户有责任创建明确的重载方法。您可以始终查看推断类型以确定是否正确执行它们。例如,一个没有歧义的修改版本如下:
type T() =
    member this.Test (x: 'a) = printfn "generic"; 1
    member this.Test (x: System.ValueType) = printfn "non-generic"; 2

let t = T()
let d = t.Test(3)  // calls non-generic version
let e = t.Test(test) // call generic version

更新:

这涉及到一个核心概念,协变性。F# 不支持数组、列表、函数等的协变性。确保类型安全通常是一件好事情(参见此示例)。

因此,很容易解释为什么Array.BinarySearch([|2;3|], 2)被解析为BinarySearch<T>(T[] array, T value)。以下是另一个关于函数参数的示例:

T.Test((fun () -> 2), 2)

被解析为

T.Test(f: unit -> 'a, v: 'a)

但不包括

T.Test(f: unit -> obj, v: obj)

有相当多的BCL .Net方法存在歧义重载(它们在C#中可以很好地解决)。我曾认为F#的解析与C#相同,以保持处理.Net方法的一致性。 - colinfang
2
方法重载使类型推断变得更加困难。考虑到 F# 是一种函数式编程语言,我认为该语言已经做出了合理的(实用主义的)选择。 - pad
请帮忙看一下我的更新问题,为什么 F# 在 Assert.AreEqualArray.BinarySearch 中的解析方式不同? - colinfang

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