函数通过返回types重载?
为什么没有更多的主stream静态types语言支持通过返回types重载函数/方法? 我想不出有什么办法。 它似乎没有比通过参数types支持超载更有用或更合理。 它怎么不那么受欢迎?
与其他人所说的相反,按返回types重载是可能的,并且是由一些现代语言完成的。 通常的反对意见是在代码中
int func(); string func(); int main() { func(); }
你不能分辨哪个func()
被调用。 这可以通过几种方式解决:
- 有一个可预测的方法来确定在这种情况下调用哪个函数。
- 每当发生这种情况,这是一个编译时错误。 但是,有一个语法允许程序员消除歧义,例如
int main() { (string)func(); }
int main() { (string)func(); }
。 - 不要有副作用。 如果你没有副作用,而且你从来不使用函数的返回值,那么编译器可以避免在第一时间调用函数。
我经常使用的两种语言( ab )通过返回types使用重载: Perl和Haskell 。 让我描述他们做什么。
在Perl中 , 标量和列表上下文之间存在根本区别(和其他,但我们假设有两个)。 Perl中的每个内置函数都可以根据调用的上下文来做不同的事情。 例如,当scalar
操作符强制标量上下文时, join
操作符强制列表上下文(在被连接的事物上),所以比较:
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
每个条目都logging了这个。 例如,以下是glob EXPR
条目的一部分:
在列表上下文中,返回一个(可能是空的)
EXPR
值的文件扩展列表,例如标准的Unix shell/bin/csh
。 在标量上下文中,glob遍历这样的文件名扩展,当列表耗尽时返回undef。
现在,列表和标量上下文有什么关系? 那么, man perlfunc
说
请记住以下重要规则:没有规则将列表上下文中的expression式行为与其在标量上下文中的行为相关联,反之亦然。 它可能会做两件完全不同的事情。 每个运算符和函数决定在标量上下文中最适合返回哪种types的值。 一些运算符返回列表上下文中返回的列表的长度。 一些运算符返回列表中的第一个值。 一些运算符返回列表中的最后一个值。 一些操作员返回成功操作的次数。 一般来说,他们做你想做的,除非你想要一致。
所以这不是一个单一function的简单问题,然后在最后做简单的转换。 事实上,我之所以selectlocaltime
例子。
这不仅仅是具有这种行为的内置插件。 任何用户都可以使用wantarray
来定义这样一个函数,它允许您区分列表,标量和无效上下文。 所以,例如,如果您在无效的情况下被调用,您可以决定什么也不做。
现在,您可能会抱怨这不是真正的通过返回值重载,因为您只有一个函数,它被告知调用的上下文,然后作用于该信息。 然而,这显然是等价的(类似于Perl不允许通常的字面重载,但函数只能检查它的参数)。 而且,很好地解决了这个回应开始时提到的模棱两可的情况。 Perl不会抱怨它不知道要调用哪个方法; 它只是调用它。 所有它需要做的是找出什么上下文function被调用,这是永远有可能的:
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采取另一种方法,即没有副作用。 它也有一个强大的types系统,所以你可以编写如下的代码:
main = do n <- readLn print (sqrt n) -- note that this is aligned below the n, if you care to run this
该代码从标准input读取浮点数,并打印其平方根。 但是有什么令人惊讶的呢? 那么readLn
的types是readLn :: Read a => IO a
。 这意味着对于任何可以Read
types(forms上,每个types都是Read
types的一个实例), readLn
可以读取它。 Haskell怎么知道我想读一个浮点数? 那么, sqrt
的types是sqrt :: Floating a => a -> a
,这实际上意味着sqrt
只能接受浮点数作为input,所以Haskell推断出我想要的。
当Haskell无法推断我想要什么时会发生什么? 那么,有几个可能性。 如果我根本不使用返回值,那么Haskell根本就不会调用函数。 但是,如果我确实使用了返回值,那么Haskell会抱怨它不能推断出这个types:
main = do n <- readLn print n -- this program results in a compile-time error "Unresolved top-level overloading"
我可以通过指定我想要的types来解决歧义:
main = do n <- readLn print (n::Int) -- this compiles (and does what I want)
无论如何,这整个讨论的意思是,通过返回值重载是可能的,并完成,这回答了你的问题的一部分。
你的问题的另一部分是为什么更多的语言不这样做。 我会让别人回答。 然而,有一些评论:主要原因可能是在这里混淆的机会确实大于通过参数types重载的机会。 您还可以查看各种语言的基本原理:
Ada :“看起来最简单的重载parsing规则就是使用所有的信息 – 尽可能广泛地使用所有信息 – 来解决重载的引用,这个规则可能很简单,但是没有什么帮助,它需要读者扫描任意大的文本,并进行任意复杂的推理(如上面的(g))我们认为一个更好的规则是使任务成为人类读者或编译器必须执行的任务,并且这使得这个任务尽可能让读者自然。“
C ++(Bjarne Stroustrup的“C ++编程语言”第7.4.1小节):“在重载parsing中不考虑返回types,原因在于为单独的运算符或函数调用保持parsing独立于上下文。
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) }
如果考虑到返回types,就不再可能孤立地查看sqrt()
的调用,并确定调用哪个函数。“(注意,为了比较,在Haskell中没有隐式转换)。
Java( Java语言规范9.4.1 ):“其中一个inheritance的方法必须是返回types可替代的任何其他inheritance的方法;否则,会发生编译时错误。 (是的,我知道这并不是说明理由,我确信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 }
如果函数被返回types重载,并且有这两个重载
int func(); string func();
编译器不可能找出这两个函数中的哪一个调用这样的调用
void main() { func(); }
出于这个原因,语言devise者经常不允许返回值超载。
但是,某些语言(如MSIL)允许通过返回types进行重载。 他们当然也面临着上述困难,但他们有解决方法,你必须查阅他们的文档。
用这样的语言,你将如何解决以下问题:
f(g(x))
如果f
有重载void f(int)
和void f(string)
并且g
有重载int g(int)
和string g(int)
? 你需要某种消歧器。
我认为你可能需要这个的情况会更好地通过select一个新的函数名称。
从另一个非常相似的问题 (愚蠢?)窃取一个C ++的具体答案 :
函数返回types不会在重载parsing中起作用,因为Stroustrup(我假设来自其他C ++架构师的input)希望重载parsing是“独立于上下文的”。 请参见“C ++编程语言,第三版”中的7.4.1 – “重载和返回types”。
原因是保持一个单独的运算符或函数调用独立的解决scheme。
他们希望它只基于如何调用超载,而不是如何使用结果(如果它被使用的话)。 事实上,许多函数被调用而不使用结果,或者结果将被用作较大expression式的一部分。 当我们确定这一点时,我确信的一个因素是,如果返回types是分辨率的一部分,那么会有很多调用重载的函数,这些调用需要用复杂的规则来解决,或者必须让编译器抛出这个电话是模棱两可的。
而且,主知道,C ++重载决议是足够复杂的,因为它站着…
在haskell中,即使它没有函数重载也是可能的。 Haskell使用types类。 在一个程序中你可以看到:
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#。 在所有dynamic语言中,它都是一个简写:
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()
所以没有太多的意义。 大多数人都不会感兴趣,语言是否能够帮助你在任何地方使用它,而不用花费一分一秒的时间。
模式匹配有点类似于函数重载,我猜有时候也是类似的。 这种情况并不常见,因为它仅适用于less数程序,而且在大多数语言中实现起来很棘手。
您会发现有无数其他更好的易于实现的function可以实现到该语言中,其中包括:
- dynamictypes
- 内部支持列表,字典和Unicodestring
- 优化(JIT,types推断,编译)
- 集成的部署工具
- 图书馆支持
- 社区支持和聚会的地方
- 丰富的标准库
- 良好的语法
- 阅读eval打印循环
- 支持reflection编程
很好的答案! A.Rex的答案尤其是非常详细和有益的。 正如他指出的那样,C ++在编译lhs = func();
时会考虑用户提供的types转换操作符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 =返回types)的解决scheme:
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 :)
这个解决scheme的好处是,包含这些模板定义的任何代码都可以为更多的types添加更多的特化。 也可以根据需要对结构进行部分特化。 例如,如果您想要对指针types进行特殊处理:
template <typename T> struct func<T*> { operator T*() { <<special handling for T*>> } };
作为否定,你不能写int x = func();
与我的解决scheme。 你必须写int x = func<int>();
。 你必须明确地说出返回types是什么,而不是要求编译器通过查看types转换操作符来解决这个问题。 我会说“我的”解决scheme和A.Rex都属于解决这个C ++困境的方法的最佳方式:)
正如已经显示的那样 – 只有返回types不同的函数的模糊调用引入了模糊性。 歧义导致有缺陷的代码。 必须避免有缺陷的代码。
试图模糊化所带来的复杂性表明,这不是一个好的破解。 除了智力练习之外,为什么不使用具有参考参数的程序。
procedure(reference string){}; procedure(reference int){}; string blah; procedure(blah)
这个重载function不难pipe理,如果你以一种稍微不同的方式来看待它。 考虑以下几点,
public Integer | String f(int choice){ if(choice==1){ return new string(); }else{ return new Integer(); }}
如果语言确实返回重载,则会导致参数重载,但不能重复。 这将解决以下问题:
main (){ f(x) }
因为只有一个f(intselect)可供select。
在.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; }
为了logging, Octave根据返回元素是标量还是数组允许不同的结果。
x = min ([1, 3, 0, 2, 0]) ⇒ x = 0 [x, ix] = min ([1, 3, 0, 2, 0]) ⇒ x = 0 ix = 3 (item index)
也可以是奇异值分解 。
大多数静态语言现在也支持generics,这将解决您的问题。 如前所述,没有参数差异,没有办法知道要调用哪一个。 所以,如果你想这样做,只需使用generics,并称之为一天。