【问题标题】:How to write a nested search the Ruby way?如何用 Ruby 方式编写嵌套搜索?
【发布时间】:2014-02-22 19:00:48
【问题描述】:

我正在尝试编写快速简洁的代码。我很感激您对编写以下代码的最佳方式以及原因的想法:

选项#1

def get_title
  title = check_in_place_one
  if title.empty?
    title = check_in_place_two
    if title.empty?
      title = check_in_place_three
    end
  end
  return title
end

选项#2

def get_title
  title = check_in_place_one
  title = check_in_place_two unless !title.empty?
  title = check_in_place_three unless !title.empty?
  return title
end

我认为选项#1 更好,因为如果check_in_place_one 找到title,我们测试一次title.empty?,然后跳过方法中的其余代码并返回。但是,它看起来太长了。选项#2 看起来更好,但处理title.empty? 一次额外时间,并且在返回之前有不必要的时间。另外,我错过了第三种选择吗?

【问题讨论】:

  • 除了你的主要观点之外,双重否定unless !... 是不好的,无论是可读性还是性能。
  • @sawa 从可读性角度还是其他原因?
  • @izmangyo 主要是可读性。 unless !... 是双重否定的。 if ... 更好。
  • check_in_place... 是方法还是变量?虽然在方法中不使用括号是可以的,但在您的代码中很难分辨。
  • @theTinMan check_in_place.. 是方法。它们不带任何参数,因为它们都在一个公共实例变量上工作。

标签: ruby coding-style


【解决方案1】:

从性能来看,您的代码的两个版本之间没有区别(除了可能来自解析的非常微小的差异,应该可以忽略不计)。控制结构相同。

从可读性来看,如果您可以摆脱嵌套,那么这样做会更好。你的第二个选择更好。

通常最好去掉不需要进一步处理的任何情况。这是由return 完成的。

def get_title
  title = check_in_place_one
  return title unless title.empty?
  title = check_in_place_two
  return title unless title.empty?
  title = check_in_place_three
  return title
end

上面代码中最后的title =return 是多余的,但我把它们放在那里是为了保持一致性,这样可以提高可读性。

您可以像这样使用tap 进一步压缩代码:

def get_title
  check_in_place_one.tap{|s| return s unless s.empty?}
  check_in_place_two.tap{|s| return s unless s.empty?}
  check_in_place_three
end

tap 是一种非常快的方法,与instance_eval 不同,它的性能损失通常可以忽略不计。

【讨论】:

  • 我不知道tap。从我正在阅读的内容来看,它似乎对于处理方法链的中间结果很有用。但看起来你纯粹是为了紧凑而使用它。是对的吗?谢谢!
【解决方案2】:

以下方法可用于任意数量的顺序测试。而且,它是完全通用的。可以更改返回条件,可以轻松地将参数分配给测试方法等。

tests = %w[check_in_place_one check_in_place_two check_in_place_three]

def do_tests(tests)
  title = nil # Define title outside block
  tests.each do |t|
    title = send(t)
    break unless title.empty?
  end
  title
end

让我们试试吧:

def check_in_place_one
  puts "check 1"
  []
end

def check_in_place_two
  puts "check 2"
  ''
end

def check_in_place_three
  puts "check 3"
  [3]
end

do_tests(tests) #=> [3]
check 1
check 2
check 3
  #=> [3]

现在更改其中一项测试:

def check_in_place_two
  puts "check 2"
  'cat'
end

do_tests(tests) #=> 'cat'
check 1
check 2
  #=> "cat"         

如果有更多测试,将它们放在一个模块中可能会很方便,该模块将 included 放入一个类中。混合方法的行为与您为类定义的方法相同。例如,他们可以访问实例变量。我将通过第一个测试方法的定义来证明这一点。我们可能希望将测试方法设为私有。我们可以这样做:

module TestMethods
  private

  def check_in_place_one
    puts "@pet => #{@pet}"
    puts "check 1"
    []
  end

  def check_in_place_two
    puts "check 2"
    ''
  end

  def check_in_place_three
    puts "check 3"
    [3]
  end
end

class MyClass  
  @@tests = TestMethods.private_instance_methods(false)
  puts "@@tests = #{@@tests}"

  def initialize
    @pet = 'dog'
  end

  def do_tests
    title = nil # Define title outside block
    @@tests.each do |t|
      title = send(t)
      break unless title.empty?
    end
    title
  end   

  include TestMethods       
end

解析代码时显示如下:

@@tests = [:check_in_place_one, :check_in_place_two, :check_in_place_three]

现在我们进行测试:

MyClass.new.do_tests #=> [3]
@pet => dog
check 1
check 2
check 3 

确认测试方法是私有的:

MyClass.new.check_in_place_one
  #=> private method 'check_in_place_one' called for...'

使用模块的好处是您可以添加、删除、重新排列和重命名测试方法,而无需对类进行任何更改。

【讨论】:

  • 我在课堂上将“测试方法”作为私有方法,但我觉得它们已经失控了。我喜欢将它们放在单独的模块中的建议,但如果我这样做并将模块包含在类中(如您上面的代码所示),它们是否仍然可以访问类的实例变量?谢谢!
  • iz,我进行了编辑以解决您的问题,并展示了如何将测试方法设为私有。随时提出其他问题或提出改进建议。
【解决方案3】:

嗯,这里有一些其他的选择。

选项 1:返回第一个非空支票。

def get_title
  return check_in_place_one   unless check_in_place_one.empty?
  return check_in_place_two   unless check_in_place_two.empty?
  return check_in_place_three
end

选项 2: 具有短路评估的辅助方法。

def get_title
  check_place("one") || check_place("two") || check_place("three")
end

private

def check_place(place)
  result = send("check_in_place_#{place}")
  result.empty? ? nil : result
end

选项 3:检查所有位置,然后找到第一个非空的位置。

def get_title
  [
    check_in_place_one,
    check_in_place_two,
    check_in_place_three,
  ].find{|x| !x.empty? }
end

【讨论】:

  • 在选项 1 中,您至少调用每个方法两次,因此它不是最佳选择。选项 2 很好,并且在一条线上得到了要点。选项 3 可以通过替换 .find 方法和 block 来缩短 .compact 假设这些检查方法将返回 nils 而不是空字符串,但它仍然始终调用所有方法而不会像选项 2 那样短路。因此,您的选项 2 是最短且最佳的。
  • @diego.greyrobot 调用每个方法两次可能会或可能不会有问题,具体取决于这些方法到底是什么。如果它们只是普通的旧 getter 方法,那应该不是问题。哦,检查方法确实返回空字符串;那就是问题所在。否则,如果没有辅助方法,选项 2 也可以正常工作。
  • @ajedi32 选项 2 可能适用于我的情况。这些方法做复杂的事情,如果不成功则返回空数组,所以我不想调用它们两次。我不知道send 方法。如果我按照上面 Cary Swoveland 的建议进行操作并将这些方法存储在一个单独的模块中并将它们包含在我的类中,那么您使用 send 的辅助方法是否仍然可以像上面那样工作?谢谢!
【解决方案4】:

选项 2 看起来不错,尽管您使用 unless !title.empty? 进行了 360 度转弯。您可以将其缩短为 if title.empty?,因为 unless 等同于 if !,因此执行 unless ! 会将您返回到 if

如果您只需要查看 3 个地方,那么选项 2 是最好的。它简短、简洁且易于阅读(一旦你修复了前面提到的 whoopsie 就更容易了)。如果您可以添加到您寻找标题的地方,您可以获得更多动态:

def get_title(num_places = 4)
  title, cur_place = nil, 0
  title = check_in_place(cur_place += 1) while title.nil? && cur_place < num_places
end

def check_in_place(place_num)
  # some code to check in the place # given by place_num
end

棘手的行是其中包含while 的行。发生的情况是while 将检查表达式title.nil? &amp;&amp; cur_place &lt; num_places 并返回true,因为title 仍然为零且0 小于4。

然后我们将调用check_in_place 方法并传入一个值 1,因为cur_place += 1 表达式会将其值增加到 1 然后将其返回给该方法(假设我们要开始签入位置 1,当然)。

这将重复,直到 check_in_place 返回一个非 nil 值,或者我们用完了可以检查的地方。

现在get_title 方法更短,并且将自动支持检查num_places 位置,因为您的check_in_place 方法也可以查看更多位置。

还有一件事,你不妨给https://codereview.stackexchange.com/看一下,这个问题似乎很适合它。

【讨论】:

  • 这可能适用于与我的情况略有不同的情况。在我的例子中,在代码中拥有方法的实际名称很重要,因为它们是描述性的,可以向代码阅读器准确解释正在检查的内容。感谢您使用 unless 解释我的错误并推荐 Code Review 网站——它看起来非常适合我的需求!
【解决方案5】:

我认为没有任何理由变得太聪明:

def get_title
  check_in_place_one || check_in_place_two || check_in_place_three
end

编辑:如果 check_in_place_X 方法确实在失败时返回一个空字符串,那么让它们返回nil 会更好(而且更惯用)。它不仅允许像上面的代码那样进行真实的比较,return "" 还会导致构造一个新的不必要的 String 对象。

【讨论】:

  • 这行不通。 OP 在返回值上调用 .empty? 的事实意味着,当没有找到值时,它们返回空字符串,而不是 nil。还有!!"" == true
猜你喜欢
  • 1970-01-01
  • 2023-04-03
  • 2013-07-15
  • 1970-01-01
  • 1970-01-01
  • 2013-02-23
  • 1970-01-01
  • 2017-10-13
  • 1970-01-01
相关资源
最近更新 更多