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


当前回答

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

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
}

其他回答

好的答案!A.Rex的回答尤其详细且有启发性。正如他所指出的,c++在编译lhs = func()时确实会考虑用户提供的类型转换操作符;(func实际上是结构体的名称)。我的解决方法有点不同——不是更好,只是不同(尽管它基于相同的基本思想)。

而我想写的是……

template <typename T> inline T func() { abort(); return T(); }

template <> inline int func()
{ <<special code for int>> }

template <> inline double func()
{ <<special code for double>> }

.. etc, then ..

int x = func(); // ambiguous!
int x = func<int>(); // *also* ambiguous!?  you're just being difficult, g++!

我最终得到了一个使用参数化结构体(T =返回类型)的解决方案:

template <typename T>
struct func
{
    operator T()
    { abort(); return T(); } 
};

// explicit specializations for supported types
// (any code that includes this header can add more!)

template <> inline
func<int>::operator int()
{ <<special code for int>> }

template <> inline
func<double>::operator double()
{ <<special code for double>> }

.. etc, then ..

int x = func<int>(); // this is OK!
double d = func<double>(); // also OK :)

这种解决方案的一个好处是,任何包含这些模板定义的代码都可以为更多类型添加更多专门化。此外,您还可以根据需要对结构进行部分特殊化。例如,如果你想对指针类型进行特殊处理:

template <typename T>
struct func<T*>
{
    operator T*()
    { <<special handling for T*>> } 
};

作为一个负数,你不能写int x = func();用我的解决方案。你必须写int x = func<int>();。您必须显式地说明返回类型是什么,而不是让编译器通过查看类型转换操作符来确定它。我想说的是,“我的”解决方案和a . rex的解决方案都属于解决这个c++困境的帕累托最优方法:)

根据返回元素是标量还是数组,Octave允许不同的结果。

x = min ([1, 3, 0, 2, 0])
   ⇒  x = 0

[x, ix] = min ([1, 3, 0, 2, 0])
   ⇒  x = 0
      ix = 3 (item index)

Cf也是奇异值分解。

如果你想重载具有不同返回类型的方法,只需添加一个具有默认值的虚拟参数来允许重载执行,但不要忘记参数类型应该是不同的,因此重载逻辑工作接下来是delphi上的示例:

type    
    myclass = class
    public
      function Funct1(dummy: string = EmptyStr): String; overload;
      function Funct1(dummy: Integer = -1): Integer; overload;
    end;

像这样使用它

procedure tester;
var yourobject : myclass;
  iValue: integer;
  sValue: string;
begin
  yourobject:= myclass.create;
  iValue:= yourobject.Funct1(); //this will call the func with integer result
  sValue:= yourobject.Funct1(); //this will call the func with string result
end;

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

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
}

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

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

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

main (){
f(x)
}

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