为什么主流的静态类型语言不支持按返回类型重载函数/方法?我想不出有什么能做到。这似乎并不比支持按参数类型重载更有用或更合理。为什么它不那么受欢迎呢?
当前回答
如果你稍微换个角度来看,这个重载特性并不难管理。考虑以下几点:
public Integer | String f(int choice){
if(choice==1){
return new string();
}else{
return new Integer();
}}
如果一种语言确实返回重载,它将允许参数重载,但不允许重复。 这将解决以下问题:
main (){
f(x)
}
因为只有一个f(int)选项可供选择。
其他回答
如前所述,对仅因返回类型不同而不同的函数的模糊调用会引入模糊。 模糊性会导致有缺陷的代码。 必须避免有缺陷的代码。
试图模糊化所带来的复杂性表明这不是一个好的hack。 除了智力练习之外,为什么不使用带有引用参数的过程呢?
procedure(reference string){};
procedure(reference int){};
string blah;
procedure(blah)
在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打印循环 对反射编程的支持
在这样一种语言中,你将如何解决以下问题:
f(g(x))
如果f有重载void f(int)和void f(字符串)和g有重载int g(int)和字符串g(int)?你需要某种消歧器。
我认为,在需要这个函数的情况下,最好为函数选择一个新名称。
与其他人所说的相反,根据返回类型进行重载是可能的,并且一些现代语言已经做到了这一点。通常的反对意见是,在代码中
int func();
string func();
int main() { func(); }
你不能告诉哪个func()被调用。这个问题可以通过以下几种方式解决:
有一个可预测的方法来确定在这种情况下调用哪个函数。 无论何时出现这种情况,都是编译时错误。但是,要有允许程序员消除歧义的语法,例如int main() {(string)func();}。 没有副作用。如果你没有副作用,并且你从不使用函数的返回值,那么编译器可以避免在第一个地方调用函数。
我经常通过返回类型使用重载的两种语言:Perl和Haskell。让我来描述一下他们的工作。
在Perl中,标量上下文和列表上下文(以及其他上下文,但我们假设有两个)之间有基本的区别。Perl中的每个内置函数都可以根据调用它的上下文做不同的事情。例如,连接操作符强制使用列表上下文(在被连接的对象上),而标量操作符强制使用标量上下文,因此比较:
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.
Every operator in Perl does something in scalar context and something in list context, and they may be different, as illustrated. (This isn't just for random operators like localtime. If you use an array @a in list context, it returns the array, while in scalar context, it returns the number of elements. So for example print @a prints out the elements, while print 0+@a prints the size.) Furthermore, every operator can force a context, e.g. addition + forces scalar context. Every entry in man perlfunc documents this. For example, here is part of the entry for glob EXPR:
在列表上下文中,返回一个(可能 空)上的文件名扩展列表 EXPR值如标准 Unix shell /bin/csh可以。在 标量上下文,glob迭代遍历 这样的文件名扩展,返回 当列表耗尽时Undef。
现在,列表和标量上下文之间的关系是什么?嗯,perlfunc说
Remember the following important rule: There is no rule that relates the behavior of an expression in list context to its behavior in scalar context, or vice versa. It might do two totally different things. Each operator and function decides which sort of value it would be most appropriate to return in scalar context. Some operators return the length of the list that would have been returned in list context. Some operators return the first value in the list. Some operators return the last value in the list. Some operators return a count of successful operations. In general, they do what you want, unless you want consistency.
所以这不是一个简单的函数,然后在最后做简单的转换。事实上,出于这个原因,我选择了localtime示例。
并不是只有内置程序才有这种行为。任何用户都可以使用wantarray定义这样的函数,这允许您区分列表、标量和void上下文。例如,你可以决定什么都不做如果你在void context中被调用。
Now, you may complain that this isn't true overloading by return value because you only have one function, which is told the context it's called in and then acts on that information. However, this is clearly equivalent (and analogous to how Perl doesn't allow usual overloading literally, but a function can just examine its arguments). Moreover, it nicely resolves the ambiguous situation mentioned at the beginning of this response. Perl doesn't complain that it doesn't know which method to call; it just calls it. All it has to do is figure out what context the function was called in, which is always possible:
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
This code reads a floating point number from standard input, and prints its square root. But what is surprising about this? Well, the type of readLn is readLn :: Read a => IO a. What this means is that for any type that can be Read (formally, every type that is an instance of the Read type class), readLn can read it. How did Haskell know that I wanted to read a floating point number? Well, the type of sqrt is sqrt :: Floating a => a -> a, which essentially means that sqrt can only accept floating point numbers as inputs, and so Haskell inferred what I wanted.
要是哈斯克尔猜不出我想要什么怎么办?有几种可能性。如果我根本不使用返回值,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: "It might appear that the simplest overload resolution rule is to use everything - all information from as wide a context as possible - to resolve the overloaded reference. This rule may be simple, but it is not helpful. It requires the human reader to scan arbitrarily large pieces of text, and to make arbitrarily complex inferences (such as (g) above). We believe that a better rule is one that makes explicit the task a human reader or a compiler must perform, and that makes this task as natural for the human reader as possible."
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 Language Specification 9.4.1): "One of the inherited methods must be return-type-substitutable for every other inherited method, or else a compile-time error occurs." (Yes, I know this doesn't give a rationale. I'm sure the rationale is given by Gosling in "the Java Programming Language". Maybe someone has a copy? I bet it's the "principle of least surprise" in essence.) However, fun fact about Java: the JVM allows overloading by return value! This is used, for example, in Scala, and can be accessed directly through Java as well by playing around with internals.
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
}
从另一个非常相似的问题(dupe?)中窃取一个c++特定的答案:
函数返回类型不会在重载解析中发挥作用,因为Stroustrup(我假设来自其他c++架构师的输入)希望重载解析是“上下文独立的”。参见“c++编程语言,第三版”中的“重载和返回类型”。
原因是为了保持独立于上下文的单个操作符或函数调用的解析。
They wanted it to be based only on how the overload was called - not how the result was used (if it was used at all). Indeed, many functions are called without using the result or the result would be used as part of a larger expression. One factor that I'm sure came into play when they decided this was that if the return type was part of the resolution there would be many calls to overloaded functions that would need to be resolved with complex rules or would have to have the compiler throw an error that the call was ambiguous.
而且,上帝知道,c++的重载解析已经足够复杂了……