为什么更多主流静态类型语言不支持返回类型的函数/方法重载?我想不出有什么办法。它似乎与支持参数类型的重载一样有用或合理。怎么人气这么低?
14 回答
与其他人所说的相反,通过返回类型重载是可能的,并且由一些现代语言完成。通常的反对意见是在代码中
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 根本不会首先调用该函数。但是,如果我确实使用了返回值,那么 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))
iff
有重载void f(int)
and有重载void f(string)
and ? 你需要某种消歧器。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、类型推断、编译)
- 集成部署工具
- 图书馆支持
- 社区支持和聚会场所
- 丰富的标准库
- 良好的语法
- 读取评估打印循环
- 支持反射式编程
好答案!特别是 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++ 困境的帕累托最优前沿:)
如果您想重载具有不同返回类型的方法,只需添加一个具有默认值的虚拟参数以允许重载执行,但不要忘记参数类型应该不同,因此重载逻辑接下来起作用的是例如在 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;
如前所述 - 仅通过返回类型不同的函数的模棱两可调用会引入模棱两可。歧义会导致有缺陷的代码。必须避免有缺陷的代码。
试图模棱两可导致的复杂性表明这不是一个好的hack。除了智力练习之外 - 为什么不使用带有参考参数的程序。
procedure(reference string){};
procedure(reference int){};
string blah;
procedure(blah)
如果您以稍微不同的方式看待它,那么这个重载功能并不难管理。考虑以下,
public Integer | String f(int choice){
if(choice==1){
return new string();
}else{
return new Integer();
}}
如果一种语言确实返回重载,它将允许参数重载,但不允许重复。这将解决以下问题:
main (){
f(x)
}
因为只有一个 f(int choice) 可供选择。
在 .NET 中,有时我们使用一个参数来指示通用结果的所需输出,然后进行转换以获得我们期望的结果。
C#
public enum FooReturnType{
IntType,
StringType,
WeaType
}
class Wea {
public override string ToString()
{
return "Wea class";
}
}
public static object Foo(FooReturnType type){
object result = null;
if (type == FooReturnType.IntType)
{
/*Int related actions*/
result = 1;
}
else if (type == FooReturnType.StringType)
{
/*String related actions*/
result = "Some important text";
}
else if (type == FooReturnType.WeaType)
{
/*Wea related actions*/
result = new Wea();
}
return result;
}
static void Main(string[] args)
{
Console.WriteLine("Expecting Int from Foo: " + Foo(FooReturnType.IntType));
Console.WriteLine("Expecting String from Foo: " + Foo(FooReturnType.StringType));
Console.WriteLine("Expecting Wea from Foo: " + Foo(FooReturnType.WeaType));
Console.Read();
}
也许这个例子也可以帮助:
C++
#include <iostream>
enum class FooReturnType{ //Only C++11
IntType,
StringType,
WeaType
}_FooReturnType;
class Wea{
public:
const char* ToString(){
return "Wea class";
}
};
void* Foo(FooReturnType type){
void* result = 0;
if (type == FooReturnType::IntType) //Only C++11
{
/*Int related actions*/
result = (void*)1;
}
else if (type == FooReturnType::StringType) //Only C++11
{
/*String related actions*/
result = (void*)"Some important text";
}
else if (type == FooReturnType::WeaType) //Only C++11
{
/*Wea related actions*/
result = (void*)new Wea();
}
return result;
}
int main(int argc, char* argv[])
{
int intReturn = (int)Foo(FooReturnType::IntType);
const char* stringReturn = (const char*)Foo(FooReturnType::StringType);
Wea *someWea = static_cast<Wea*>(Foo(FooReturnType::WeaType));
std::cout << "Expecting Int from Foo: " << intReturn << std::endl;
std::cout << "Expecting String from Foo: " << stringReturn << std::endl;
std::cout << "Expecting Wea from Foo: " << someWea->ToString() << std::endl;
delete someWea; // Don't leak oil!
return 0;
}
作为记录,Octave允许根据返回元素是标量还是数组而产生不同的结果。
x = min ([1, 3, 0, 2, 0])
⇒ x = 0
[x, ix] = min ([1, 3, 0, 2, 0])
⇒ x = 0
ix = 3 (item index)
参见奇异值分解。
这与 C++ 略有不同;我不知道它是否会被视为直接通过返回类型重载。它更像是一种模板专业化,其行为方式如下。
实用程序.h
#ifndef UTIL_H
#define UTIL_H
#include <string>
#include <sstream>
#include <algorithm>
class util {
public:
static int convertToInt( const std::string& str );
static unsigned convertToUnsigned( const std::string& str );
static float convertToFloat( const std::string& str );
static double convertToDouble( const std::string& str );
private:
util();
util( const util& c );
util& operator=( const util& c );
template<typename T>
static bool stringToValue( const std::string& str, T* pVal, unsigned numValues );
template<typename T>
static T getValue( const std::string& str, std::size_t& remainder );
};
#include "util.inl"
#endif UTIL_H
实用程序.inl
template<typename T>
static bool util::stringToValue( const std::string& str, T* pValue, unsigned numValues ) {
int numCommas = std::count(str.begin(), str.end(), ',');
if (numCommas != numValues - 1) {
return false;
}
std::size_t remainder;
pValue[0] = getValue<T>(str, remainder);
if (numValues == 1) {
if (str.size() != remainder) {
return false;
}
}
else {
std::size_t offset = remainder;
if (str.at(offset) != ',') {
return false;
}
unsigned lastIdx = numValues - 1;
for (unsigned u = 1; u < numValues; ++u) {
pValue[u] = getValue<T>(str.substr(++offset), remainder);
offset += remainder;
if ((u < lastIdx && str.at(offset) != ',') ||
(u == lastIdx && offset != str.size()))
{
return false;
}
}
}
return true;
}
实用程序.cpp
#include "util.h"
template<>
int util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stoi( str, &remainder );
}
template<>
unsigned util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stoul( str, &remainder );
}
template<>
float util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stof( str, &remainder );
}
template<>
double util::getValue( const std::string& str, std::size_t& remainder ) {
return std::stod( str, &remainder );
}
int util::convertToInt( const std::string& str ) {
int i = 0;
if ( !stringToValue( str, &i, 1 ) ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to int";
throw strStream.str();
}
return i;
}
unsigned util::convertToUnsigned( const std::string& str ) {
unsigned u = 0;
if ( !stringToValue( str, &u, 1 ) ) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to unsigned";
throw strStream.str();
}
return u;
}
float util::convertToFloat(const std::string& str) {
float f = 0;
if (!stringToValue(str, &f, 1)) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to float";
throw strStream.str();
}
return f;
}
double util::convertToDouble(const std::string& str) {
float d = 0;
if (!stringToValue(str, &d, 1)) {
std::ostringstream strStream;
strStream << __FUNCTION__ << " Bad conversion of [" << str << "] to double";
throw strStream.str();
}
return d;
}
这个例子并没有完全使用返回类型的函数重载解析,但是这个 c++ 非对象类使用模板特化来模拟返回类型的函数重载解析和私有静态方法。
每个convertToType
函数都在调用函数模板stringToValue()
,如果您查看它正在调用的此函数模板的实现细节或算法,getValue<T>( param, param )
它会返回一个类型T
并将其存储到作为其参数之一T*
传递给函数模板的 a 中stringToValue()
.
除了这样的事情;C++ 并没有真正的机制来通过返回类型进行函数重载解析。可能还有其他我不知道的构造或机制可以通过返回类型来模拟解析。
我认为这是现代 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”中的代码?
大多数静态语言现在也支持泛型,这将解决您的问题。如前所述,如果没有参数差异,就无法知道要调用哪一个。因此,如果您想这样做,只需使用泛型并收工。