217

不久前,我受到来自 R 核心团队的 Simon Urbanek 的指责(我相信),因为我建议用户return在函数结束时显式调用(尽管他的评论已被删除):

foo = function() {
  return(value)
}

相反,他建议:

foo = function() {
  value
}

可能在这种情况下,它是必需的:

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

他的评论阐明了为什么return除非非常需要才打电话是一件好事,但这已被删除。

我的问题是:为什么不调用return更快或更好,因此更可取?

4

9 回答 9

147

问题是:为什么不(明确地)调用 return 更快或更好,因此更可取?

R 文档中没有声明做出这样的假设。
主页“功能”说:

function( arglist ) expr
return(value)

不调用return会更快吗?

function()和都是return()原始函数,function()即使不包含函数,它本身也会返回最后的评估值return()

return()将最后一个值作为参数调用.Primitive('return')将完成相同的工作,但需要再调用一次。这样这个(通常)不必要的.Primitive('return')调用会消耗额外的资源。然而,简单的测量表明产生的差异非常小,因此不能成为不使用显式返回的原因。下图是根据以这种方式选择的数据创建的:

bench_nor2 <- function(x,repeats) { system.time(rep(
# without explicit return
(function(x) vector(length=x,mode="numeric"))(x)
,repeats)) }

bench_ret2 <- function(x,repeats) { system.time(rep(
# with explicit return
(function(x) return(vector(length=x,mode="numeric")))(x)
,repeats)) }

maxlen <- 1000
reps <- 10000
along <- seq(from=1,to=maxlen,by=5)
ret <- sapply(along,FUN=bench_ret2,repeats=reps)
nor <- sapply(along,FUN=bench_nor2,repeats=reps)
res <- data.frame(N=along,ELAPSED_RET=ret["elapsed",],ELAPSED_NOR=nor["elapsed",])

# res object is then visualized
# R version 2.15

功能经过时间比较

上图在您的平台上可能略有不同。根据测量数据,返回对象的大小不会造成任何差异,重复次数(即使按比例放大)只会产生很小的差异,这在实际数据和实际算法中无法计算或使您的脚本运行得更快。

不调用return会更好吗?

Return是一个很好的工具,可以清楚地设计例程应该结束的代码“叶子”,跳出函数并返回值。

# here without calling .Primitive('return')
> (function() {10;20;30;40})()
[1] 40
# here with .Primitive('return')
> (function() {10;20;30;40;return(40)})()
[1] 40
# here return terminates flow
> (function() {10;20;return();30;40})()
NULL
> (function() {10;20;return(25);30;40})()
[1] 25
> 

这取决于程序员的策略和编程风格,他使用什么风格,他可以使用 no return() 因为它不是必需的。

R核心程序员使用这两种方法,即。有和没有明确的 return() 因为可以在“基本”函数的来源中找到。

很多时候,在有条件地停止函数的情况下,只使用 return()(无参数)返回 NULL。

目前尚不清楚它是否更好,因为使用 R 的标准用户或分析师看不到真正的区别。

我认为问题应该是:使用来自 R 实现的显式返回是否有任何危险?

或者,也许更好的是,编写函数代码的用户应该总是问:在函数代码使用显式返回(或将要返回的对象作为代码分支的最后一叶)有什么影响?

于 2012-08-06T19:19:23.583 回答
111

如果每个人都同意

  1. return在函数体的末尾不需要
  2. 不使用return稍微快一点(根据@Alan 的测试,4.3 微秒对 5.1 微秒)

我们都应该return在函数结束时停止使用吗?我当然不会,我想解释一下原因。我希望听到其他人是否同意我的意见。如果这不是对 OP 的直接回答,我很抱歉,而更像是一个长长的主观评论。

正如保罗指出的那样,我不使用return的主要问题是,函数体中的其他地方可能需要它。如果你被迫return在函数中间的某个地方使用,为什么不让所有return语句都明确呢?我讨厌前后矛盾。我也认为代码读起来更好;可以扫描函数并轻松查看所有退出点和值。

保罗用了这个例子:

foo = function() {
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

不幸的是,有人可以指出它可以很容易地重写为:

foo = function() {
 if(a) {
   output <- a
 } else {
   output <- b
 }
output
}

后一个版本甚至符合一些提倡每个函数一个返回语句的编程编码标准。我认为一个更好的例子可能是:

bar <- function() {
   while (a) {
      do_stuff
      for (b) {
         do_stuff
         if (c) return(1)
         for (d) {
            do_stuff
            if (e) return(2)
         }
      }
   }
   return(3)
}

使用单个 return 语句重写这将更加困难:它需要多个breaks 和一个复杂的布尔变量系统来传播它们。所有这一切都说明单一返回规则在 R 中不能很好地发挥作用。因此,如果您需要return在函数体的某些地方使用,为什么不保持一致并在任何地方都使用它呢?

我不认为速度论点是一个有效的论点。当您开始查看实际执行某些操作的函数时,0.8 微秒的差异不算什么。我能看到的最后一件事是打字少了,但是嘿,我并不懒惰。

于 2012-08-10T04:21:14.800 回答
28

这是一个有趣的讨论。我认为@flodel 的例子非常好。但是,我认为它说明了我的观点(@koshke 在评论中提到了这一点),当您使用命令式而不是函数式编码风格时,这return是有道理的

不是要强调这一点,但我会这样重写foo

foo = function() ifelse(a,a,b)

函数式风格避免了状态变化,比如存储output. 在这种风格return下,显得格格不入;foo看起来更像一个数学函数。

我同意@flodel:使用复杂的布尔变量系统bar会不太清楚,当你有return. 使陈述bar如此顺从的return原因是它是以命令式的风格编写的。事实上,布尔变量代表了功能风格中避免的“状态”变化。

用函数式重写真的很难bar,因为它只是伪代码,但想法是这样的:

e_func <- function() do_stuff
d_func <- function() ifelse(any(sapply(seq(d),e_func)),2,3)
b_func <- function() {
  do_stuff
  ifelse(c,1,sapply(seq(b),d_func))
}

bar <- function () {
   do_stuff
   sapply(seq(a),b_func) # Not exactly correct, but illustrates the idea.
}

循环将while是最难重写的,因为它是由状态更改控制的a

调用造成的速度损失return可以忽略不计,但通过避免return和重写函数式风格所获得的效率往往是巨大的。告诉新用户停止使用return可能无济于事,但引导他们使用实用风格会有所收获。


@Paulreturn在命令式风格中是必需的,因为您经常希望在循环中的不同点退出函数。函数式风格不使用循环,因此不需要return. 在纯函数式风格中,最终调用几乎总是所需的返回值。

在 Python 中,函数需要一个return语句。但是,如果您以函数式风格编写函数,则可能只有一个return语句:在函数的末尾。

使用另一个 StackOverflow 帖子中的示例,假设我们想要一个函数,TRUE如果给定中的所有值x都具有奇数长度,则该函数会返回。我们可以使用两种样式:

# Procedural / Imperative
allOdd = function(x) {
  for (i in x) if (length(i) %% 2 == 0) return (FALSE)
  return (TRUE)
}

# Functional
allOdd = function(x) 
  all(length(x) %% 2 == 1)

在函数式风格中,要返回的值自然落在函数的末尾。同样,它看起来更像是一个数学函数。

@GSee 中概述的警告?ifelse绝对很有趣,但我不认为他们试图阻止使用该功能。实际上,ifelse具有自动矢量化功能的优势。例如,考虑一个稍微修改过的版本foo

foo = function(a) { # Note that it now has an argument
 if(a) {
   return(a)
 } else {
   return(b)
 }
}

这个函数在length(a)为 1 时工作正常。但如果你fooifelse

foo = function (a) ifelse(a,a,b)

现在foo适用于任何长度的a. 事实上,它甚至可以在a是一个矩阵时起作用。test返回一个与矢量化具有相同形状的值,这不是问题。

于 2012-08-11T11:43:04.143 回答
22

似乎没有return()它会更快...

library(rbenchmark)
x <- 1
foo <- function(value) {
  return(value)
}
fuu <- function(value) {
  value
}
benchmark(foo(x),fuu(x),replications=1e7)
    test replications elapsed relative user.self sys.self user.child sys.child
1 foo(x)     10000000   51.36 1.185322     51.11     0.11          0         0
2 fuu(x)     10000000   43.33 1.000000     42.97     0.05          0         0

____编辑__ _ __ _ __ _ __ _ __ _ ___

我继续进行其他基准测试(benchmark(fuu(x),foo(x),replications=1e7)),结果相反......我将在服务器上尝试。

于 2012-07-31T13:48:17.230 回答
15

我的问题是:为什么不打电话return更快

它更快,因为它return是 R 中的(原始)函数,这意味着在代码中使用它会产生函数调用的成本。将此与大多数其他编程语言进行比较,其中return是关键字,但不是函数调用:它不会转换为任何运行时代码执行。

也就是说,以这种方式调用原始函数在 R 中非常快,并且调用return会产生很小的开销。这不是省略的论据return

还是更好,因此更可取?

因为没有理由使用它。

因为它是冗余的,并且不会增加有用的冗余。

需要明确的是:冗余有时是有用的。但大多数冗余不是这种类型的。相反,它是一种在不添加信息的情况下增加视觉混乱的类型:它相当于一个填充词chartjunk的编程)。

考虑下面的解释性注释示例,它被普遍认为是不好的冗余,因为注释只是解释了代码已经表达的内容:

# Add one to the result
result = x + 1

在 R 中使用return属于同一类别,因为 R 是一种函数式编程语言,并且在 R 中每个函数调用都有一个值。这是 R 的一个基本属性。一旦你从每个表达式(包括每个函数调用)都有一个值的角度来看 R 代码,问题就会变成:“我为什么使用return?” 需要有一个积极的理由,因为默认情况下不使用它。

一个这样的积极原因是发出提前退出函数的信号,例如在保护子句中:

f = function (a, b) {
    if (! precondition(a)) return() # same as `return(NULL)`!
    calculation(b)
}

这是对return. 然而,与其他语言相比,这样的保护子句在 R 中很少见,并且由于每个表达式都有一个值,因此正if则不需要return

sign = function (num) {
    if (num > 0) {
        1
    } else if (num < 0) {
        -1
    } else {
        0
    }
}

我们甚至可以这样重写f

f = function (a, b) {
    if (precondition(a)) calculation(b)
}

if (cond) expr与 相同的地方if (cond) expr else NULL

最后,我想先排除三个常见的反对意见:

  1. 一些人认为 usingreturn增加了清晰度,因为它表示“这个函数返回一个值”。但正如上面所解释的,每个函数都返回 R 中的某些内容。将return其视为返回值的标记不仅是多余的,而且还具有误导性

  2. 与此相关的是,Python之有一个奇妙的指导方针,应该始终遵循:

    显式优于隐式。

    删除冗余如何不return违反这一点?因为函数式语言中函数的返回值总是明确的:它是它的最后一个表达式。这又是关于明确性冗余性的相同论点。

    实际上,如果您想要明确性,请使用它来突出规则的例外情况:标记返回有意义值的函数,这些函数仅因其副作用(例如cat)而被调用。除了 R 有比return这种情况更好的标记:invisible. 例如,我会写

    save_results = function (results, file) {
        # … code that writes the results to a file …
        invisible()
    }
    
  3. 但是长函数呢?忘记退回的东西不是很容易吗?

    两个答案:首先,不是。规则很明确:函数的最后一个表达式就是它的值。没有什么可追踪的。

    但更重要的是,长函数的问题不在于缺少显式return标记。这是函数的长度。长函数几乎(?)总是违反单一职责原则,即使它们不这样做,它们也会从为了可读性而被分解中受益。

于 2019-11-28T14:10:07.850 回答
14

没有明确地将“return”放在末尾的一个问题是,如果在方法的末尾添加额外的语句,突然返回值是错误的:

foo <- function() {
    dosomething()
}

这将返回 的值dosomething()

现在我们第二天来并添加一个新行:

foo <- function() {
    dosomething()
    dosomething2()
}

我们希望我们的代码返回 的值dosomething(),但它不再这样做了。

通过显式返回,这变得非常明显:

foo <- function() {
    return( dosomething() )
    dosomething2()
}

我们可以看到这段代码有一些奇怪的地方,并修复它:

foo <- function() {
    dosomething2()
    return( dosomething() )
}
于 2012-12-05T03:36:24.873 回答
6

我认为return这是一个诡计。作为一般规则,函数中计算的最后一个表达式的值成为函数的值——这种一般模式在许多地方都可以找到。以下所有评估为 3:

local({
1
2
3
})

eval(expression({
1
2
3
}))

(function() {
1
2
3
})()

return所做的并不是真正返回一个值(无论有没有它都可以完成),而是以不规则的方式“突破”函数。从这个意义上说,它是 R 中最接近 GOTO 语句的等价物(还有 break 和 next)。我return很少使用,也从不在函数末尾使用。

 if(a) {
   return(a)
 } else {
   return(b)
 }

...这可以重写为if(a) a else b可读性更好且花括号更少。return这里根本不需要。我使用“return”的典型案例类似于......

ugly <- function(species, x, y){
   if(length(species)>1) stop("First argument is too long.")
   if(species=="Mickey Mouse") return("You're kidding!")
   ### do some calculations 
   if(grepl("mouse", species)) {
      ## do some more calculations
      if(species=="Dormouse") return(paste0("You're sleeping until", x+y))
      ## do some more calculations
      return(paste0("You're a mouse and will be eating for ", x^y, " more minutes."))
      }
   ## some more ugly conditions
   # ...
   ### finally
   return("The end")
   }

一般来说,多次退货的需求表明问题要么丑陋,要么结构不良。

[编辑]

return实际上并不需要一个函数来工作:您可以使用它来打破一组要评估的表达式。

getout <- TRUE 
# if getout==TRUE then the value of EXP, LOC, and FUN will be "OUTTA HERE"
# .... if getout==FALSE then it will be `3` for all these variables    

EXP <- eval(expression({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   }))

LOC <- local({
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })

FUN <- (function(){
   1
   2
   if(getout) return("OUTTA HERE")
   3
   })()

identical(EXP,LOC)
identical(EXP,FUN)
于 2013-10-28T21:48:54.590 回答
2

return可以增加代码的可读性:

foo <- function() {
    if (a) return(a)       
    b     
}
于 2016-05-04T23:09:06.350 回答
2

冗余的论点在这里出现了很多。在我看来,这还不足以省略return(). 冗余并不是一件坏事。当战略性地使用时,冗余使代码更清晰,更易于维护。

考虑这个例子:函数参数通常有默认值。所以指定一个与默认值相同的值是多余的。除了它使我期望的行为很明显。无需打开功能手册页来提醒自己默认值是什么。并且不用担心函数的未来版本会改变其默认值。

调用的性能损失可以忽略不计return()(根据其他人在此处发布的基准),它归结为风格而不是对与错。对于“错误”的事情,需要有一个明显的劣势,这里没有人令人满意地证明包含或省略return()具有一致的劣势。它似乎非常特定于案例和特定于用户。

所以这就是我的立场。

function(){
  #do stuff
  ...
  abcd
}

我对上面示例中的“孤立”变量感到不舒服。abcd会成为我没有写完的声明的一部分吗?它是我的代码中拼接/编辑的残余物并且需要删除吗?我是否不小心从其他地方粘贴/移动了某些东西?

function(){
  #do stuff
  ...
  return(abdc)
}

相比之下,第二个示例让我清楚地看到它是一个预期的返回值,而不是一些意外或不完整的代码。对我来说,这种冗余绝对不是无用的。

当然,一旦函数完成并工作,我可以删除返回。但是删除它本身就是一个多余的额外步骤,在我看来,这比return()首先包含它更没用。

综上所述,我不使用return()简而言之未命名的单行函数。在那里,它构成了函数代码的很大一部分,因此主要导致视觉混乱,使代码不那么清晰。但是对于更大的正式定义和命名的函数,我使用它并且可能会继续使用它。

于 2019-11-29T10:49:36.990 回答