Ruby中的条件链接

24
有没有一种好的方法可以在Ruby中有条件地链接方法?
我想要的功能是:
if a && b && c
 my_object.some_method_because_of_a.some_method_because_of_b.some_method_because_of_c
elsif a && b && !c
 my_object.some_method_because_of_a.some_method_because_of_b
elsif a && !b && c
 my_object.some_method_because_of_a.some_method_because_of_c

etc...

根据多种条件,我想确定在方法链中调用哪些方法。

到目前为止,我最好的尝试是有条件地构建方法字符串,并使用 eval,但肯定有更好的、更符合 Ruby 风格的方法吧?


2
我想知道为什么更多的人对条件链接不感兴趣。它可以大大简化代码。 - Kelvin
10个回答

36
您可以将方法放入一个数组中,然后执行数组中的所有方法。
l= []
l << :method_a if a
l << :method_b if b
l << :method_c if c

l.inject(object) { |obj, method| obj.send(method) }

Object#send 方法执行指定名称的方法。 Enumerable#inject 在迭代数组时,将上次迭代返回的值和当前数组项传递给块。

如果你想让你的方法接收参数,也可以这样做。

l= []
l << [:method_a, arg_a1, arg_a2] if a
l << [:method_b, arg_b1] if b
l << [:method_c, arg_c1, arg_c2, arg_c3] if c

l.inject(object) { |obj, method_and_args| obj.send(*method_and_args) }

虽然如此,如果这些方法需要带参数,我能使用它吗? - DanSingerman
2
我认为这不会起作用,因为 obj.send 的结果替换了循环中的累加器,那么在下一次运行时,它可能就不是一个有效的对象来发送请求的方法。简单的解决方法:明确返回“obj”。 - hurikhan77
好的回答。不过我会这样写这个赋值语句:l = [(:method_a if a), (:method_b if b), (:method_c if c)].compact。 - tokland

13

您可以使用tap

my_object.tap{|o|o.method_a if a}.tap{|o|o.method_b if b}.tap{|o|o.method_c if c}

这是Rails,而不是纯粹的Ruby,对吧? - DanSingerman
1
实际上,Rails使用returning,而tap来自纯Ruby 1.8.7和1.9。 - MBO
太棒了 - 我认为这是实现我想要的最好方法。而且在1.8.6中,你可以轻松地猴子补丁它来定义tap方法(我刚刚尝试过,似乎工作得很好)。 - DanSingerman
已经撤回了正确答案,因为我认为在一些新代码中尝试后,它实际上并不起作用。 - DanSingerman
2
@DanSingerman 是的,如果你的方法返回不同的对象而不是在内部修改 my_object,它就不起作用。我本应该写下来的,但我看到了你的评论,认为这正是你想要的。 - MBO
显示剩余3条评论

5

这是一个示例类,以演示可以进行方法链式调用而不会修改调用方实例的拷贝实例。这可能是您应用程序所需的库。

class Foo
  attr_accessor :field
    def initialize
      @field=[]
    end
    def dup
      # Note: objects in @field aren't dup'ed!
      super.tap{|e| e.field=e.field.dup }
    end
    def a
      dup.tap{|e| e.field << :a }
    end
    def b
      dup.tap{|e| e.field << :b }
    end
    def c
      dup.tap{|e| e.field << :c }
    end
end

猴子补丁:这是你想要添加到你的应用程序中以启用条件链的内容。
class Object
  # passes self to block and returns result of block.
  # More cumbersome to call than #chain_if, but useful if you want to put
  # complex conditions in the block, or call a different method when your cond is false.
  def chain_block(&block)
    yield self
  end
  # passes self to block
  # bool:
  # if false, returns caller without executing block.
  # if true, return result of block.
  # Useful if your condition is simple, and you want to merely pass along the previous caller in the chain if false.
  def chain_if(bool, &block)
    bool ? yield(self) : self
  end
end

示例用法

# sample usage: chain_block
>> cond_a, cond_b, cond_c = true, false, true
>> f.chain_block{|e| cond_a ? e.a : e }.chain_block{|e| cond_b ? e.b : e }.chain_block{|e| cond_c ? e.c : e }
=> #<Foo:0x007fe71027ab60 @field=[:a, :c]>
# sample usage: chain_if
>> cond_a, cond_b, cond_c = false, true, false
>> f.chain_if(cond_a, &:a).chain_if(cond_b, &:b).chain_if(cond_c, &:c)
=> #<Foo:0x007fe7106a7e90 @field=[:b]>

# The chain_if call can also allow args
>> obj.chain_if(cond) {|e| e.argified_method(args) }

有趣的东西! - Saim
我还想到了 #chain_if,当条件适用于Rails作用域链时非常方便。 - Epigene
我来这里写这个解决方案,但你比我先完成了!我把我的称为 tap? 而不是 chain_if。非常好! - baka-san

4
尽管inject方法是完全有效的,但这种Enumerable用法会让人感到困惑,并且受到无法传递任意参数的限制。
对于这种应用程序,以下模式可能更好:
object = my_object

if (a)
  object = object.method_a(:arg_a)
end

if (b)
  object = object.method_b
end

if (c)
  object = object.method_c('arg_c1', 'arg_c2')
end

我发现在使用命名作用域时这很有用。例如:

scope = Person

if (params[:filter_by_age])
  scope = scope.in_age_group(params[:filter_by_age])
end

if (params[:country])
  scope = scope.in_country(params[:country])
end

# Usually a will_paginate-type call is made here, too
@people = scope.all

过滤作用域正是我遇到这个问题的使用情况。 - DanSingerman
1
要直接将参数应用于条件,以下代码片段可能会有用:Person.all(:conditions => params.slice(:country,:age)) - hurikhan77

4

使用#yield_self或自Ruby 2.6以来的#then!

my_object.
  then{ |o| a ? o.some_method_because_of_a : o }.
  then{ |o| b ? o.some_method_because_of_b : o }.
  then{ |o| c ? o.some_method_because_of_c : o }

太棒了,我终于可以像在 Laravel 中一样进行查询了https://laravel.com/docs/5.6/queries#conditional-clauses,但这甚至更加简单明了。 - Jonathan

3
这里有一种更加功能化的编程方式。
使用 break 以便让 tap() 返回结果。(tap 仅在 Rails 中存在,正如其他答案中所提到的)。
'hey'.tap{ |x| x + " what's" if true }
     .tap{ |x| x + "noooooo" if false }
     .tap{ |x| x + ' up' if true }
# => "hey"

'hey'.tap{ |x| break x + " what's" if true }
     .tap{ |x| break x + "noooooo" if false }
     .tap{ |x| break x + ' up' if true }
# => "hey what's up"

如果它能正常工作,那就很好 - 从2.6版本开始可以使用then {} - Jonathan

1

我使用这个模式:

class A
  def some_method_because_of_a
     ...
     return self
  end

  def some_method_because_of_b
     ...
     return self
  end
end

a = A.new
a.some_method_because_of_a().some_method_because_of_b()

我真的不明白这有什么帮助。你能详细说明一下吗? - DanSingerman
我改变了我的示例来阐述我的想法。或者我只是没有理解你的问题,你想动态构建方法列表吗? - ceth
Demas可能意味着您应该将if a ...测试放在some_method_because_of_a内部,然后只需调用整个链并让方法决定要做什么。 - glenn jackman
这并不够通用。例如,如果链中的某些方法是本地 Ruby 方法,我不想为了这一个使用案例而对它们进行猴子补丁。 - DanSingerman

1
如果您正在使用Rails,可以使用#try。而不是
foo ? (foo.bar ? foo.bar.baz : nil) : nil

写入:

foo.try(:bar).try(:baz)

或者,带有参数:
foo.try(:bar, arg: 3).try(:baz)

在原始的Ruby中未定义,但它并不需要太多代码

我会为CoffeeScript的?.运算符付出很多。


2
我知道这是一个旧问题的旧答案,但是自 Ruby 2.3 起,Ruby 确实有了等效的“安全导航”运算符!它是 &.(请参阅 Ruby 主干中的此功能问题:https://bugs.ruby-lang.org/issues/11537) - wspurgin

1

也许你的情况比这更复杂,但为什么不试试:

my_object.method_a if a
my_object.method_b if b
my_object.method_c if c

my_object.method_a.method_b 不等同于 my_object.method_a 和 my_object.method_b。 - DanSingerman
啊,我想我更多地考虑的是my_object.method_a!,等等。 - Alison R.

0
我最终写了以下内容:
class Object

  # A naïve Either implementation.
  # Allows for chainable conditions.
  # (a -> Bool), Symbol, Symbol, ...Any -> Any
  def either(pred, left, right, *args)

    cond = case pred
           when Symbol
             self.send(pred)
           when Proc
             pred.call
           else
             pred
           end

    if cond
      self.send right, *args
    else
      self.send left
    end
  end

  # The up-coming identity method...
  def itself
    self
  end
end


a = []
# => []
a.either(:empty?, :itself, :push, 1)
# => [1]
a.either(:empty?, :itself, :push, 1)
# => [1]
a.either(true, :itself, :push, 2)
# => [1, 2]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接