小编典典

返回类型的函数重载?

all

为什么更多主流静态类型语言不支持返回类型的函数/方法重载?我想不出有什么办法。它似乎与支持参数类型的重载一样有用或合理。怎么人气这么低?


阅读 113

收藏
2022-05-16

共1个答案

小编典典

与其他人所说的相反,通过返回类型重载 可能的,并且 一些现代语言完成。通常的反对意见是在代码中

int func();
string func();
int main() { func(); }

你不知道哪个func()被调用。这可以通过以下几种方式解决:

  1. 有一个可预测的方法来确定在这种情况下调用哪个函数。
  2. 每当发生这种情况时,它就是一个编译时错误。但是,具有允许程序员消除歧义的语法,例如int main() { (string)func(); }.
  3. 不要有副作用。如果您没有副作用并且从不使用函数的返回值,那么编译器可以避免一开始就调用该函数。

我经常(ab)按返回类型使用重载的两种语言: Perl
Haskell 。让我描述一下他们的工作。

Perl中, 标量列表 上下文(以及其他,但我们假设有两个)之间存在根本区别。Perl 中的每个内置函数都可以根据调用它的
上下文执行不同的操作。 例如,join运算符强制列表上下文(在被连接的事物上),而scalar​​运算符强制标量上下文,所以比较:

print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.

Perl 中的每个运算符都在标量上下文中执行某些操作,在列表上下文中执行某些操作,并且它们可能不同,如图所示。(这不仅适用于像localtime. )
此外,每个运算符都可以 强制 一个上下文,例如加法强制标量上下文。每个条目都记录了这一点。例如,这里是条目的一部分:@a``print @a``print 0+@a __+``man perlfunc``glob EXPR

在列表上下文中,返回一个(可能为空的)值的文件名扩展列表,EXPR例如标准 Unix
shell/bin/csh会执行的操作。在标量上下文中,glob 遍历此类文件名扩展,当列表用尽时返回 undef。

现在,列表和标量上下文之间的关系是什么?嗯,man perlfunc

请记住以下重要规则:没有规则将列表上下文中的表达式行为与其在标量上下文中的行为联系起来,反之亦然。它可能会做两件完全不同的事情。每个运算符和函数决定在标量上下文中返回哪种值最合适。一些运算符返回在列表上下文中返回的列表长度。一些运算符返回列表中的第一个值。一些运算符返回列表中的最后一个值。一些运算符返回成功​​操作的计数。一般来说,他们做你想做的事,除非你想要一致性。

所以不是一个简单的函数,然后你在最后做简单的转换。事实上,我选择这个localtime例子就是出于这个原因。

不仅仅是内置函数有这种行为。任何用户都可以使用 定义这样的函数wantarray,它允许您区分列表、标量和 void 上下文。因此,例如,如果您在
void 上下文中被调用,您可以决定什么都不做。

现在,您可能会抱怨这不是 真正 的返回值重载,因为您只有一个函数,该函数被告知调用它的上下文,然后根据该信息进行操作。然而,这显然是等价的(类似于
Perl 不允许通常的重载,但函数可以只检查它的参数)。此外,它很好地解决了本回复开头提到的模棱两可的情况。Perl
不会抱怨它不知道调用哪个方法。它只是调用它。它所要做的就是找出调用函数的上下文,这总是可能的:

sub func {
    if( not defined wantarray ) {
        print "void\n";
    } elsif( wantarray ) {
        print "list\n";
    } else {
        print "scalar\n";
    }
}

func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"

(注意:当我指的是函数时,我有时可能会说 Perl 运算符。这对于本次讨论并不重要。)

Haskell 采用另一种方法,即没有副作用。它还具有强大的类型系统,因此您可以编写如下代码:

main = do n <- readLn
          print (sqrt n) -- note that this is aligned below the n, if you care to run this

此代码从标准输入读取浮点数,并打印其平方根。但这有什么令人惊讶的呢?嗯,类型readLnreadLn :: Read a => IO a。这意味着对于任何可能的类型Read(正式地,作为类型类的实例的每个Read类型)readLn都可以读取它。Haskell
是如何知道我想读取浮点数的?好吧,sqrtis的类型sqrt :: Floating a => a -> a,这本质上意味着它sqrt只能接受浮点数作为输入,所以 Haskell 推断出我想要的。

当 Haskell 无法推断出我想要什么时会发生什么?嗯,有几种可能性。如果我根本不使用返回值,Haskell 根本不会首先调用该函数。但是,如果我
确实 使用了返回值,那么 Haskell 会抱怨它无法推断类型:

main = do n <- readLn
          print n
-- this program results in a compile-time error "Unresolved top-level overloading"

我可以通过指定我想要的类型来解决歧义:

main = do n <- readLn
          print (n::Int)
-- this compiles (and does what I want)

无论如何,整个讨论的意思是通过返回值重载是可能的并且已经完成,这回答了您的部分问题。

您问题的另一部分是为什么更多的语言不这样做。我会让其他人回答这个问题。但是,有几点评论:主要原因可能是这里混淆的机会确实比按参数类型重载要大。您还可以查看各个语言的基本原理:

Ada:“看起来最简单的重载解析规则是使用所有内容(来自尽可能广泛的上下文的所有信息)来解析重载引用。这条规则可能很简单,但没有帮助。它需要人工阅读扫描任意大的文本,并做出任意复杂的推论(例如上面的(g))。我们认为更好的规则是明确人类阅读器或编译器必须执行的任务,并且使该任务对人类读者来说尽可能自然。”

C (Bjarne Stroustrup 的“C 编程语言”的第 7.4.1
小节):“在重载解析中不考虑返回类型。原因是保持单个运算符或函数调用的解析与上下文无关。考虑:

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
    float fl = sqrt(da);     // call sqrt(double)
    double d = sqrt(da); // call sqrt(double)
    fl = sqrt(fla);            // call sqrt(float)
    d = sqrt(fla);             // call sqrt(float)
}

如果考虑到返回类型,就不再可能sqrt()孤立地查看调用并确定调用了哪个函数。”(注意,为了比较,在 Haskell 中没有 隐式 转换。)

Java(Java 语言规范
9.4.1
):“其中一个继承的方法必须可以返回类型替代所有其他继承的方法,否则会发生编译时错误。”
(是的,我知道这并没有给出理由。我确信 Gosling 在“Java 编程语言”中给出了理由。也许有人有一份副本?我敢打赌它本质上是“最少意外原则”。 )
然而,关于 Java 的有趣事实:JVM 允许
通过返回值重载!例如,它在Scala中使用,并且可以通过 Java
直接
访问,也可以通过玩内部结构来访问。

PS。最后一点,实际上可以通过 C++ 中的返回值通过技巧来重载。见证:

struct func {
    operator string() { return "1";}
    operator int() { return 2; }
};

int main( ) {
    int x    = func(); // calls int version
    string y = func(); // calls string version
    double d = func(); // calls int version
    cout << func() << endl; // calls int version
    func(); // calls neither
}
2022-05-16