Ruby中块的使用顺序是什么

What is the order of using blocks in Ruby

我正在创建一个 gem 来支持一些来自命令行的邮件。我用了一些Gem。 我正在使用 Mail Gem。正如您在 mail gem 的描述中看到的那样。

mail = Mail.new do
  from    'mikel@test.lindsaar.net'
  to      'you@test.lindsaar.net'
  subject 'This is a test email'
  body    File.read('body.txt')
end

在块中,我从 Mail class(从、到、主题、正文)中调用方法。这是有道理的,所以我在自己的邮件程序中构建它 class

def initialize(mail_settings, working_hours)
  @mail_settings = mail_settings
  @working_hours = working_hours
  @mailer = Mail.new do
    to mail_settings[:to]
    from mail_settings[:from]
    subject mail_settings[:subject]
    body "Start #{working_hours[:start]} \n\
          Ende #{working_hours[:end]}\n\
          Pause #{working_hours[:pause]}"
  end
end

这看起来很简单。只需调用块并填写我通过构造函数获得的值。 现在 我的问题来了。

我试图将邮件的正文构造放到一个单独的方法中。但是我不能在 gem 的 Mail 构造函数中使用它。

module BossMailer
  class Mailer
  def initialize(mail_settings, working_hours)
    @mail_settings = mail_settings
    @working_hours = working_hours
    @mailer = Mail.new do
      to mail_settings[:to]
      from mail_settings[:from]
      subject mail_settings[:subject]
      body mail_body
    end
  end

  def mail
    @mailer.delivery_method :smtp, address: "localhost", port: 1025
    @mailer.deliver
  end

  def mail_body
    "Start #{working_hours[:start]} \n\
    Ende #{working_hours[:end]}\n\
    Pause #{working_hours[:pause]}"
  end
end

结束

此代码出现此错误。

这意味着我无法在此块中使用我的 class 方法或 class 变量(以 @a 开头)。

问题

块中的执行顺序是什么?如果我设置我的变量 @mail_settings,我就不能在块中使用它。 Ruby 是在 Mail class 中搜索 @mail_settings 吗?为什么我可以通过块使用 BossMailer::Mailer 构造函数中的给定参数而不出现错误?

如果我使用和变量将内容解析到块中,为什么这会起作用? (body_content = mail_body) 有效!

def initialize(mail_settings, working_hours)
  @mail_settings = mail_settings
  @working_hours = working_hours
  body_content = mail_body
  @mailer = Mail.new do
    to mail_settings[:to]
    from mail_settings[:from]
    subject mail_settings[:subject]
    body body_content
  end
end

一切都与上下文有关。

mail = Mail.new do
  from    'mikel@test.lindsaar.net'
  to      'you@test.lindsaar.net'
  subject 'This is a test email'
  body    File.read('body.txt')
end

fromto 方法(以及其余)是 Mail::Message instance. For you to be able to call them in this nice DSL-manner, the block you pass to constructor is instance_eval'ed.

上的方法

这意味着在此块内部,self 不再是您的邮件程序,而是一封邮件。因此,无法访问您的邮件程序方法。

而不是 instance_eval,他们可以只有 yieldblock.call,但这不会使 DSL 成为可能。

至于为什么局部变量有效:这是因为 ruby 块是词法范围的 closures (意思是,它们保留了声明的局部上下文。如果有从定义块的地方可见的局部变量,它会在调用块时记住变量及其值)

替代方法

不要使用块形式。使用这个:https://github.com/mikel/mail/blob/0f9393bb3ef1344aa76d6dac28db3a4934c65087/lib/mail/message.rb#L92-L96

mail = Mail.new
mail['from'] = 'mikel@test.lindsaar.net'
mail[:to]    = 'you@test.lindsaar.net'
mail.subject 'This is a test email'
mail.body    = 'This is a body'

代码

尝试 commenting/uncommenting 一些行。

class Mail
  def initialize(&block)
    # block.call(self) # breaks DSL
    instance_eval(&block) # disconnects methods of mailer
  end

  def to(email)
    puts "sending to #{email}"
  end

end

class Mailer
  def admin_mail
    # get_recipient = 'vasya@example.com'
    Mail.new do
      to get_recipient
    end
  end

  def get_recipient
    'sergio@example.com'
  end
end


Mailer.new.admin_mail

问题是 mail_body 是在 Mail::Message 的上下文中计算的,而不是在 BossMailer::Mailer class 的上下文中计算的。考虑以下示例:

class A
  def initialize
    yield
  end
end

class B
  def initialize(&block)
    instance_eval { block.call }
  end
end

class C
  def initialize(&block)
    instance_eval(&block)
  end
end

class Caller
  def test
    A.new { hi 'a' }
    B.new { hi 'b' }
    C.new { hi 'c' }
  end

  def hi(x)
    puts "hi there, #{x}"
  end
end

Caller.new.test

这会让你

hi there, a
hi there, b
`block in test': undefined method `hi' for #<C:0x286e1c8> (NoMethodError)

查看 gem 的代码,这正是发生的事情:

Mail.new just passes the block given to Mail::Message's constructor.

The said constructor works exactly as the C case above


instance_eval 基本上改变了当前上下文中 self 的内容。

关于为什么 BC 情况不同 - 你可以认为 & 将 'change' block 对象从 proc 到块(是的,我在那里选择的变量名不是很好)。更多关于区别 here.