按返回类型重载函数?
为什么更主流的静态类型语言不支持按返回类型重载函数/方法? 我想不出有什么可以做到的。 它看起来并不比支持参数类型重载有用或合理。 怎么人气这么低?
Why don't more mainstream statically typed languages support function/method overloading by return type? I can't think of any that do. It seems no less useful or reasonable than supporting overload by parameter type. How come it's so much less popular?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(14)
与其他人所说的相反,返回类型重载是是可能的,并且是由一些现代语言完成的。 通常的反对意见是,在这样的代码中,
您无法判断正在调用哪个
func()
。 这可以通过以下几种方式解决:我经常 (ab) 使用重载的两种语言返回类型:Perl 和 Haskell。 让我描述一下他们的工作。
在Perl中,标量和列表上下文(以及其他上下文,但我们假设有两个)之间存在根本区别。 Perl 中的每个内置函数都可以执行不同的操作,具体取决于调用它的上下文。 例如,
join
运算符强制列表上下文(在被连接的事物上),而scalar
运算符强制标量上下文,因此比较:Perl 中的每个运算符都在标量上下文中执行某些操作以及列表上下文中的某些内容,它们可能不同,如图所示。 (这不仅仅适用于像
localtime
这样的随机运算符。如果您在列表上下文中使用数组@a
,它会返回该数组,而在标量上下文中,它会返回例如,print @a
打印出元素,而print 0+@a
则打印大小。)此外,每个运算符都可以强制< /em> 上下文,例如加法+
强制标量上下文。man perlfunc
中的每个条目都记录了这一点。 例如,以下是glob EXPR
条目的一部分:现在,列表和标量上下文之间的关系是什么? 嗯,
man perlfunc
说所以这不是一个简单的问题,只有一个函数,然后在最后进行简单的转换。 事实上,出于这个原因,我选择了
localtime
示例。不仅仅是内置程序具有这种行为。 任何用户都可以使用
wantarray
定义这样的函数,它允许您区分列表、标量和 void 上下文。 因此,例如,如果您在 void 上下文中被调用,您可以决定不执行任何操作。现在,您可能会抱怨这不是true返回值重载,因为您只有一个函数,该函数被告知调用它的上下文,然后对该信息进行操作。 然而,这显然是等价的(类似于 Perl 不允许通常的字面重载,但函数只能检查其参数)。 而且,它很好地解决了本回复开头提到的模棱两可的情况。 Perl 不会抱怨它不知道调用哪个方法;而是抱怨它不知道调用哪个方法。 它只是调用它。 它所要做的就是弄清楚函数被调用的上下文,这总是可能的:(
注意:当我指的是函数时,我有时可能会说 Perl 运算符。这对于本次讨论来说并不重要。)
Haskell 采用另一种方法,即不产生副作用。 它还具有强大的类型系统,因此您可以编写如下代码:
该代码从标准输入读取浮点数,并打印其平方根。 但这有什么令人惊讶的呢? 那么,
readLn
的类型是readLn :: Read a => IO a
。 这意味着对于任何可以Read
的类型(正式来说,是Read
类型类的每个实例),readLn
可以阅读。 Haskell 如何知道我想读取浮点数? 那么,sqrt
的类型是sqrt :: Floating a => 一个-> a
,这本质上意味着sqrt
只能接受浮点数作为输入,因此 Haskell 推断出了我想要的。当 Haskell 无法推断我想要什么时会发生什么? 嗯,有几种可能性。 如果我根本不使用返回值,Haskell 一开始就不会调用该函数。 但是,如果我确实使用返回值,那么 Haskell 会抱怨它无法推断类型:
我可以通过指定我想要的类型来解决歧义:
无论如何,整个讨论的含义是通过返回值进行重载是可能的并且已经完成,这回答了您的部分问题。
你问题的另一部分是为什么更多的语言不这样做。 我会让其他人来回答这个问题。 然而,有几点评论:主要原因可能是这里产生混淆的机会确实比按参数类型重载更大。 您还可以查看各个语言的基本原理:
Ada :“看起来最简单的重载解析规则是使用所有内容 - 来自尽可能广泛的上下文的所有信息 - 来解析重载引用。这个规则可能很简单,但没有帮助。它要求人类读者扫描任意大的文本片段,并做出任意复杂的推论(例如上面的(g)),我们认为更好的规则是明确人类读者或编译器必须执行的任务,并且使该任务如下:对人类读者来说尽可能自然。”
C++(Bjarne Stroustrup 的“C++ 编程语言”第 7.4.1 小节):“在重载解析中不考虑返回类型。原因是保持单个运算符或函数调用的解析与上下文无关。考虑:
如果返回类型是考虑到这一点,将不再可能单独查看
sqrt()
的调用并确定调用了哪个函数。” (请注意,为了比较,在 Haskell 中没有隐式转换。)Java (Java 语言规范 9.4.1): "其中一个继承方法必须是可替换所有其他继承方法的返回类型,或者否则会发生编译时错误。” (是的,我知道这并没有给出一个基本原理。我确信 Gosling 在“Java 编程语言”中给出了基本原理。也许有人有一份副本?我敢打赌这本质上是“最小惊喜原则”。 )然而,关于 Java 的一个有趣的事实是:JVM允许通过返回值进行重载! 例如,它在 Scala< 中使用/a>,并且可以访问 直接通过 Java 以及通过内部操作。
附言。 最后一点,实际上可以通过 C++ 中的返回值通过技巧进行重载。 证人:
Contrary to what others are saying, overloading by return type is possible and is done by some modern languages. The usual objection is that in code like
you can't tell which
func()
is being called. This can be resolved in a few ways:int main() { (string)func(); }
.Two of the languages I regularly (ab)use overload by return type: Perl and Haskell. Let me describe what they do.
In Perl, there is a fundamental distinction between scalar and list context (and others, but we'll pretend there are two). Every built-in function in Perl can do different things depending on the context in which it is called. For example, the
join
operator forces list context (on the thing being joined) while thescalar
operator forces scalar context, so compare: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 exampleprint @a
prints out the elements, whileprint 0+@a
prints the size.) Furthermore, every operator can force a context, e.g. addition+
forces scalar context. Every entry inman perlfunc
documents this. For example, here is part of the entry forglob EXPR
:Now, what's the relation between list and scalar context? Well,
man perlfunc
saysso it's not a simple matter of having a single function, and then you do simple conversion at the end. In fact, I chose the
localtime
example for that reason.It's not just the built-ins that have this behavior. Any user can define such a function using
wantarray
, which allows you to distinguish between list, scalar, and void context. So, for example, you can decide to do nothing if you're being called in 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:
(Note: I may sometimes say Perl operator when I mean function. This is not crucial to this discussion.)
Haskell takes the other approach, namely to not have side effects. It also has a strong type system, and so you can write code like the following:
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
isreadLn :: Read a => IO a
. What this means is that for any type that can beRead
(formally, every type that is an instance of theRead
type class),readLn
can read it. How did Haskell know that I wanted to read a floating point number? Well, the type ofsqrt
issqrt :: Floating a => a -> a
, which essentially means thatsqrt
can only accept floating point numbers as inputs, and so Haskell inferred what I wanted.What happens when Haskell can't infer what I want? Well, there a few possibilities. If I don't use the return value at all, Haskell simply won't call the function in the first place. However, if I do use the return value, then Haskell will complain that it can't infer the type:
I can resolve the ambiguity by specifying the type I want:
Anyway, what this whole discussion means is that overloading by return value is possible and is done, which answers part of your question.
The other part of your question is why more languages don't do it. I'll let others answer that. However, a few comments: the principle reason is probably that the opportunity for confusion is truly greater here than in overloading by argument type. You can also look at rationales from individual languages:
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++ (subsection 7.4.1of Bjarne Stroustrup's "The C++ Programming Language"): "Return types are not considered in overload resolution. The reason is to keep resolution for an individual operator or function call context-independent. Consider:
If the return type were taken into account, it would no longer be possible to look at a call of
sqrt()
in isolation and determine which function was called." (Note, for comparison, that in Haskell there are no implicit conversions.)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. As a final note, it is actually possible to overload by return value in C++ with a trick. Witness:
如果函数由返回类型重载并且您有这两个重载,
那么编译器在看到这样的调用时无法确定要调用这两个函数中的哪一个。
因此,语言设计者通常不允许返回值重载。
然而,某些语言(例如 MSIL)允许按返回类型进行重载。 当然,他们也面临上述困难,但他们有解决方法,您必须查阅他们的文档。
If functions were overloaded by the return type and you had these two overloads
there is no way the compiler could figure out which of those two functions to call upon seeing a call like this
For this reason, language designers often disallow return-value overloading.
Some languages (such as MSIL), however, do allow overloading by return type. They too face the above difficulty of course, but they have workarounds, for which you'll have to consult their documentation.
在这样的语言中,您将如何解决以下问题:
如果
f
重载了void f(int)
和void f(string)
和 < code>g 重载了int g(int)
和string g(int)
? 你需要某种消歧器。我认为,在您可能需要此功能的情况下,为该函数选择一个新名称会更好。
In such a language, how would you resolve the following:
if
f
had overloadsvoid f(int)
andvoid f(string)
andg
had overloadsint g(int)
andstring g(int)
? You would need some kind of disambiguator.I think the situations where you might need this would be better served by choosing a new name for the function.
要窃取 C++ 特定的另一个非常相似的问题的答案(欺骗?):
函数返回类型在重载解析中不起作用,仅仅是因为 Stroustrup(我假设有其他 C++ 架构师的输入)希望重载解析“上下文无关”。 请参阅《C++ 编程语言,第三版》中的 7.4.1 -“重载和返回类型”。
他们希望它仅基于重载的调用方式,而不是结果的使用方式(如果使用的话)。 事实上,许多函数在不使用结果的情况下被调用,或者结果将被用作更大表达式的一部分。 当他们决定这一点时,我确信发挥作用的一个因素是,如果返回类型是解析的一部分,则会有许多对重载函数的调用,这些函数需要使用复杂的规则来解析,或者必须让编译器抛出调用不明确的错误。
而且,上帝知道,C++ 重载解析就其现状来说已经足够复杂了……
To steal a C++ specific answer from another very similar question (dupe?):
Function return types don't come into play in overload resolution simply because Stroustrup (I assume with input from other C++ architects) wanted overload resolution to be 'context independent'. See 7.4.1 - "Overloading and Return Type" from the "C++ Programming Language, Third Edition".
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.
And, Lord knows, C++ overload resolution is complex enough as it stands...
在 haskell 中,即使它没有函数重载,也是可能的。 Haskell 使用类型类。 在程序中你可以看到:
函数重载本身并不那么流行。 我见过的大多数语言是 C++,也许是 java 和/或 C#。 在所有动态语言中,它都是以下内容的简写:
因此,它没有多大意义。 大多数人不感兴趣语言是否可以帮助你在使用它的地方放下一行。
模式匹配有点类似于函数重载,我想有时工作原理也类似。 但它并不常见,因为它只对少数程序有用,并且在大多数语言上实现起来很棘手。
您会看到,还有无数其他更好、更易于实现的功能可以实现到该语言中,包括:
In haskell it's possible even though it doesn't have function overloading. Haskell uses type classes. In a program you could see:
Function overloading itself is not so popular. Mostly languages I've seen with it are C++, perhaps java and/or C#. In all dynamic languages it's a shorthand for:
Therefore there's no much point in it. Most people aren't interested whether language can help you drop a single line per where ever you use it.
Pattern matching is somewhat similar to function overloading, and I guess sometimes work similarly. It's not common though because it is useful only for few programs and is tricky to implement on most of languages.
You see there's infinitely many other better easier-to-implement features to implement into the language, including:
好的答案! A.Rex 的回答尤其非常详细且具有启发性。 正如他指出的,C++ 在编译 lhs = func(); 时确实会考虑用户提供的类型转换运算符(其中 func 实际上是结构体的名称) )。 我的解决方法有点不同 - 不是更好,只是不同(尽管它基于相同的基本思想)。
虽然我想要写...
我最终得到了一个使用参数化结构的解决方案(T = 返回类型):
该解决方案的一个好处是包含这些模板定义的任何代码可以为更多类型添加更多专业化。 您还可以根据需要对结构进行部分特化。 例如,如果您想要对指针类型进行特殊处理:
作为否定,您不能使用我的解决方案编写
int x = func();
。 您必须编写int x = func();
。 您必须明确说明返回类型是什么,而不是要求编译器通过查看类型转换运算符来弄清楚它。 我想说,“我的”解决方案和 A.Rex 的解决方案都属于Good answers! A.Rex's answer in particular is very detailed and instructive. As he points out, C++ does consider user-supplied type-conversion operators when compiling
lhs = func();
(where func is really the name of a struct). My workaround is a bit different - not better, just different (although it's based on the same basic idea).Whereas I had wanted to write...
I ended up with a solution that uses a parameterized struct (with T = the return type):
A benefit of this solution is that any code which includes these template definitions can add more specializations for more types. Also you can do partial specializations of the struct as needed. For example, if you wanted special handling for pointer types:
As a negative, you can't write
int x = func();
with my solution. You have to writeint x = func<int>();
. You have to explicitly say what the return type is, rather than asking the compiler to suss it out by looking at type conversion operators. I would say that "my" solution and A.Rex's both belong in a pareto-optimal front of ways to tackle this C++ dilemma :)如果你想重载具有不同返回类型的方法,只需添加一个带有默认值的虚拟参数即可允许重载执行,但不要忘记参数类型应该不同,以便重载逻辑接下来的工作是delphi 上的一个例子:
像这样使用它
if you want to overload methods with different return types, just add a dummy parameter with default value to allow the overload execution, but don't forget the parameter type should be different so the overload logic works next is an e.g on delphi:
use it like this
如前所述 - 仅返回类型不同的函数的模糊调用会引入歧义。
歧义会导致有缺陷的代码。
必须避免有缺陷的代码。
试图模糊性所带来的复杂性表明这不是一个好的破解方法。
除了智力练习之外 - 为什么不使用带有参考参数的程序。
As already shown - ambiguous calls of a function that differs only by return type introduces ambiguity.
Ambiguity induces defective code.
Defective code must be avoided.
The complexity driven by the attempt to ambiguity shows that this is not a good hack.
Apart from an intellectual exercise - why not use procedures with reference parameters.
如果您以稍微不同的方式看待它,那么这种重载功能并不难管理。 考虑以下情况,
如果一种语言确实返回重载,它将允许参数重载,但不允许重复。
这将解决以下问题:
因为只有一个 f(int choice) 可供选择。
this overloading feature is not hard to manage, if you look at it in a slightly different way. consider the following,
if a language did return overloading it would allow parameter overloading, but not duplications.
this would solve the problem of:
because there is only one f(int choice) to choose from.
在 .NET 中,有时我们使用一个参数来指示通用结果所需的输出,然后进行转换以获得我们期望的结果。
C#
也许这个例子也有帮助:
C++
In .NET, sometimes we use one parameter to indicate the desired output from a generic result, and then made a conversion to get what we expect.
C#
Maybe this example could help too:
C++
根据记录,Octave 根据返回元素是标量还是数组允许不同的结果。
还请参阅 奇异值分解 。
For the record, Octave allows different outcome according to return element being scalar vs array.
Cf also Singular Value Decomposition.
这对于 C++ 来说略有不同; 不知道直接按返回类型算不算重载。 它更像是一种以以下方式起作用的模板专业化。
util.h
util.inl
util.cpp
这个示例并不完全使用返回类型的函数重载解析,但是这个 C++ 非对象类是使用模板专门化通过私有静态方法的返回类型来模拟函数重载解析。
每个
convertToType
函数都调用函数模板stringToValue()
,如果您查看此函数模板的实现细节或算法,它会调用getValue;( param, param )
返回类型T
并将其存储到传递给stringToValue() 的
函数模板作为其参数之一。T*
中除了这样的事情之外; C++ 并没有真正具有通过返回类型进行函数重载解析的机制。 可能还有其他我不知道的构造或机制可以通过返回类型模拟解析。
This one is slightly different for C++; I don't know if it would be considered overloading by return type directly. It is more of a template specialization that acts in the manner of.
util.h
util.inl
util.cpp
This example is not exactly using function overload resolution by return type, however this c++ non object class is using template specialization to simulate function overload resolution by return type with a private static method.
Each of the
convertToType
functions are calling the function templatestringToValue()
and if you look at the implementation details or algorithm of this function template it is callinggetValue<T>( param, param )
and it is returning back a typeT
and storing it into aT*
that is passed into thestringToValue()
function template as one of its parameters.Other than something like this; C++ does not really have a mechanism to have function overloading resolution by return type. There may be other constructs or mechanisms that I'm not aware of that could simulate resolution by return type.
我认为这是现代 C++ 定义中的一个差距……为什么?
为什么 C++ 编译器不能在示例“3”中抛出错误
接受示例“1+2”中的代码?
I think this is a GAP in modern C++ definition… why ?
Why can a C++ compiler can not throw an error in example "3" and
accept the code in example "1+2" ??
大多数静态语言现在也支持泛型,这可以解决您的问题。 如前所述,如果没有参数差异,就无法知道要调用哪一个。 因此,如果您想这样做,只需使用泛型即可。
Most static languages also now support generics, which would solve your problem. As stated before, without having parameter diffs, there is not way to know which one to call. So if you want to do this, just use generics and call it a day.