Ruby 项目里如何动态查找已存在的类?

anklos · 2012年03月21日 · 最后由 colorfulberry 回复于 2015年06月15日 · 4428 次阅读

项目里有个地方要动态的查找某个 string 是不是一个 class

我发现 defined?的方法似乎要直接传类名才可以。

比如:defined?(String)的结果是'constant',正确。 但是 defined?('String'.constantize) 结果是‘method'。不解。irb 里 String == 'String'.constantize的结果是 true。但同时我也发现String === 'String'.constantize是 false 的。

或者用 Module.const_get 这个方法去判断一个类是否存在,如果想判断诸如‘A::B::C'的在 module 下面的类的话,const_get 会抛 name error 的异常。

目前就简单的 hack 出一个方法来帮助判断:

def present?(name)
  begin 
     eval name
     true
  rescue
     false
   end
end


但觉得这种做法很烂。请问大家是如何处理这种情况的?

这样可行吗?

def present?(name)
   name.split('::').inject(Object) {
        |o, x| o = o.const_get(x) if x.capitalize==x && o.const_defined?(x) 
   }
end

benchmark

user system total real eval: 3.580000 0.000000 3.580000 ( 3.585330) const_get: 1.830000 0.000000 1.830000 ( 1.824264)

看半天还是没明白楼主要说什么。

Ruby 下面有叫做 constantize 的方法或变量吗?

irb 和正常的代码,执行结果当然可能不一样。irb 额外加载很多库的。

constantize 是 ActiveSupport 里定义的

不好意思,没看懂 lz 想表达什么,看到 @hhuai 的回复。我想到了 ActiveResource 里查找类的一个方法供参考:

ActiveResource::Base#find_resource_in_modules 根据资源名称和模块名称查找类。从模块的最内层到最外层来查找。 ActiveResource::Base#find_or_create_resource_for 调用了这个方法。

源码见 https://github.com/rails/rails/blob/2-3-stable/activeresource/lib/active_resource/base.rb

# Tries to find a resource in a non empty list of nested modules
# Raises a NameError if it was not found in any of the given nested modules
def find_resource_in_modules(resource_name, module_names)
  receiver = Object
  namespaces = module_names[0, module_names.size-1].map do |module_name|
    receiver = receiver.const_get(module_name)
  end  
  if namespace = namespaces.reverse.detect { |ns| ns.const_defined?(resource_name) }
    return namespace.const_get(resource_name)
  else 
    raise NameError
  end  
end  

# Tries to find a resource for a given name; if it fails, then the resource is created
def find_or_create_resource_for(name)
  resource_name = name.to_s.camelize
  ancestors = self.class.name.split("::")
  if ancestors.size > 1
    find_resource_in_modules(resource_name, ancestors)
  else 
    self.class.const_get(resource_name)
  end  
rescue NameError
  if self.class.const_defined?(resource_name)
    resource = self.class.const_get(resource_name)
  else 
    resource = self.class.const_set(resource_name, Class.new(ActiveResource::Base))
  end  
  resource.prefix = self.class.prefix
  resource.site   = self.class.site
  resource
end 

@anklos 我觉得你的这个函数是最简单的实现,已经够用了,如果觉得 eval 不好看,换成这样试试:

# 不要叫 present?,会跟 Object#present? 重名
class String
  def has_defined_class?
    begin
      !!self.classify.constantize
    rescue NameError
      false
    end
  end
end

'not_exist_class'.has_defined_class? # => false
'integer'.has_defined_class? # => true

Object.constants.include? :"A"

?

先谢谢各位回答。 @hhuai 这个顶层 module 名也会显示是一个类。比如“M1::M2::class", if present?("M1") 也会通过。 其他我没发现什么问题。但是放到项目里处理时还是会出错。暂时没仔细 debug @zhangyuan 这个是正宗的。哈哈 @iwinux 不应将抛异常来作为一个判断条件。

#8 楼 @anklos 只是一种代码风格吧,在代码逻辑上是没有问题的。另外 5 楼那个方法最后也是要处理 NameError 异常的。

#8 楼 @anklos 这好解决,

def present?(name)
   name.split('::').inject(Object) {
        |o, x| o = o.const_get(x) if x.capitalize==x && o.const_defined?(x) 
   }.is_a? Class
end

eval 性能很差的,我这个性能好很多。

#6 楼 @iwinux 你这个写法性能也很差的,当有异常抛出时,eval 和 resuce 的性能差得死。

Class.const_defined?("User") 不过要先'运行'一下类

要么在启动 server 的时候 Dir.foreach("RAILS_APP_PATH/app/modules").each{|v| next if v =~ /^\./; require v}

@hhuai 谢谢! @iwinux 5 楼的源码里是根据情况抛出异常,不是根据抛出异常这个行为来判断应该返回什么值 @Ddl1st 这个还是对 module 里面的 class 会抛 name error 的错。比如你想判断 project 里有无'M1::M2::CLASS"这个类

#13 楼 @anklos ... 指正一下 String === "String" 是判断"String"是否是 String 的实例

def present?(name)
  !name.constantize.blank? ;rescue NameError; false
end


const_get 和 const_set

@hhuai 今天调试了下,发现 bug 在哪了。 x.capitalize 只转换首字母,遇到'MethodA'这种格式的类名就不对了。当然也很好改,换成 camelize 就好了

#17 楼 @anklos 我的本意是判断第一个字母是否为大写,因为这才符合 class 或 module 的命名,谁知道 capitalize 发挥了我意想不到的作用。

刚刚翻 Rails 3.2 的 Release Notes,发现 Module 有一个新的实例方法 #qualified_const_defined?,看看是不是你想要的?

https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/module/qualified_const.rb

另外还有一个新方法是 safe_constantize,不会抛出 NameError,而是会返回 nil

@iwinux 我靠,2 个都很好用,谢啦:)

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