【问题标题】:conditional chaining in rubyruby 中的条件链接
【发布时间】:2009-11-25 14:18:44
【问题描述】:

有没有在 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 chaining


【解决方案1】:

你可以把你的方法放入一个数组中,然后执行这个数组中的所有内容

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

【讨论】:

  • 不过,如果方法需要带参数,我可以使用它吗?
  • 我认为这不会起作用,因为 obj.send 的结果替换了循环中的累加器,这可能不是在下次运行时将请求的方法发送到的有效对象。简单的解决方法:显式返回“obj”。
  • 好答案。不过,我会这样写作业: l = [(:method_a if a), (:method_b if b), (:method_c if c)].compact
【解决方案2】:

你可以使用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,而不是 vanilla ruby​​,不是吗?
  • 其实rails使用returningtap是从纯Ruby 1.8.7和1.9而来的
  • 很棒 - 我认为这是实现我想要的最佳方式。另外在 1.8.6 中,您可以轻松地对其进行修补以定义 tap 方法(我刚刚尝试过,并且似乎工作正常)
  • 已撤回正确答案,因为我认为这实际上不起作用,现在我已经在一些新代码中尝试过
  • @DanSingerman 是的,如果您的方法返回不同的对象而不是在内部修改my_object,它将不起作用。我应该写的,但我得到了你的评论,那就是你想要的
【解决方案3】:

虽然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

【讨论】:

  • 对范围进行过滤正是我遇到此问题的用例。
  • 要将参数直接应用于条件,以下 sn-p 可能很有用:Person.all(:conditions => params.slice(:country, :age))
【解决方案4】:

示例类来演示返回复制实例而不修改调用者的链接方法。 这可能是您的应用所需的库。

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

monkeypatch:这是您要添加到应用中以启用条件链接的内容

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

【讨论】:

  • 有趣的东西!
  • 我还提出了#chain_if,当条件适用于 Rails 范围链时会派上用场。
【解决方案5】:

使用#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 }

【讨论】:

    【解决方案6】:

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

    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
    • 啊。我想我在 my_object.method_a! 等方面考虑得更多。
    【解决方案7】:

    我使用这种模式:

    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()
    

    【讨论】:

    • 我真的不明白这有什么帮助。可以扩展一下吗?
    • 我改变了我的例子来说明我的想法。或者我只是不明白你的问题,你想动态构建方法列表?
    • Demas 可能打算暗示您应该将 if a ... 测试放在 some_method_because_of_a 中,然后调用整个链并让方法决定要做什么
    • 这还不够通用。例如如果链中的某些方法是原生 ruby​​ 方法,我真的不想为这个用例修补它们。
    【解决方案8】:

    如果您使用的是 Rails,则可以使用 #try。而不是

    foo ? (foo.bar ? foo.bar.baz : nil) : nil
    

    写:

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

    或者,带参数:

    foo.try(:bar, arg: 3).try(:baz)
    

    在 vanilla ruby​​ 中没有定义,但它是 isn't a lot of code

    对于 CoffeeScript 的 ?. 运算符,我不会给出什么。

    【讨论】:

    • 我知道这是一个老问题的老答案,但是从 Ruby 2.3 开始,Ruby 现在确实有一个等效的“安全导航”运算符!它是&amp;.(在 Ruby 主干中引用此功能问题:bugs.ruby-lang.org/issues/11537
    【解决方案9】:

    这是一种更实用的编程方式。

    使用break 以获取tap() 以返回结果。 (如另一个答案中所述,点击仅在导轨中)

    '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"
    

    【讨论】:

      【解决方案10】:

      我最终写了以下内容:

      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]
      

      【讨论】:

        猜你喜欢
        • 2017-09-08
        • 2017-12-08
        • 2011-09-28
        • 1970-01-01
        • 2021-01-27
        • 1970-01-01
        • 2015-08-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多