是否显式调用return函数


199

前阵子我被指责西蒙Urbanek从R核心团队(我相信)为用户推荐到显式调用return的函数结束时(他的评论被删除虽然):

foo = function() {
  return(value)
}

相反,他建议:

foo = function() {
  value
}

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

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

他的评论阐明了为什么return除非严格需要,否则不打电话是一件好事,但是已删除。

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


12
return即使在最后一个示例中也没有必要。删除return可能会使它更快一些,但是在我看来,这是因为R被认为是一种函数式编程语言。
kohske 2012年

4
@kohske您能否将您的评论扩展成答案,包括更多有关为何更快的详细信息,以及与R作为函数式编程语言有何关系?
Paul Hiemstra

2
return导致非本地跳转,显式非本地跳转对于FP是不常见的。实际上,例如,方案没有return。我认为我的评论太短了(可能不正确)作为答案。
kohske 2012年

2
F#没有returnbreakcontinue或者,这是乏味的时候。
colinfang

Answers:


128

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

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

function( arglist ) expr
return(value)

不打电话回去会更快吗?

这两个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 是清晰设计代码“叶”的好工具,例程应在该“叶”中结束,跳出函数并返回值。

# 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
> 

这取决于程序员的策略和编程风格,他使用哪种风格,因为不需要,他不能使用return()。

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

很多情况下只使用return()(无参数)在有意停止该函数的情况下返回NULL。

目前尚不清楚是否更好还是不如标准用户或使用R的分析师无法看到真正的区别。

我的观点是,问题应该是:使用来自R实现的显式返回是否存在危险?

或者,也许更好的做法是,用户编写功能代码时应该总是问:功能代码使用显式返回(或将要返回的对象作为代码分支的最后一个叶子)会产生什么效果?


4
感谢您的答复。我相信使用不会有危险return,而归结于程序员是否愿意使用它。
保罗·希姆斯特拉

37
速度return确实是您应该担心的最后一件事。
哈德利2014年

2
我认为这是一个错误的答案。原因归结为根本上不同意使用不必要的return函数调用的价值。您应该问的问题不是您最后提出的问题。相反,它是:“为什么使用冗余return?它提供什么好处?” 事实证明,答案是“不多”,甚至“什么都没有”。您的回复对此不满意。
康拉德·鲁道夫

@KonradRudolph ...您实际上重复了Paul最初的要求(为什么明确的回报不好)。我也想知道正确的答案(对任何人来说都是正确的):)。您是否考虑向该网站的用户提供解释?
Petr Matousu

1
@Dason我在其他地方链接了Reddit帖子,解释了为什么这种说法在这种情况下存在缺陷。不幸的是,注释似乎每次都会自动删除。简短的版本return就像是一个明确的注释,在代码旁边显示“将x递增1” x = x + 2。换句话说,它的明确性与(a)完全无关,并且(b)传达了错误的信息。因为returnR中的语义纯粹是“中止此功能”。它并不意味着同return其他语言。
Konrad Rudolph

102

如果每个人都同意

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

我们都应该return在函数结尾处停止使用吗?我当然不会,我想解释原因。我希望听到别人是否也同意我的看法。如果不是对OP的直接回答,而是更长时间的主观评论,我深表歉意。

我不使用的主要问题return是,正如Paul指出的那样,函数主体中可能还有其他需要的地方。并且如果您被迫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微秒的差异并不算什么。我能看到的最后一件事是输入更少,但是,我并不懒。


7
+1,return在某些情况下显然需要该语句,如@flodel所示。另外,在某些情况下,最好省略return语句,例如,许多小的函数调用。在其他所有情况(例如95%)中,是否使用return均不重要,这取决于偏好。我喜欢使用return,因为它在您的意思上更加明确,因此更具可读性。也许这个讨论类似于<-vs =
Paul Hiemstra

7
这将R视为命令性编程语言,而并非如此:它是一种功能性编程语言。函数式编程的工作原理有所不同,并且return返回值与编写if (x == TRUE)而不是相比是荒谬的if (x)
Konrad Rudolph

4
您还重写foofoo <- function(x) if (a) a else b(根据需要使用换行符)。不需要显式的返回值或中间值。
哈德利2014年

26

这是一个有趣的讨论。我认为@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可能无济于事,但引导他们使用实用的风格会有所收获。


@Paul return在命令式中是必需的,因为您经常想在循环中的不同点退出该函数。功能样式不使用循环,因此不需要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是有助于矢量化的功能,这不是问题。


对我来说还不清楚为什么return不适合编程的功能风格。无论是强制性编程还是功能性编程,在某个阶段,函数或子例程都需要返回某些内容。例如,在python中进行函数式编程仍然需要一条return语句。您能否详细说明这一点。
Paul Hiemstra,2012年

在这种情况下,使用ifelse(a,a,b)是我的宠儿。似乎每一行都在?ifelse尖叫:“不要用我代替if (a) {a} else b。” 例如,“ ...返回的值与test“,”的形状相同,“ yes或如果no太短,则其元素被回收。”,“结果的模式可能取决于test“”的值,结果的类属性取自test和可能不适合于从选择的值yesno
GSEE

从第二个角度看,foo没有多大意义。它将始终返回TRUE或b。使用ifelse它将返回1或几个TRUE,和/或1或几个bs。最初,我认为该功能的目的是说“如果某些语句为TRUE,则返回某些内容,否则,返回其他内容”。我不认为应该向量化,因为那将成为“返回一些对象是真实的元素,并为所有不是真正的元素,回归b
GSEE

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)),结果相反...我将在服务器上尝试。


您能否评论这种差异发生的原因?
保罗·希姆斯特拉

4
@PaulHiemstra Petr的答案涵盖了这一主要原因。使用时有两次通话return(),如果没有,则一次通话。在函数的末尾function()返回它的最后一个值是完全多余的。您只会在函数的许多重复中注意到这一点,而在内部却没有做太多的事情,因此花费的成本return()成为函数总计算时间的很大一部分。
加文·辛普森

13

没有明确在最后放置“ return”的问题是,如果在方法的末尾添加其他语句,突然返回值是错误的:

foo <- function() {
    dosomething()
}

这将返回的值dosomething()

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

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

我们希望我们的代码返回的值dosomething(),但不再返回。

有了明确的回报,这变得非常明显:

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

我们可以看到此代码有些奇怪,并对其进行了修复:

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

1
是的,实际上我发现在调试时显式的return()是有用的;一旦代码被清理,它的需求就不那么吸引人了,我更喜欢没有它的优雅……
PatrickT 2014年

但这在实际代码中实际上不是问题,这纯粹是理论上的。可能会遭受此困扰的代码有一个更大的问题:晦涩难懂的代码流不太明显,以至于简单的添加都会破坏它。
Konrad Rudolph'Mar

@KonradRudolph我认为您正在对它做一个不正确的苏格兰人;-)“如果这在您的代码中有问题,那么您是一个糟糕的程序员!”。我真的不同意。我认为,尽管您可以摆脱对一小段代码的捷径,但您会真正了解每一行,但是随着代码的变大,这将再次对您造成困扰。
休·帕金斯

2
@HughPerkins不是一个真正的苏格兰人;相反,它是关于代码复杂性的经验性观察,并得到数十年软件工程最佳实践的支持:保持单个函数简短,代码流显而易见。省略return不是捷径,而是函数式编程中的适当样式。使用不必要的return函数调用是货物崇拜编程的一个实例。
康拉德·鲁道夫

好吧...我不明白这如何阻止您在return声明后添加某些内容,并且没有注意到它不会被执行。您也可以在要返回的值之后添加评论,例如dosomething() # this is my return value, don't add anything after it unless you know goddam well what you are doing
lebatsnok

10

我的问题是:为什么通话return速度不快

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

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

或更好,因此更可取?

因为没有任何理由使用它。

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

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

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

# Add one to the result
result = x + 1

return在R中使用属于同一类别,因为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. 有人认为使用return会增加清晰度,因为它表示“此函数返回值”。但是,如上所述,每个函数都在R中返回一些内容。将其return视为返回值的标记不仅是多余的,而且还很容易引起误解

  2. 与此相关的是,PythonZen具有出色的准则,应始终遵循:

    显式胜于隐式。

    如何删除冗余return不违反此规定?因为函数在函数式语言中的返回值始终是显式的:它是它的最后一个表达式。这再次是关于显式性冗余性的相同论点。

    实际上,如果您需要明确性,可以使用它来突出显示规则的异常:标记返回有意义值的函数,这些函数仅出于副作用而被调用(例如cat)。除了R具有比return这种情况更好的标记:invisible。例如,我会写

    save_results = function (results, file) {
        # … code that writes the results to a file …
        invisible()
    }
  3. 但是长函数呢?难道不知道要退还什么?

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

    但更重要的是,长函数的问题不是缺少显式return标记。它是函数长度。长函数几乎(?)始终违反单一职责原则,即使不这样做,它们也将从拆开可读性中受益。


也许我应该补充一点,有人提倡使用return它使其与其他语言更加相似。但这是一个不好的论点:其他函数式编程语言往往都不使用它们return。只有命令式语言会使用它,而并非每个表达式都有一个值。
康拉德·鲁道夫

我对这个问题的看法是,使用return支持明显更好,并在完全批评的情况下阅读了您的答案。您的回答使我对这一观点进行了反思。我认为需要return显式使用(至少在我自己的情况下)与更好地在以后的时间点修改我的功能有关。考虑到我的函数可能太复杂了,我现在可以看到,改善我的编程风格的目标是努力构造代码以保持显式而不使用return。感谢您的思考和见解!!
Kasper Thystrup Karstensen

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")
   }

通常,需要多次退货表明该问题要么丑陋要么结构不良.g

<>

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)

今天,我发现了一个实际可能需要的情况return(上面的丑陋示例是高度虚假的):假设您需要测试一个值是否为NULLNA:在这些情况下,返回一个空字符串,否则返回该character值。但是对的测试is.na(NULL)会产生错误,因此看来只能先执行if(is.null(x)) return(""),然后再继续if(is.na(x)) .....。(一个人可以length(x)==0代替,is.null(x)但仍然可以使用,length(x)==0 | is.na(x)如果x是,则不能使用NULL。)
lebatsnok

1
这是因为您使用了|(对双方都进行求值的矢量化或)而不是||(短路或,而不是对谓词进行求值的矢量化)。考虑if (TRUE | stop()) print(1)if (TRUE || stop()) print(1)
asac

2

return 可以提高代码的可读性:

foo <- function() {
    if (a) return(a)       
    b     
}

3
也许可以。但这在您的示例中没有做到。相反,它使代码流变得模糊(或更复杂)。
Konrad Rudolph'Mar

1
您的函数可以简化为:(foo <- function() a || b这是IMO更具可读性;在任何情况下,都没有“纯”可读性,但在某些人看来可读性:有人说汇编语言是完全可读的)
lebatsnok

1

冗余的论点在这里已经很多了。我认为这是没有足够的理由忽略的return()。冗余并不是一件坏事。如果战略性地使用冗余,则可使代码更清晰,更可维护。

考虑以下示例:函数参数通常具有默认值。因此,指定与默认值相同的值是多余的。除非它使我预期的行为显而易见。无需调出功能手册页即可提醒自己默认值是什么。不用担心该功能的未来版本会更改其默认值。

通话的性能损失可忽略不计return()(根据其他人在此处发布的基准),它归结为风格,而不是对与错。对于某些“错误”,必须存在明显的不利条件,并且这里没有人令人满意地证明包含或省略return()具有一致的不利条件。似乎是非常特定于案例和特定于用户的。

所以这就是我坚持的立场。

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

我对上面的示例中的“孤立”变量感到不舒服。当时abcd将是我没写完一份声明中的一部分?它是我的代码中拼接/编辑的残余,需要删除吗?我是否偶然从其他地方粘贴/移动了某些东西?

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

相比之下,第二个示例对我来说显然是一个预期的返回值,而不是某些意外或不完整的代码。对我而言,这种冗余绝对不是没有用的。

当然,一旦函数完成并正常工作,我就可以删除返回值。但是删除它本身是多余的多余步骤,而且我认为比起一开始就没有更多用处return()

综上所述,我不return()简短地使用未命名的单线函数。在那里,它占了函数代码的很大一部分,因此大多数情况下会导致视觉混乱,从而使代码难以辨认。但是对于较大的正式定义和命名的函数,我将使用它,并且可能会继续这样做。


“ abcd会成为我还没写完的声明的一部分吗?” —但是,这与您编写的任何其他表达式有何不同?我认为这是我们分歧的核心。在命令式编程语言中,独自拥有一个变量可能是特殊的,但是在函数式编程语言中,这是完全正常且期望的。我声称,问题仅仅是因为您对函数式编程不熟悉(您所说的是“语句”而不是“表达式”的事实加强了这一点)。
Konrad Rudolph

这是不同的,因为其他所有语句通常都以更明显的方式执行某些操作:这是一个赋值,一个比较,一个函数调用...是的,我的第一个编码步骤是使用命令式语言,而我仍然使用命令式语言。跨语言(只要语言允许)的统一视觉提示使我的工作更加轻松。return()R中的A不花费任何费用。它在客观上是多余的,但是“无用”是您的主观判断。冗余和无用不一定是同义词。那就是我们不同意的地方。
cymon

另外,我不是软件工程师,也不是计算机科学家。不要对我的术语使用太多细微差别。
cymon

需要澄清的是:“冗余和无用不一定是同义词。那就是我们不同意的地方。” —不,我完全同意这一点,我在回答中明确指出了这一点。冗余可能会有所帮助,甚至至关重要。但这需要积极展示,而不是假设。我理解您的论点,以解释为什么您认为这是对的return,即使我不确定我认为这是有可能的(它绝对是一种命令式语言,但我相信它不会转换为功能语言)。
康拉德·鲁道夫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.