与其他人所说的相反,按返回类型重载is可能和is由一些现代语言完成。通常的反对意见是在类似的代码中
int func();
string func();
int main() { func(); }
你不知道哪个func()
正在被呼叫。这可以通过以下几种方式解决:
- 有一个可预测的方法来确定在这种情况下调用哪个函数。
- 每当出现这种情况时,就会出现编译时错误。然而,有一个允许程序员消除歧义的语法,例如
int main() { (string)func(); }
.
- 不要有副作用。如果您没有副作用并且从不使用函数的返回值,那么编译器可以避免首先调用该函数。
我经常使用的两种语言(ab)按返回类型使用重载:Perl and Haskell。让我描述一下他们的工作。
In Perl,之间有一个根本的区别scalar and list上下文(以及其他,但我们假设有两个)。 Perl 中的每个内置函数都可以根据不同的情况执行不同的操作context它被称为。例如,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
打印尺寸。)此外,每个操作员都可以force上下文,例如添加+
强制标量上下文。中的每个条目man perlfunc
记录了这一点。例如,这是条目的一部分glob EXPR
:
在列表上下文中,返回一个(可能是
空)文件名扩展列表
的价值EXPR
比如标准
Unix外壳/bin/csh
会做。在
标量上下文,glob 迭代
这样的文件名扩展,返回
当列表耗尽时 undef。
现在,列表和标量上下文之间的关系是什么?出色地,man perlfunc
says
请记住以下重要规则:
没有相关的规则
列表中表达式的行为
其标量行为的上下文
上下文,反之亦然。它可能会做
两个完全不同的事情。每个
运算符和函数决定哪个
这是最有价值的
适合以标量返回
语境。一些运算符返回
列表的长度
在列表上下文中返回。一些
运算符返回第一个值
列表。一些运算符返回
列表中的最后一个值。一些
运算符返回成功的计数
运营。一般来说,他们做什么
你想要的,除非你想要一致性。
所以这不是一个简单的问题,只有一个函数,然后在最后进行简单的转换。事实上,我选择的是localtime
出于这个原因的例子。
不仅仅是内置程序具有这种行为。任何用户都可以使用定义这样的函数wantarray
,它允许您区分列表、标量和 void 上下文。因此,例如,如果您在 void 上下文中被调用,您可以决定不执行任何操作。
现在,你可能会抱怨这不是true按返回值重载,因为您只有一个函数,该函数被告知调用它的上下文,然后对该信息进行操作。然而,这显然是等价的(类似于 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
此代码从标准输入读取浮点数,并打印其平方根。但这有什么令人惊讶的呢?嗯,类型readLn
is readLn :: Read a => IO a
。这意味着对于任何可以Read
(正式来说,每个类型都是Read
类型类),readLn
可以阅读。 Haskell 如何知道我想读取浮点数?嗯,类型sqrt
is sqrt :: Floating a => a -> a
,这本质上意味着sqrt
只能接受浮点数作为输入,因此 Haskell 推断出了我想要的。
当 Haskell 无法推断我想要什么时会发生什么?嗯,有几种可能性。如果我根本不使用返回值,Haskell 一开始就不会调用该函数。然而,如果我do使用返回值,那么 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 中没有implicit转换。)
Java (Java 语言规范 9.4.1): “其中一个继承的方法必须是每个其他继承方法的返回类型可替换的,否则会发生编译时错误。” (是的,我知道这并没有给出一个基本原理。我确信这个基本原理是 Gosling 在《Java 编程语言》中给出的。也许有人有一份副本?我敢打赌这本质上是“最小惊喜原则”。 )然而,关于 Java 的一个有趣事实:JVMallows按返回值重载!例如,这用于Scala,并且可以访问直接通过Java以及通过玩弄内部结构。
附言。最后一点,实际上可以通过 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
}