Ruby 在 block 中怎么访问到类的 attr_accessor ?

tablecell · 2021年09月27日 · 最后由 illusove 回复于 2021年10月04日 · 802 次阅读

app.rb

require "rack"

handler = Rack::Handler::WEBrick

class Base
  attr_accessor :route
  attr_accessor :req

  class << self
    def route
      @route ||= Hash["get" => {}]
    end

    def request
      @req
    end

    def newrequest (env)  # 这个 env 参数只能从 rackapp 的 call 中才能获取 
      @req = Rack::Request.new(env)
      puts @req.params
    end

    def get(path, &block)
      Base.route["get"][path] = block
    end
  end
end

class MyApp < Base
  get "/" do
    print request.inspect
    "access request"
  end
end

class RackApp
  def call(env)
    # req = Rack::Request.new(env)
    path = env["PATH_INFO"]
    method = env["REQUEST_METHOD"].downcase
    Base.newrequest env
    cb = Base.route[method][path]
    resp = cb.call
    [200, { "Content-Type" => "text/html" }, [resp]]
  end
end

handler.run RackApp.new

ruby app.rb

http://localhost:8080/?ruby=rails

self.newquest 里面 可以打印参数 self.request 中打印参数是 nil

attr_accessor 宏是一个魔法,
attr_accessor req 会给你定义 写入req= 以及 读取this.req 方法,
实际上你已经在后续代码使用了,而你在调用self.request时并未先写入req的值,所以打印出来是nil
req想象成一个小柜子的话,你没有事先存放物品在里面,所以你从柜子里拿不到任何东西

LiinNs 回复

attr_accessor 的用法是针对实例方法的,如果 Base 不想生成实例,只用类方法实现(写在 class<< self 中) req=的 setter 要带 env 参数 这个 env 参数只能从 rackapp 的 call 中才能获取 怎么样实现 类似 req= 的功能?

tablecell 回复

可以使用其他方法,
比如你代码的调用链是 A -> B -> C ,想在 B 中使用 无障碍使用 req,
则可以自己稍微调整下调用链,在 A -> B 之间插入 before_B 方法来处理 req=的赋值功能,变成 A -> before_B -> B -> C
元编程或者 Rack 中间件应该是有这种 API 的。

翻了下 webbrick 的源码,他自身定义了挺多常用变量了,可以参考

https://github.com/ruby/webrick/blob/master/lib/webrick/httprequest.rb

Base.class_variable_set :@req, Rack::Request.new(env)

报错 ERROR NameError: `@req' is not allowed as a class variable name

Base.class_variable_set :@@req, Rack::Request.new(env) 是可以的

但定义类的时侯 不管是实例方法 或者类方法 getter/setter @req 都是正常的,只是同一个名字,不同的 object_id 这算不算坑?

test.rb

class Test
  attr_accessor :req

  class << self
    def setter val
      @req = val
    end

    def getter
      @req
    end

    def request
      @req
    end
  end
end

#Test.class_variable_set :@req,"foo"

Test.setter "foo"
p Test.getter
p Test.request

t = Test.new
t.req = "bar"
p t.req


Ruby 支持五种类型的变量。

一般小写字母、下划线开头:变量(Variable)。
$开头:全局变量(Global variable)。
@开头:实例变量(Instance variable)。
@@开头:类变量(Class variable)类变量被共享在整个继承链中
大写字母开头:常数(Constant)。

类的 object_id 在运行过程中是始终一致的

LiinNs 回复

按 class 是 Class 的一个实例的说法,Class 的实例 class XXX 中的 attribute 用 @req来表示实例变量是可以的,实际上写 getter/setter 都能正常访问 也不报错

Base 当成 Class 的实例,用 Base.instance_variable_set :@req, Rack::Request.new(env) 可以设置 @req 但是 block 中只能写成这样 print Base.request.inspect

类本身是实例,可以有实例变量,叫类实例变量。

tablecell 回复
Base.class_variable_set :@req, Rack::Request.new(env)

这里的 class_variable_set 方法表明要设定类变量,但是你这里设置的是类实例变量,类变量需要以@@开头。 类实例变量,也就是这个类独有的实例变量,子类无法继承

同理,你下面的 test.rb 中两个@req,用 attr_accessor 生成@req的是 Test 的实例的实例变量,而你写在 class << self 里的@req是 Test 的实例变量 (只不过此时 Test 是个类而已)

回复顶楼问题,你想要的可能是:

Base.newrequest env
cb = Base.route[method][path]
resp = Base.instance_eval(&cb) # cb 在 Base 的实例环境下执行

但这个设计我觉得有问题,多线程环境下 Base 的实例变量会有并发冲突。

illusove 回复

上面的例子 不考虑 实例的情况 就是自定义的类不用 new 的用法

比如 下面这段代码,Base/MyApp 都不运行 new 也正常读到 params 只是 block 中要写成 Base.request

require "rack"

handler = Rack::Handler::WEBrick

class Base
  class << self
    def route
      @route ||= Hash["get" => {}]
    end
    def request 
      @req
    end 

    def get(path, &block)
      Base.route["get"][path] = block
    end
  end
end

class MyApp < Base
  get "/" do
    print Base.request.inspect # print  request.inspect
    "access request"
  end
end

class RackApp
  def call(env)
      dup._call(env)
  end 
  def _call(env)
    # req = Rack::Request.new(env)
    path = env["PATH_INFO"]
    method = env["REQUEST_METHOD"].downcase
    Base.instance_variable_set :@req,  Rack::Request.new(env)
    cb = Base.route[method][path]
    resp = cb.call
    [200, { "Content-Type" => "text/html" }, [resp]]
  end
end

handler.run RackApp.new
tablecell 回复

你上面的做法,get 方法的 self 是 MyApp,所以直接执行 request.inspect 应该是空值,因为 MyApp 中没有初始化@route@req。 后面的 call 方法里一直都是对 Base 的独有的类实例变量进行赋值,所以需要加 Base.request.inspect 才能取到值,因为全程 MyApp 都只是起到了初始化 Base 里的类实例变量的作用。

把 call 方法里的 Base 都改成 MyApp, get 方法里的 block 里应该就可以直接用 request.inspect 了 或者像@Rei大佬说的一样,用 Base 的环境运行 block 也行,至于并发隐患,我还不懂😂(好像是因为@req只有一个,所以会导致多个请求同时到来的时候,@req会混乱❓ )

illusove 回复

rack 的 call 不是并发安全的吗?

tablecell 回复

Rack 只是接口,并发是交由 app server 实现,例如 puma 就是多线程模型。如果你要实现框架的话,要在框架层实现并发安全。

tablecell 回复

这我就不清楚了,不过如果 call 并发安全的话。 我有一个疑问,服务器开多线程的作用是啥😂

illusove 回复

多线程的是为了省内存啊,多进程和多线程除了内存还有啥不同?

Rei 回复

多线程的问题太多了,框架线程安全了,并不能保证所用的 gem 都是线程安全的 主流的浏览器多 Tab 浏览开始用的多线程,到最后都搞不定了内存问题,无一例外地都换成了多进程结构

tablecell 回复

以前 unicorn 流行就是因为进程模型简单,但是占内存。在 puma 稳定,并且主流 gem 都实现了线程安全之后,现在流行的就是 puma 多进程 + 多线程模型。未来有希望跟随 Ruby 发展用上 ractor + fiber 的并发模型,但还需要时间。

想说的是,如果你的框架不考虑线程安全问题,只能限定运行的 app server 用多进程模型。

tablecell 回复

额,这样么😂 ,如果多线程只是省内存,那怎么能加快程序的运行速度呢❓ ,因为我工作中使用多线程一般都是为了提高程序的运行速度,所以有此疑问😂

illusove 回复

app server 的多线程是为了提高并发,不然前一个请求完成之前,后面来的请求都要阻塞。

illusove 回复

用 MyApp 是对的

require "rack"

handler = Rack::Handler::WEBrick

class Base
  class << self
    def route
      @route ||= Hash["get" => {}]
    end
    def request 
      @req
    end 

    def get(path, &block)
      route["get"][path] = block
    end
  end
end

class MyApp < Base
  get "/" do
    print request.inspect 
    "access request"
  end
  get "/favicon.ico" do 
     File.open "favicon.ico",&:read
  end 
end

class RackApp
  def call(env)
     dup._call(env)
  end 
  def _call(env)
    path = env["PATH_INFO"]
    method = env["REQUEST_METHOD"].downcase
    MyApp.instance_variable_set :@req,  Rack::Request.new(env)
    cb = MyApp.route[method][path]
    resp = cb.call
    [200, { "Content-Type" => "text/html" }, [resp]]
  end
end

handler.run RackApp.new

另外提高程序的运行速度是在特定的场景下有前提的 比如机器轻负荷并且任务单元互相独立没有依赖关系的情况 典型的象迅雷下载文件的场景,但你在一台重负荷的机器上用多线程安装 gem 并不会提高程序运行速度,反而可能降低速度。不要迷信一些书上的结论,可能写书的老先生是20年前 mfc 桌面软件流行的时候写的,现在的应用场景与以前完全不同了,在 web 环境下,response 是完全依赖 request 的,你不可能没解析完 request,就把 response 发出去。技术问题要看需求场景和实际数据,比如同样的机器多进程/多线程在机器负荷不同的情况下结果完全不同,高负荷的机器上,多进程的并发数是高于多线程的,如果你只会背书,除了说一些貌似正确的废话,面对实际问题,只会一脸蒙逼了

illusove 回复

@route 是程序启动的时侯写入的,多个请求的时候 读@route相当于是只读,只有 @req会乱 可以把@req放在 以 Request-ID 为 key 的 Hash 里面 要读取 request 根据 env 中 Request-ID 返回 Hash 中的@req即可

tablecell 回复

哦哦,大致明白😂 。 另外,这里其实可以不用 instance_variable_set 直接操作类实例变量

MyApp.instance_variable_set :@req,  Rack::Request.new(env)

Rei 回复

感谢解答,大致明白

illusove 回复

这个写法很少见啊,我搜 self 都没见过 你在哪看到这个写法

tablecell 回复

gem 里应该还比较常见,因为 attr_accessor 就是生成 get 和 set 方法的,所以当需要对类实例变量的 get 和 set 时就在类的 singleton_class 里用 attr_accessor 就行 (类比正常的 class 和实例的关系)https://github.com/rails/rails/blob/4ace047c91d450af17eb218b0ea0139a6ea6f731/railties/lib/rails.rb#L26

illusove 回复

在 class << self 里面的类方法里面,有办法访问类的普通成员变量吗?aa 是定义在 A 下面的 class << self 只定义了 bb 比如 如果在 class << self 要访问 A下面 aa 怎么写?

class A
  attr_accessor :aa
  def initialize 
     @aa=10
  end 

  def ten
    puts self.object_id
    dup.five
  end

  def five
    puts self.object_id
         @aa / 2
  end

  class << self
    attr_accessor :bb

    def getbb
      @bb = 200
    end

    def getaa
       @aa
    end
  end
end

a = A.new
p a.five
p a.ten
p a.five

p A.getbb
p A.getaa


tablecell 回复

你这个问题的意思是要在类方法里访问实例对象的实例变量,可以是可以,但是因为一个类会有很多实例,所以无法确定是要访问哪一个的实例对象的实例变量,感觉只能在类方法中新生成一个实例对象访问 aa 的默认值或者指定访问对象才行

class A
  attr_accessor :aa

  def initialize 
     @aa=10
  end

  class << A
    attr_accessor :bb
    def getaa
      A.new.aa
    end 

    def get_aa(obj)
      obj.aa
    end 
  end
end

30 楼 已删除
需要 登录 后方可回复, 如果你还没有账号请 注册新账号