避免在Ruby中更改方法参数的良好实践?

时间:2018-06-28 19:54:29

标签: ruby

我知道避免在Ruby中更改方法参数是一种很好的做法(除非专门用于此目的)。

关于如何管理此代码的简单的经验法则是什么(在样式,命名约定等方面)?

例如,这是一种好习惯吗?

def array_of_three(a, b, c)
   d = a.dup
   e = b.dup
   f = c.dup
   # Do work on d, e and f
   return [d, e, f]
end

我该如何命名方法参数与函数中可以更改的重复项? (假设类似上述方法是“正确的”!)

谢谢。

3 个答案:

答案 0 :(得分:3)

Ruby的“良好实践”通常可以归结为不变异您不拥有的数据。问题在于所有权可能有些模糊,因此如果有疑问,您将假定您拥有数据并制作副本如果,则需要进行突变它。

不良行为的表现方式是这样的一个例子:

a = [ 1, 2, 3 ]

do_stuff(a) # Works
do_stuff(a) # Doesn't do anything for some reason

a
# => [ ]    # Hey! Who did that?

假设do_stuff是一个混蛋,是这样做的:

def do_stuff(a)
  while (v = a.pop)
    puts a
  end
end

那破坏了给您的参数,这很糟糕。相反,您应该采用非破坏性方法:

def do_stuff(a)
  a.each do |v|
    puts v
  end
end

或者如果有必要操纵该结构,则制作一个副本,例如:

def do_stuff(a)
  a = a.uniq.sort
  while (v = a.pop)
    puts v
  end
end

测试方法是否正常的一种好方法是在测试中提供冻结数据:

a = [ 1, 2, 3 ].freeze

do_stuff(a) # Fails trying to manipulate frozen object

我倾向于使用激进的冻结数据(例如deep_freeze)编写单元测试,以确保参数不能被修改。这真的很快消除了所有权问题。

如果您的函数具有这样的方法签名:

def do_stuff(*a, **b)
  # ...
end

然后,您将同时拥有a(变量)和b(变量),因为它们是专门针对您的方法而创建的。在所有其他情况下,您需要小心。在这里,按合同编程很重要,因为您的方法的调用者必须知道要移交哪些参数,哪些不移交。文档必须明确这一点。

答案 1 :(得分:2)

这更多是对其他答案的扩展注释,而不是其本身的答案,但是,如果您要对自己不拥有的数据进行变异,则方法名称应以{{ 1}}。对此没有花哨的语法。 !只是终止方法的有效字符。例如,看下面的代码:

!

这将输出a = [1, 2, 2, 3, 3, 3] puts a.uniq puts a ,然后输出[1, 2, 3],如您所愿。另一方面,此代码:

[1, 2, 2, 3, 3, 3]

将输出两次a = [1, 2, 2, 3, 3, 3] puts a.uniq! puts a ,因为它就地修改了[1, 2, 3]。如果有两件事是对的,那就特别好:

  1. 您同样可以有效地使用该函数的一个非变异和变异版本
    • 例如a不太适合这种模式,因为您如何在不变异的情况下弹出?
  2. 两者都有一定的合理需求。
    • 这有点模糊,但是对于表示对某些数据进行操作的事物来说,通常是正确的-您可能希望它既能够对数据进行操作,又不会对其进行更改,并且对其进行更改出于效率考虑或只是避免自我分配或临时变量。

这不是一成不变的规则,但是它是采用的一种不错的样式,因为它使人们一眼就可以看出事物已被突变。

请注意,如果pop已经是所有唯一元素(例如a),则a = [4, 5, 6]将返回uniq!,这是标准的用于最终不会执行修改的就地修改功能。我建议,如果您采用nil样式,那么也应采用这种样式,以避免与人们对!功能如何工作的心理模型进行比较时产生混淆。

答案 2 :(得分:1)

我知道这是基于我的观点的答案,但这是我在ruby中“套用”参数的首选方式(不求助于.dup之类)是将其传递给函数,该函数通过一个lambda的参数,该参数在适当位置求值:

def argument_trap(*args)
  lambda { args }.call
end

def array_of_three(a, b, c)
  a, b, c = argument_trap(a, b, c)
  a += 1
  b -= 1
  c *= 2
  [a, b, c]
end

def array_of_n(*args)
  args = argument_trap(*args)
  args.map { |arg| arg += arg }
end

a, b, c, d, e, f = 1, 2, 3, 4, 5, 6

three = array_of_three(a, b, c)
n = array_of_n(a, b, c, d, e, f)

p [a, b, c]
p three
puts
p [a, b, c, d, e, f]
p n

给予

[1, 2, 3]
[2, 1, 6]

[1, 2, 3, 4, 5, 6]
[2, 4, 6, 8, 10, 12]

Online demo here

使用这种模式,在第一条函数主体行上很明显,参数名称现在指向克隆。但这又是一个优先事项-我喜欢保留原始参数名称,而不是命名它们为dupA或类似名称,因为对我来说,很明显,我现在在这些函数中正在处理重复项。 / p>

这也阻止了同时执行以下功能:使用对原始值的引用和重复项,这在我眼中是一个优势,因为同时执行会导致很多代码混乱。另一方面,一个人可能会忘记一两个参数,并且再次陷入地狱。.