在C ++和Java中只有一个返回值的原因是什么?

时间:2015-04-05 12:34:04

标签: java c++

通常我会瞥一眼这个事实并接受它作为"这就是轮子旋转的方式。但今天我想知道它来自何处:为什么函数只有一个返回(参考)值?

为什么不能,函数返回多个值会变得困难或不明智?是因为对象以及您应该如何期望始终以对象的引用形式返回数据包?

如果你看到一个声明的函数作为一个契约,它说明了如何调用一个函数,它应该采用哪些参数,以及它具有哪种返回值类型,那么我就不会看到只有一个返回的逻辑值,因为您可以反过来应用相同的逻辑(从而创建多个返回值的合约)。

基于我可以看到的逻辑的唯一原因是,如果一个函数返回多于1个的东西,它还应该操作多于1个的东西超过1个原因,这违背了函数应该只做的哲学一件事。如果有一件事正在操纵一个对象并返回另一个对象就会有意义,你可以用操纵对象返回一个参考值。

那么为什么存在这种限制呢?

10 个答案:

答案 0 :(得分:32)

每当你有多个值x1 ... xN时,你就会得到元组(x1, ..., xN)这是一个单独的(尽管是复合的)值。如果您没有使用您的语言的元组,请随意使用任何聚合类型(structclass,数组,其他集合)。因此,从这个角度来看,返回“多个”值并返回单个值是完全等效的。

这只是意味着你只需要一个,但为什么要省略另一个?

首先,您无论如何都需要聚合类型,因此选择是在具有聚合类型还是仅具有聚合类型之间。其次,如果一个函数可以返回“多个值”,那么你将面临一个语义难题:突然一个表达式没有评估为值(我们之前已经非常具体地定义过),它就是这个新的,不同类别的事物,称为“多个值”。现在这个结果的静态类型是什么?该计划可以做什么而不做呢?在实施条款中它意味着什么?

你当然可以人为地回答这些问题,但任何合理的方法都只会构成元组类型。忽略这一点会让你对一个非常有用的观点视而不见,拒绝让它们成为一等价值的人可能更多复杂而不是说“这些是元组类型,它们可以像这样构造并像这样解构”

答案 1 :(得分:7)

当一个函数返回多个东西时,这些东西彼此相关,至少由于单个函数返回。单独考虑就足以要求返回多个事物的函数的作者通过创建适当的数据结构将这些多个事物组合在一起,这已经可以通过返回单个项目的函数来实现。

例如,在C ++中std::set::insert需要返回两个值 - 一个迭代器和一个bool来指示迭代器是一个旧元素还是我们刚插入的元素。解决方案是返回std::pair

pair<iterator,bool> insert (const value_type& val);

第二个考虑因素纯粹是实用的:在像

这样的表达式中处理多个返回值
int x = f(y);
当只有一个返回值时,

简单直观。尝试返回多个值,如

int x, y = f(w, z);

会变得难以解析,因为读者必须检查f的返回类型,看看它是否返回int或两个int

当返回项目的数据类型不同时,我甚至没有触及这种情况!语法很快就会变成一场噩梦,而语言的表达能力却没有相应的增长。

答案 2 :(得分:7)

我希望人们停止解释“我不能用语言 Y ”来解释 X ,因为“语言 Y 有限制阻止我从做 X “。有时这实际上是正确的 - 例如C ++'const修饰符基本上只是禁止你以一种语法方式改变值,这种方式可以很好地表达,但如果你想要的话,就不认为是明智的确保您的程序以某种可预测的方式运行。

OTOH,对于大多数功能 X ,你可以想出它只是不会让任何感觉想要一种语言。如果我问,“我不能只differentiate所有C ++函数的原因是什么?”对于一个对编程一无所知的物理学家或工程师来说,这似乎不是一个牵强附会的事情,如同这些家伙传统上只处理a particular kind 1 functions in the maths sense总是可以随意区分

但是对于编程语言中的大多数函数来说,从概念上来说概念是绝对没有意义的。对于程序员来说,分析函数只是所有函数的一小部分,仅仅因为导数的概念只适用于连续域(在实践中,基本上适用于向量空间) 2 ,当然它是完全与程序和OO语言中的“函数”允许的副作用不兼容。

现在,允许返回多个值肯定比允许区分函数更加平凡,但它仍然以“函数”的意味着的基本方式发生变化。你不能再仅仅参考函数的 返回类型了,你需要弄清楚用多个返回值组合函数意味着什么,你需要为函数指针提出一些新的语法,等等
与差异化示例不同,所有这一切都可能是,但应该清楚它会改变几乎所有的语言。它非常只是“停止阻止我返回多个东西!”

某些语言,例如Fortran和Matlab对语言中的多个返回值进行了硬连线支持。哦,小伙子,我可以告诉你这不会使这些语言的编程更好:你可以很容易地用C ++函数做的许多事情都被阻止了,因为它们会被多次返回搞砸!
其他语言,例如Python,有一个光线替代方案:看起来就像你正在返回多个东西,但实际上你只是返回一个元组并获得一些语法糖,用于将组件模式匹配回单个变量。

当然,正如其他答案所说,返回元组或类似的东西是对你的问题的合理解决方案,尽管它有时会在对元组没有很好的语法支持的语言中有点笨拙。 (但这只是一些无害的样板。)

<小时/> 1 好吧,至少他们假装他们正在处理分析函数。通常,这在数学上几乎不可接受。实际上,他们只是通过分析函数逼近所有内容。

2 有趣的是,在具有适当代数数据类型的语言中,a remarkable analogue对于经典的分化思想以非常令人惊讶的方式出现,而实际上并不需要连续的域。这仍然与例如“取 x 2 /(1+ e x )的衍生物”


更进一步:可以说,函数也应该只有一个参数。那也可以是复合类型,但你甚至不需要元组:有一个名为Currying的东西 - 两个参数的任何函数也可以表示为单个参数的函数,返回一个函数第二个论点。这可能看起来有点像一种奇怪的思维方式,但实际上它的效果非常好。 Haskell这样做了,许多人认为这是关于语言的疯狂事情之一,但它允许你比大多数其他语言更可能地表达一些操作,同时语法实际上保持不变非常简单。

答案 3 :(得分:6)

这纯粹是历史性的。这通常是数学函数的工作方式。而且,一些语言(实际上很多)允许返回多个值 - 通常使用元组对象。

回归多个对象会很好的想法来得太晚了。为什么选择通过对象完成,而不是语言语法?因为OO编程在野外使用并且被普遍接受。

从语义上讲,两种方式具有相同的含义 - &#34;返回多个方面&#34;。因此,添加此类功能将等同于支持具有相同含义的另一种语法。有两种方法可以做一件事在编程中不是很受欢迎。程序员想知道选择哪种方式,有些只知道一种方法,编译器开发人员必须维护两种功能没有区别的机制。这是一个非常好的理由。 Okham的剃须刀是受欢迎的工具。对象更有用。

另一件事是,你的论证:&#34; 一个声明的函数作为一个契约,它说明了如何调用一个函数,它应该采用哪些参数,以及它具有哪个返回值类型< / strong> &#34;,并不擅长大胆的部分。

返回类型通常不被视为功能签名的一部分。您不能只有返回值不同的重载。话虽这么说,当然编译器需要知道在函数完成后它应该占用多少字节作为返回值。所以它并非完全不相关,尽管通常语言不考虑这一点。

答案 4 :(得分:6)

您的问题并未明确您正在寻找的具体功能:使其成为&#34;外观&#34;像你一样,返回几个值,或者函数返回多个值的基本能力。

如果是前者,那么就没有什么可以给出的答案:语言只是他们为此提供的语法支持水平不同。例如,LISP完全支持多值返回,并且许多语言(Scala,Clojure)支持 destructuring bind 成语,这非常类似于您正在寻找的内容,但是针对引用类型的工作元组。一般来说Java的语法很差,所以它缺乏对解构绑定的支持也就不足为奇了。

对于后者 - 在C中,返回struct类型正是您正在寻找的功能。结构将放在堆栈框架上,并包含任意数量的值。其他语言不允许您以这样的细节级别控制堆栈/堆放置,但仍允许您返回指向此类结构的指针。

答案 5 :(得分:4)

之前我从未参与编译器内部工作,但术语函数通常是指二元关系,它对其中的每个项目(输入集合)都有唯一的映射它的codomain(输出集)。 现在,这些设置元素本身可以是聚合的。 也许更熟悉编译器理论的人可以详细说明这一点。

答案 6 :(得分:4)

在数学中,函数具有域和密码域。 f:S->T表示具有域S和codomain T的函数。

具有两个参数和一个或多或少的返回值的方法对应于函数f:SxT->U,其中x表示笛卡尔积。因此,例如,参数类型为doubleint且返回boolean的方法大致对应于函数f:DxI->B,其中D是一组所有double值等。这是在不将两个参数一起捆绑成元组的情况下完成的。

没有理由为什么我们不能同样地使用与f:X->YxZ形式的函数相对应的方法,而不将两个返回值捆绑在一起形成元组。

例如,Java方法

BigInteger[] divideAndRemainder(BigInteger divisor)

可以很容易地签名

BigInteger q, BigInteger r divideAndRemainder(BigInteger divisor)

在方法正文的某处,会有一个return语句,看起来像return q, r;。根本没有理由使用阵列。

事实上,这会更好,因为您可以通过编写

来调用该方法
BigInteger q, BigInteger r = bigInt1.divideAndRemainder(bigInt2);

而不是

BigInteger[] arr = bigInt1.divideAndRemainder(bigInt2);
BigInteger q = arr[0];
BigInteger r = arr[1];

我想这是不允许的原因是它会使语言复杂化而不需要它。如果您查看多种语言的标准库并比较将多个返回值捆绑到单个对象中的方法的数量,与具有多个参数的方法的数量相比,您可能会发现后者是更常见。

答案 7 :(得分:4)

当天某些CPU寄存器具有特定的约定,并且CPU寄存器受到限制。因此汇编代码通常只返回一个值,例如,在8086的AX中。

编写汇编代码很费力,并且'C'出现以减轻一些负担,同时仍然允许开发人员实现类似的汇编代码性能。

为了强调这一点,请考虑这个'C'代码:

int foo(void)
{
    // no return statement; default register used
}

在这种情况下,函数foo将返回CPU类型的“返回值”寄存器中的值,例如,32位x86处理器上的EAX。

'C ++'与'C'可互操作,并不引入第二种形式的return语句。

答案 8 :(得分:3)

函数只是在程序堆中留下内容的子程序。如果堆栈中剩下的东西是不可预测的,那么你现在必须有一些方法来预测堆栈上剩下的东西。这会产生开销。我们决定只指出一件可能是许多事情的数据结构或可能只是一个int的事情。

数学家会告诉你一个函数是关于将每个参数(输入值)与相应的输出值相关联,但是对于计算机科学家来说,函数是关于堆栈在返回时将保留的状态的契约。

答案 9 :(得分:1)

我认识到了这个问题。假设您循环访问数据集合,您需要在某些方面过滤数据。作为遍历数据的副作用,您将找到另一个您想要拥有的有趣功能,但该结果不符合所请求的功能结果。

示例:遍历所有代码以找出哪些函数被哪些函数调用。在遍历时,您可能会发现从不使用/调用的副作用函数。您只能在遍历所有函数时找到这些函数,它不是调用的结果,严格说来与结果无关,但它是非常有趣的信息来检索。 你该怎么办?编写执行相同功能的另一个功能,但现在有不同的结果?创建一个存储两个结果的单独对象?将结果存储在对象的新变量中?或者具有多种结果的功能?

从设计编程语言的角度来看,这些副作用是不可预测的。你真的无法根据不可预测的要求设计一种语言。接下来的功能应如何应对呢?为了使函数尽可能松散地相互耦合,保持函数结果尽可能简单是一种好的做法。功能设计应适用德米特定律。让函数具有不相关结果的输出使得不可能编写松散耦合的代码。如果你必须改变一系列函数,它可能会成为一个真正的噩梦,因为具有多个输出的第一个函数会丢弃其中一个结果 - 或者为它添加一个新结果。

我对上述问题的解决方案是两次“编写”代码。一次找到函数之间的所有关系,另一次查找从未被调用过的所有函数。这样,函数的内容与应用程序的其余部分之间就会有一个清晰的分离。我只是假装我不知道我之前写过这个函数。

我认为让一个函数只产生一个连贯的结果是一个很好的设计。它使整个过程的设计更加结构化,这有助于构建更大,更复杂的应用程序。