为什么不允许使用返回类型进行重载?(至少使用常用语言)


9

我不了解所有编程语言,但很明显,通常不支持考虑到方法的返回类型(假设其参数具有相同的数字和类型)而重载方法的可能性。

我的意思是这样的:

 int method1 (int num)
 {

 }
 long method1 (int num)
 {

 }

这并不是编程的大问题,但在某些情况下,我会对此表示欢迎。

显然,如果没有区分所调用方法的方法,这些语言将无法支持它,但是其语法可能像[int] method1(num)或[long] method1(num)这样简单。这样,编译器将知道哪个将被调用。

我不知道编译器是如何工作的,但这看起来并不难,所以我想知道为什么通常不会实现这样的事情。

为什么不支持类似的原因是什么?


举个例子,两个返回类型之间不存在隐式转换(例如,类Foo和)时,您的问题可能会更好Bar

那么,为什么这样的功能会有用呢?
James Youngman

3
@JamesYoungman:例如,要将字符串解析为不同的类型,可以使用int read(String s),float read(String s)等方法。方法的每个重载变体都会为适当的类型执行解析。
Giorgio

顺便说一下,这只是静态类型语言的问题。在动态类型的语言(例如Javascript或Python)中,拥有多种返回类型是很常规的。
Gort Robot

1
@StevenBurnap,嗯,不。使用例如JavaScript,您根本无法执行函数重载。因此,实际上这只是支持函数名称重载的语言的问题。
David Arno

Answers:


19

它使类型检查变得复杂。

当您仅允许基于参数类型的重载,并且仅允许从其初始化程序推导变量类型时,所有类型信息都朝一个方向流动:在语法树上。

var x = f();

given      f   : () -> int  [upward]
given      ()  : ()         [upward]
therefore  f() : int        [upward]
therefore  x   : int        [upward]

当您允许类型信息在两个方向上传播时(例如,根据变量的用法推导变量的类型),您需要一个约束求解器(例如Hindley-Milner类型系统的Algorithm W)来确定类型。

var x = parse("123");
print_int(x);

given      parse        : string -> T  [upward]
given      "123"        : string       [upward]
therefore  parse("123") : ∃T           [upward]
therefore  x            : ∃T           [upward]
given      print_int    : int -> ()    [upward]
therefore  print_int(x) : ()           [upward]
therefore  int -> ()    = ∃T -> ()     [downward]
therefore  ∃T           = int          [downward]
therefore  x            : int          [downward]

在这里,我们需要将type x作为未解析的type变量保留,我们所∃T知道的仅是可解析的。直到以后,当x将其用于具体类型时,我们才具有足够的信息来解决约束并确定约束∃T = int,从而将类型信息从调用表达式向下传播到语法树中x

如果我们无法确定的类型x,那么该代码也将被重载(以便调用者确定类型),或者我们将不得不报告有关歧义的错误。

由此,语言设计师可以得出以下结论:

  • 它增加了实现的复杂性。

  • 在病理情况下,它会使类型检查变慢,呈指数级下降。

  • 很难产生良好的错误消息。

  • 它与现状有很大的不同。

  • 我不想实现它。


1
另外:很难理解,在某些情况下,编译器会做出程序员绝对不希望的选择,从而导致难以发现错误。
gnasher729 '16

1
@ gnasher729:我不同意。我经常使用具有此功能的Haskell,但从来没有被它的重载(即typeclass实例)所困扰。如果有些地方含糊不清,它只会迫使我添加类型注释。而且我仍然选择用我的语言实现完全类型推断,因为它非常有用。这个答案是我扮演魔鬼的拥护者。
乔恩·普迪

4

因为模棱两可。以C#为例:

var foo = method(42);

我们应该使用哪个过载?

好吧,也许那有点不公平。如果我们不使用您的假设语言告诉编译器,编译器将无法确定使用哪种类型。因此,在您的语言中隐式键入是不可能的,并且还有匿名方法和Linq以及它。

这个怎么样?(稍微重新定义了签名以说明要点。)

short method(int num) { ... }
int method(int num) { ... }

....

long foo = method(42);

我们应该使用int过载还是short过载?我们只是不知道,我们必须使用您的[int] method1(num)语法来指定它。老实说,解析和编写代码有点麻烦。

long foo = [int] method(42);

事实是,它与C#中的通用方法惊人地相似。

long foo = method<int>(42);

(C ++和Java具有相似的功能。)

简而言之,语言设计人员选择以不同的方式解决问题,以简化解析并启用更强大的语言功能。

您说您对编译器了解不多。我强烈建议您学习语法和解析器。一旦了解了上下文无关文法是什么,您就会对歧义为何不好的想法有了更好的了解。


关于泛型的好处是,尽管您似乎将short和混为一谈int
罗伯特·哈维

是的@RobertHarvey,您是对的。我正在努力举例说明这一点。如果method返回short或int,并且将类型定义为long,则效果会更好。
RubberDuck

这似乎更好。
RubberDuck

我不赞成您的论点,即您不能使用返回类型重载的语言来进行类型推断,匿名子例程或monad理解。Haskell做到了,而Haskell拥有这三者。它还具有参数多态性。关于long/ int/的观点short更多地是关于子类型化和/或隐式转换的复杂性,而不是关于返回类型重载。毕竟,数字文字在C ++,Java,C♯和许多其他语言的返回类型上都是重载的,这似乎没有问题。您可以简单地制定一条规则:例如,选择最具体/通用的类型。
约尔格W¯¯米塔格

@JörgWMittag我的意思不是说它不可能实现,而只是使事情变得不必要地复杂。
RubberDuck

0

所有语言功能都增加了复杂性,因此它们必须提供足够的好处来证明每种功能不可避免的陷阱,极端情况和用户困惑。对于大多数语言,这种语言根本无法提供足够的好处来证明其合理性。

在大多数语言中,您希望表达式method1(2)具有确定的类型和或多或少可预测的返回值。但是,如果允许重载返回值,则意味着在不考虑表达式周围环境的情况下,无法分辨该表达式的含义。考虑一下当您的unsigned long long foo()方法的实现以结尾时会发生什么return method1(2)?那应该调用long-returning过载还是int-returning过载,或者只是给出编译器错误?

另外,如果您必须通过注释返回类型来帮助编译器,那么您不仅在发明更多语法(这增加了上述所有允许存在该功能的成本),而且您实际上在做与创建相同的事情“正常”语言中的两种名称不同的方法。是[long] method1(2)不是更直观long_method1(2)


另一方面,像Haskell这样具有强大静态类型系统的功能语言确实允许这种行为,因为它们的类型推断足够强大,您几乎不需要在这些语言中注释返回类型。但这是唯一可能的,因为这些语言真正地实现了类型安全,而不是任何传统语言,而且要求所有功能都是纯净的且引用透明的。在大多数OOP语言中这都不是可行的。


2
“以及要求所有函数都是纯净且参照透明的”:这如何使返回类型重载更容易?
Giorgio

@Giorgio不会-Rust不会强制执行函数纯度,它仍然可以返回类型重载(尽管她在Rust中的重载与其他语言非常不同(您只能使用模板重载))
Idan Arye

[long]和[int]部分将有一种方法来显式调用该方法,在大多数情况下,可以从该方法的执行被分配给该变量的类型直接推断出应如何调用该方法。
user2638180 '16

0

提供斯威夫特和工作正常那里。显然,您不能在两面都使用模棱两可的类型,因此必须在左侧知道它。

我在简单的编码/解码API中使用了它

public protocol HierDecoder {
  func dech() throws -> String
  func dech() throws -> Int
  func dech() throws -> Bool

这意味着调用已知参数类型的调用(例如init对象的)非常简单:

    private static let typeCode = "ds"
    static func registerFactory() {
        HierCodableFactories.Register(key:typeCode) {
            (from) -> HierCodable in
            return try tgDrawStyle(strokeColor:from.dech(), fillColor:from.dechOpt(), lineWidth:from.dech(), glowWidth: from.dech())
        }
    }
    func typeKey() -> String { return tgDrawStyle.typeCode }
    func encode(to:HierEncoder) {
        to.ench(strokeColor)
        to.enchOpt(fillColor)
        to.ench(lineWidth)
        to.ench(glowWidth)
    }

如果您一直在密切注意,您会注意到dechOpt上面的电话。我发现很难的方法是,在区分符返回可选值的地方重载相同的函数名太容易出错,因为调用上下文可能会引入期望它是可选值的情况。


-5
int main() {
    auto var1 = method1(1);
}

在那种情况下,编译器可能会:a)因为调用不明确而拒绝调用b)选择第一个/最后一个c)离开类型var1并继续进行类型推断,并且一旦其他表达式确定了var1用于选择的使用类型正确执行。在底线,显示类型推断并非无关紧要的情况很少证明除了该类型推断通常并非无关紧要的一点。
back2dos

1
理由不充分。例如,Rust大量使用了类型推断,在某些情况下(尤其是对于泛型而言)无法告诉您所需的类型。在这种情况下,您只需要显式地指定类型,而不必依赖类型推断。
8bittree '16

1
嗯...这没有证明重载的返回类型。 method1必须声明为返回特定类型。
Gort Robot
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.