为什么更多主流的静态类型语言不支持返回类型的函数/方法重载?我想不出那样做.通过参数类型支持过载似乎没有那么有用或合理.怎么这么不受欢迎呢?
相反,别人说的话,返回类型超载是可能的,是一些现代语言来完成.通常的反对意见是在代码中
int func();
string func();
int main() { func(); }
你不知道func()
被叫哪个.这可以通过以下几种方式解决:
有一个可预测的方法来确定在这种情况下调用哪个函数.
每当出现这种情况时,都是编译时错误.但是,有一个允许程序员消除歧义的语法,例如int main() { (string)func(); }
.
没有副作用.如果你没有副作用并且你从不使用函数的返回值,那么编译器可以避免首先调用函数.
我经常使用两种语言(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
此代码从标准输入读取浮点数,并打印其平方根.但令人惊讶的是这个?嗯,类型readLn
是readLn :: Read a => IO a
.这意味着对于任何类型Read
(正式地,每个类型都是Read
类型类的实例),readLn
都可以读取它.Haskell怎么知道我想读一个浮点数?嗯,sqrt
is 的类型sqrt :: Floating a => a -> a
,这实际上意味着sqrt
只能接受浮点数作为输入,因此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
}
如果函数被返回类型重载并且您有这两个重载
int func(); string func();
在看到这样的调用时,编译器无法确定调用这两个函数中的哪一个
void main() { func(); }
出于这个原因,语言设计者经常不允许返回值重载.
有些语言(如MSIL),然而,也允许返回类型超载.他们当然也面临上述困难,但他们有解决方法,你必须查阅他们的文档.
在这种语言中,您将如何解决以下问题:
f(g(x))
如果f
有超载void f(int)
,void f(string)
并g
有超载int g(int)
和string g(int)
?你需要某种消除歧义.
我认为通过为函数选择新名称可以更好地满足您可能需要的情况.
从另一个非常相似的问题(欺骗?)中窃取C++特定的答案:
函数返回类型不会在重载解析中发挥作用,因为Stroustrup(我假设其他C++架构师的输入)希望重载解析为"上下文无关".请参见"C++编程语言,第三版"中的7.4.1 - "重载和返回类型".
原因是保持单个运算符或函数调用的分辨率与上下文无关.
他们希望它仅基于如何调用重载 - 而不是如何使用结果(如果它被使用的话).实际上,在不使用结果的情况下调用许多函数,或者结果将被用作更大表达式的一部分.当我们确定这一点时,我肯定会发挥作用的一个因素是,如果返回类型是解决方案的一部分,那么将会有许多调用重载函数需要使用复杂规则解决或者必须让编译器抛出呼叫不明确的错误.
而且,主知道,C++重载分辨率足够复杂,因为它代表......
在haskell中,即使它没有函数重载也是可能的.Haskell使用类型类.在程序中,您可以看到:
class Example a where example :: Integer -> a instance Example Integer where -- example is now implemented for Integer example :: Integer -> Integer example i = i * 10
函数重载本身并不那么受欢迎.我见过的大部分语言都是C++,也许是java和/或C#.在所有动态语言中,它都是以下的简写:
define example:i ?i type route: Integer = [?i & 0xff] String = [?i upper] def example(i): if isinstance(i, int): return i & 0xff elif isinstance(i, str): return i.upper()
因此,它没有多大意义.大多数人都不感兴趣,语言是否可以帮助您在任何使用它的地方放弃一行.
模式匹配有点类似于函数重载,我猜有时候工作方式类似.这并不常见,因为它仅适用于少数几个程序,并且在大多数语言上都很难实现.
您会看到有很多其他更易于实现的功能可以在语言中实现,包括:
动态打字
对列表,字典和unicode字符串的内部支持
优化(JIT,类型推理,编译)
集成部署工具
图书馆支持
社区支持和聚会场所
丰富的标准库
语法很好
读取eval打印循环
支持反思编程