为什么主流的静态类型语言不支持按返回类型重载函数/方法?我想不出有什么能做到。这似乎并不比支持按参数类型重载更有用或更合理。为什么它不那么受欢迎呢?


当前回答

与其他人所说的相反,根据返回类型进行重载是可能的,并且一些现代语言已经做到了这一点。通常的反对意见是,在代码中

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
}

其他回答

在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打印循环 对反射编程的支持

我认为这是现代c++定义中的一个GAP……为什么?

int func();
double func();

// example 1. → defined
int i = func();

// example 2. → defined
double d = func();

// example 3. → NOT defined. error
void main() 
{
    func();
}

为什么c++编译器不能抛出例子“3”和错误 接受例子“1+2”中的代码??

在这样一种语言中,你将如何解决以下问题:

f(g(x))

如果f有重载void f(int)和void f(字符串)和g有重载int g(int)和字符串g(int)?你需要某种消歧器。

我认为,在需要这个函数的情况下,最好为函数选择一个新名称。

大多数静态语言现在也支持泛型,这将解决您的问题。如前所述,如果没有参数差异,就无法知道调用哪一个。如果你想这样做,使用泛型就可以了。

如果你稍微换个角度来看,这个重载特性并不难管理。考虑以下几点:

public Integer | String f(int choice){
if(choice==1){
return new string();
}else{
return new Integer();
}}

如果一种语言确实返回重载,它将允许参数重载,但不允许重复。 这将解决以下问题:

main (){
f(x)
}

因为只有一个f(int)选项可供选择。