ruby string to class

Posted by wxianfeng Wed, 16 Nov 2011 20:53:00 GMT

环境:ruby 1.9.2

有这样一个需求, 给你 任意一个字符串,把它转化为类,网上大多数的 解决办法是 下面三种:

Kernel.const_get(:User) # Object.const_get(:User)
eval(’User’)
‘User’.constantize

但是上面三种方法, 这个 User 事先必须是初始化的,不然会报错,如下:

Object.const_get(:User) # uninitialized constant User (NameError)

所以需要加个判断 这个 Class 有没有初始化,没有的话 再set 一个Class

require "rails/all"

def Kernel.const_missing(name)
  Object.const_set(name,Class.new)
end
#p Kernel.const_get("baoxiaos".to_sym) # wrong constant name baoxiaos (NameError)
p Kernel.const_get("baoxiaos".classify.to_sym) # Baoxiao , const的首字母必须大写
p Kernel.constants # []
p Object.constants.include?(:UBaoxiao) # true

刚开始把这个 const 定义在 Object 里:

require "rails/all"

def Object.const_missing(name)
  Object.const_set(name,Class.new)
end

#p Object.const_get("baoxiaos".to_sym) # wrong constant name baoxiaos (NameError)
p Object.const_get("baoxiaos".classify.to_sym)

发现 用在 rails 中后 , rails 处处报错,可能 怪我重写了 Object.const_missing 导致的 ……..

上面方法的原理是 当找不到 const的时候 执行了

  Object.const_set(name,Class.new)

那么 name 就变成是 Class 的实例了 , 为什么定义在 Kernel 中的 const_missing ,Object 也可以访问到,需要注意 Object,Kernel 之间的关系,Object 是 inluce Kernel 的,如下:

class Object
include Kernel
end

SEE:

http://niczsoft.com/2010/01/string-to-class-in-ruby-on-rails/
http://www.ruby-forum.com/topic/96222


Rails try method

Posted by wxianfeng Thu, 13 Jan 2011 19:20:00 GMT

环境:ruby 1.9.2 + rails 3.0.3

我们经常会有这样的操作:

user = User.find_by_login("wxianfeng")  # =>  nil 
user.name # => NoMethodError: undefined method `name' for nil:NilClass

假如 login 为 wxianfeng 不存在 ,会报错:

NoMethodError: undefined method `name' for nil:NilClass

那么建议使用 try 方法避免报错,try 返回的是 nil

user.try(:name) # =>nil 

也就相当于

nil.try(:name) # => nil

看下源码: here

其实就是调用了 __send__ 方法 , __send__ 方法 和 send 方法等价 , 只不过 __send__ 方法 为了防止 有已经存在的 send 方法 , nil 的话 调用 NilClass 的 try 方法

另外 发现 github上 try方法已经重新写了 ,如下: here

class Object
  # Invokes the method identified by the symbol +method+, passing it any arguments
  # and/or the block specified, just like the regular Ruby <tt>Object#send</tt> does.
  #
  # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised
  # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass.
  #
  # If try is called without a method to call, it will yield any given block with the object.
  #
  # ==== Examples
  #
  # Without try
  # @person && @person.name
  # or
  # @person ? @person.name : nil
  #
  # With try
  # @person.try(:name)
  #
  # +try+ also accepts arguments and/or a block, for the method it is trying
  # Person.try(:find, 1)
  # @people.try(:collect) {|p| p.name}
  #
  # Without a method argument try will yield to the block unless the reciever is nil.
  # @person.try { |p| "#{p.first_name} #{p.last_name}" }
  #--
  # +try+ behaves like +Object#send+, unless called on +NilClass+.
  def try(*a, &b)
    if a.empty? && block_given?
      yield self
    else
      __send__(*a, &b)
    end
  end
end

class NilClass #:nodoc:
  def try(*args)
    nil
  end
end

其实只是判断了 if a.empty? && block_given? 这种情况 则直接执行block 内容然后返回,效果一样…..

DEMO:

require "active_support/core_ext/object/try"

class Klass

  def send(*args)
    "helo " + args.join(' ')
  end

  def hello(*args)
    "Hello " + args.join(' ')
  end

  def self.foobar(s)
     "#{s} foobar"
  end
end

k = Klass.new

# __send__ 为了防止有方法名叫send , 建议用 __send__
p k.__send__ :hello, "gentle", "readers"   #=> "Hello gentle readers" 
p k.send "gentle", "readers"   #=> "Helo gentle readers"

# Ruby 里一切皆是对象,类也是对象
# Klass(类) 是 Class 的实例 , Class 是 Object 的实例 , 那么 Klass 也就是 Object 的实例 所以 Klass 可以调用try 方法
p Klass.try(:foobar,"hey") # => "hey foobar"
# k 是Klass 的实例,Klass 的父类是 Object , 所以 k 可以调用 try 方法
p k.try(:send,"bla","bla") # => "helo bla bla"

# class 得到的是 实例关系
# superclass 得到的是 继承关系
p Klass.superclass # Object
p Klass.class # Class
p k.class # Klass

另外 这是 对象nil 那如果 没有那个字段了 , 就会 报 找不到方法的错误

例如:

ruby-1.9.2-p0 > u=User.first
  User Load (175.8ms)  SELECT `users`.* FROM `users` LIMIT 1
 => #<User id: 1, login: "entos", name: "", email: "entos@entos.com", crypted_password: "557c88b0713f63397249f4198368e4a57d6d400f", salt: "4e04ef1cf506595ac3edf6a249791c55995b0f8f", remember_token: nil, remember_token_expires_at: nil, activation_code: nil, activated_at: nil, status: 2, suspend_at: nil, avatar_id: nil, orgunit_id: nil, mobile_phone: nil, last_login_at: nil, language: nil, options: nil, created_at: "2011-02-24 02:55:42", updated_at: "2011-02-24 02:55:42"> 
ruby-1.9.2-p0 > u.hi
NoMethodError: undefined method `hi' for #<User:0x9fcfe00>

建议加上 respond_to? 判断

ruby-1.9.2-p0 > u.respond_to? "hi"
 => false

Rails源码 attr_internal

Posted by wxianfeng Thu, 05 Jan 2012 18:34:00 GMT

环境:ruby 1.9.2 + rails 3.0.3 + ubuntu 10.10

params在rails中很常用,特别在表单提交的时候,params 产生的是一个Hash ,里面构造通过 form域的name构造 ,产生不同的 params 内容,今天 在看rails params 实现的时候 发现通过 attr_internal 的方法实现,params方法 的源码:

    def params
      @_params ||= request.parameters
    end

发现其实是从 request 这个方法得到的,那么request方法又是怎么定义的:

attr_internal :headers, :response, :request

就是 用了 attr_internal 方法

看下 整个 metal.rb文件: here

发现了 response,headers,session(借助delegate委派) ,status,params == 都是通过 attr_internal 实现的,来看看 attr_internal 到底是何须人也 :

源码: here

class Module
  # Declares an attribute reader backed by an internally-named instance variable.
  def attr_internal_reader(*attrs)
    attrs.each do |attr|
      module_eval "def #{attr}() #{attr_internal_ivar_name(attr)} end", __FILE__, __LINE__
    end
  end

  # Declares an attribute writer backed by an internally-named instance variable.
  def attr_internal_writer(*attrs)
    attrs.each do |attr|
      module_eval "def #{attr}=(v) #{attr_internal_ivar_name(attr)} = v end", __FILE__, __LINE__
    end
  end

  # Declares an attribute reader and writer backed by an internally-named instance
  # variable.
  def attr_internal_accessor(*attrs)
    attr_internal_reader(*attrs)
    attr_internal_writer(*attrs)
  end

  alias_method :attr_internal, :attr_internal_accessor

  class << self; attr_accessor :attr_internal_naming_format end
  self.attr_internal_naming_format = '@_%s'

  private
    def attr_internal_ivar_name(attr)
      Module.attr_internal_naming_format % attr
    end
end

发现其实就是通过 module_eval 给 对象 添加了 settet , getter 方法而已,但是命名格式是这样的:

  self.attr_internal_naming_format = '@_%s' 

DEMO:

require "active_support/core_ext/module/attr_internal"

class Foo
  
  attr_accessor :sex,:birthday # attr_accessor ruby里封装的method
  attr_internal :name,:city # attr_internal rails 封装的

  def bar
    name # call getter method # => @_name
  end

end

f = Foo.new
f.name = 'wxianfeng'
p f.instance_variables # => [:@_name]
p f.name # => "wxianfeng"
p f # => #<Foo:0x8630e18 @_name="wxianfeng">
p f.bar # => "wxianfeng"

所以 attr_internal 和 attr_accessor 其实是 等价的,只不过 从字面意思上看是内部变量(闭包变量的写法) ,attr_internal 希望你 通过方法名来调用,不用 @_%s 这个写法 来调用


所以 其实 一般我们在 controller 用的 request 方法 其实 可以直接这样写 @_request ,

request #=> @_request
params # => @_request.parameters
params # => @_params
headers #=> @_headers
status #=> @_status
.
.
.

但是一般 不建议这样写

还发现 这些和 http相关的东西都定义在 metal 模块, metal 是 rails 链接 rack 的中间件,源码中的解释:

ActionController::Metal provides a way to get a valid Rack application from a controller.

Rack 是一个 ruby实现的web server,封装了 http的请求和响应等,例如 rails,sinatra == 都是在 rack 基础上实现的……

有机会很有必要 深入学习下…

SEE:

http://rubyonrailswin.wordpress.com/2007/03/07/actioncontroller-and-what-the-heck-is-attr_internal/
http://www.oschina.net/p/rack


ruby instance_variables

Posted by wxianfeng Mon, 10 Jan 2011 16:52:00 GMT

环境:ruby 1.9.2 + ubuntu 10.10

instance_variables 得到当前 对象已经开辟内存空间的实例变量,疑惑在这里

class Foo

  attr_accessor :sex,:birthday  

end

p Foo.new.instance_variables # => []

刚开始不理解,怎么是 空……..

后来想了想,因为 ruby 是 动态的解释型的语言,如果没有 给实例变量赋值 的话,是不会开辟内存空间的,所以 instance_variables 只能得到已经开辟内存空间的 实例变量,
但是如果是 编译型的静态语言 则不然,例如java ,实例变量 声明了 就会开辟内存空间了

DEMO1:

# ruby version : 1.9.2

class Foo

  attr_accessor :sex,:birthday # attr_accessor ruby里封装的method  

end

p Foo.new.instance_variables # => []

f = Foo.new
p f.sex #=> nil
f.sex = 'M' 
p f.instance_variables #=>  [:@sex]
p f.inspect # => "#<Foo:0x85ead8c @sex=\"M\">"

b = Foo.new
b.birthday = nil # 注意赋值为nil,也开辟了内存空间
p b.instance_variables # => [:@birthday]
p b.inspect # => "#<Foo:0x9dca358 @birthday=nil>"

DEMO2:

class Foo
  
  def initialize
    @name = 'wxianfeng'
  end

  def bar # => as getter method
    @name
  end

end

f = Foo.new
p f.instance_variables #=> [:@name]
p f.inspect #=> "#<Foo:0x8aa7c08 @name=\"wxianfeng\">"
p f.name # =>  undefined method `name' for #<Foo:0x99adf7c @name="wxianfeng">
p f.bar # => "wxianfeng"

ruby 源码:

    # obj.instance_variables    => array
    #
    #
    # Returns an array of instance variable names for the receiver. Note
    # that simply defining an accessor does not create the corresponding
    # instance variable.
    #
    #    class Fred
    #      attr_accessor :a1
    #      def initialize
    #        @iv = 3
    #      end
    #    end
    #    Fred.new.instance_variables   #=> ["@iv"]
    #
    #
    def instance_variables
      # This is just a stub for a builtin Ruby method.
      # See the top of this file for more info.
    end

SEE:
http://www.megasolutions.net/ruby/instance_variables-doesnt-return-unassigned-variables-68358.aspx


ruby module extend self

Posted by wxianfeng Tue, 14 Jul 2009 17:39:00 GMT

环境:ruby 1.9.2 + ubuntu 10.10 + rails 3.0.3

我们知道 ruby 中 扩展class ,写公用方法 ,或者 利用命名空间 来模块化 ,都是通过 module 来实现的 , 今天 看 rails 中 camelize 方法的源码的时候 , 发现 module 这样写的…..

active_support/inflector/methods.rb

module ActiveSupport
  # The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without,
  # and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept
  # in inflections.rb.
  #
  # The Rails core team has stated patches for the inflections library will not be accepted
  # in order to avoid breaking legacy applications which may be relying on errant inflections.
  # If you discover an incorrect inflection and require it for your application, you'll need
  # to correct it yourself (explained below).
  module Inflector
    extend self

    # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
    # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
    #
    # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
    #
    # Examples:
    #   "active_record".camelize                # => "ActiveRecord"
    #   "active_record".camelize(:lower)        # => "activeRecord"
    #   "active_record/errors".camelize         # => "ActiveRecord::Errors"
    #   "active_record/errors".camelize(:lower) # => "activeRecord::Errors"
    #
    # As a rule of thumb you can think of +camelize+ as the inverse of +underscore+,
    # though there are cases where that does not hold:
    #
    #   "SSLError".underscore.camelize # => "SslError"
    def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
      if first_letter_in_uppercase
        lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
      else
        lower_case_and_underscored_word.to_s[0].chr.downcase + camelize(lower_case_and_underscored_word)[1..-1]
      end
    end

    # Makes an underscored, lowercase form from the expression in the string.
    #
    # Changes '::' to '/' to convert namespaces to paths.
    #
    # Examples:
    #   "ActiveRecord".underscore         # => "active_record"
    #   "ActiveRecord::Errors".underscore # => active_record/errors
    #
    # As a rule of thumb you can think of +underscore+ as the inverse of +camelize+,
    # though there are cases where that does not hold:
    #
    #   "SSLError".underscore.camelize # => "SslError"
    def underscore(camel_cased_word)
      word = camel_cased_word.to_s.dup
      word.gsub!(/::/, '/')
      word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
      word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
      word.tr!("-", "_")
      word.downcase!
      word
    end

    # Replaces underscores with dashes in the string.
    #
    # Example:
    #   "puni_puni" # => "puni-puni"
    def dasherize(underscored_word)
      underscored_word.gsub(/_/, '-')
    end

    # Removes the module part from the expression in the string.
    #
    # Examples:
    #   "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
    #   "Inflections".demodulize                                       # => "Inflections"
    def demodulize(class_name_in_module)
      class_name_in_module.to_s.gsub(/^.*::/, '')
    end

    # Creates a foreign key name from a class name.
    # +separate_class_name_and_id_with_underscore+ sets whether
    # the method should put '_' between the name and 'id'.
    #
    # Examples:
    #   "Message".foreign_key        # => "message_id"
    #   "Message".foreign_key(false) # => "messageid"
    #   "Admin::Post".foreign_key    # => "post_id"
    def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
      underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
    end

    # Ruby 1.9 introduces an inherit argument for Module#const_get and
    # #const_defined? and changes their default behavior.
    if Module.method(:const_get).arity == 1
      # Tries to find a constant with the name specified in the argument string:
      #
      #   "Module".constantize     # => Module
      #   "Test::Unit".constantize # => Test::Unit
      #
      # The name is assumed to be the one of a top-level constant, no matter whether
      # it starts with "::" or not. No lexical context is taken into account:
      #
      #   C = 'outside'
      #   module M
      #     C = 'inside'
      #     C               # => 'inside'
      #     "C".constantize # => 'outside', same as ::C
      #   end
      #
      # NameError is raised when the name is not in CamelCase or the constant is
      # unknown.
      def constantize(camel_cased_word)
        names = camel_cased_word.split('::')
        names.shift if names.empty? || names.first.empty?

        constant = Object
        names.each do |name|
          constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
        end
        constant
      end
    else
      def constantize(camel_cased_word) #:nodoc:
        names = camel_cased_word.split('::')
        names.shift if names.empty? || names.first.empty?

        constant = Object
        names.each do |name|
          constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name)
        end
        constant
      end
    end

    # Turns a number into an ordinal string used to denote the position in an
    # ordered sequence such as 1st, 2nd, 3rd, 4th.
    #
    # Examples:
    #   ordinalize(1)     # => "1st"
    #   ordinalize(2)     # => "2nd"
    #   ordinalize(1002)  # => "1002nd"
    #   ordinalize(1003)  # => "1003rd"
    def ordinalize(number)
      if (11..13).include?(number.to_i % 100)
        "#{number}th"
      else
        case number.to_i % 10
          when 1; "#{number}st"
          when 2; "#{number}nd"
          when 3; "#{number}rd"
          else    "#{number}th"
        end
      end
    end
  end
end

发现 了这样的写法

module A
module B
extend self
.....
end
end

extend self 有何作用?

先来看个demo:

module Foo
  module Bar
      extend self # self => Foo::Bar

    def hello
      p "hello"
    end
  end
end

class Klass
  include Foo::Bar
end

Klass.new.hello #  "hello"
Foo::Bar.hello # "hello"
Klass.hello # undefined method `hello' for Klass:Class (NoMethodError)

发现 module 中的方法 可以当作模块方法 直接被Module调用 , 被include 到class 中后 , 依然还是 class 的实例方法 , 恩,不错,以后 像下面 这样的写法, 都要 改改了:

module A
def self.foo
end
end

改成这样:

module A
extend self
def foo
end
end

demo里的extend self 其实就是 Foo::Bar.extend(Foo::Bar)

所以 可以 更动态的 写成这样:

module Foo
  module Bar     

    def hello
      p "hello"
    end
  end
end

class Klass
  include Foo::Bar
end

Foo::Bar.extend(Foo::Bar)

Klass.new.hello #  "hello"
Foo::Bar.hello # "hello"
Klass.hello # undefined method `hello' for Klass:Class (NoMethodError)

see:
http://www.railsfire.com/article/extending-self-module


Rails 动态生成表和Model

Posted by wxianfeng Sat, 01 Aug 2009 17:12:00 GMT

环境:ruby 1.9.2 + rails 3.0.3 + ubuntu 10.10

项目需要运行中动态生成表 和 Model , 怎么办 ?

借助 ActiveRecord::Migration 来实现 动态建表 和 字段

可以 借助 Object.const_set 来实现 动态Model

DEMO:

# RUN : rails runner lib/dynamic_table.rb

ActiveRecord::Migration.create_table :posts
ActiveRecord::Migration.add_column :posts, :title, :string

Object.const_set(:Post,Class.new(ActiveRecord::Base)) # => Object.class_eval { const_set(:Post,Class.new(ActiveRecord::Base)) }
# p Post.columns
p Post.column_names # ["id", "title"]

ActiveRecord::Migration.add_column :posts, :body, :text

p Post.column_names # ["id", "title"]

Object.class_eval { remove_const :Post }
Object.const_set(:Post,Class.new(ActiveRecord::Base))

p Post.column_names # ["id", "title", "body"]

动态Model 实质就相当于 Post = Class.new(ActiveRecord::Base) 或者 Post < ActiveRecord::Base

Class.new(ActiveRecord::Base) 参数指定 super_class , 默认是 Object

可以从ruby源码中看出:

  #     Class.new(super_class=Object)   =>    a_class
  #
  #
  # Creates a new anonymous (unnamed) class with the given superclass
  # (or <code>Object</code> if no parameter is given). You can give a
  # class a name by assigning the class object to a constant.
  #
  #
  #
  def self.new(super_class=Object)
    # This is just a stub for a builtin Ruby method.
    # See the top of this file for more info.
  end

当给表添加了新的字段后,Model 需要重新 const_set 一次 ,注意 const_set 之前 需要 remove_const 一次 , 不然会出现 已经初始化的警告

see:

http://hildolfur.wordpress.com/2006/10/29/class-reloading-in-ruby/