显示标签为“Source_Code”的博文。显示所有博文
显示标签为“Source_Code”的博文。显示所有博文

2011年9月21日星期三

ActiveSupport中的class_attribute

看rails guide的时候遇到了Class#class_attribute,怎么使用参见guide和下面两篇文章,这里不做介绍
http://blog.obiefernandez.com/content/2010/04/tr3w-highlights-activesupport-class-class-attribute.html
http://ihower.tw/blog/archives/4878

在看源码的时候,发现第79行怎么都不能理解,为什么是
singleton_class.#{name}
而不是
self.class.#{name}
呢?

由于我看的是3.0.7的版本,于是又找到了最新的3.1.0的版本
看完之后更加迷惑,为什么要定义两个reader instance method,一个用singleton_class,一个用self.class呢?

查阅了相关资料,翻看了源码提交记录,我找到了答案。
其实本来在3.0.7版本中用self.class代替singleton_class是可以的,但为了支持在singleton_class上调用writer方法
klass = Class.new { class_attribute :setting }
object = klass.new
object.singleton_class.setting = "foo"
所以用了singleton_class。

由于object的singleton_class的superclass是klass,singleton_class自己又没定义过setting=(),所以singleton_class.setting=()还是会查找到klass里的定义并调用它。

实际使用的时候,大部分情况还是object.setting=()而不是object.singleton_class.setting=(),为每个object创建singleton_class开销较大,所以做了优化:object调用时直接查找self.class,避开了singleton_class。而针对singleton_class另做处理,由于方法查找时singleton_class优先于klass,所以
def #{name}
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
end
会覆盖
def #{name}
defined?(@#{name}) ? @#{name} : self.class.#{name}
end

最后的问题是,谁引入的对singleton_class的支持呢?翻提交记录的时候找到了答案

2008年6月9日星期一

truncate(截取字符串)

ActionView::Helpers::TextHelper#truncate(text, length = 30, truncate_string = "...")
截取过长字符串,省略部分用truncate_string来代替(默认是...)。

If text is longer than length, text will be truncated to the length of length (defaults to 30) and the last characters will be replaced with the truncate_string (defaults to "…").

Examples

truncate("Once upon a time in a world far far away", 14)
# => Once upon a...

truncate("Once upon a time in a world far far away")
# => Once upon a time in a world f...

truncate("And they found that many people were sleeping better.", 25, "(clipped)")
# => And they found that many (clipped)

truncate("And they found that many people were sleeping better.", 15, "... (continued)")
# => And they found... (continued)


length = 显示的字符串长度 + truncate_string的长度,也就是说,设置length为10,实际显示的字符数是7个,还有3个是用来显示...的。
当然,如果结束符用其他的(比如......),那实际显示的字符串长度就是3个了。

源代码:
def truncate(text, length = 30, truncate_string = "...")
if text.nil? then return end
l = length - truncate_string.chars.length
(text.chars.length > length ? text.chars[0...l] + truncate_string : text).to_s
end
length的长度必须要大于truncate_string的长度,如有必要,在调用truncate方法前我们应该自己先判断一下。
这个截取方法对于中英文字符都有效,因为chars方法返回的是UTF-8的结果。

不过这样也带来了另一个问题,就是截取同样多的字符时,中文比英文显得要长一些:

truncate("Once upon a time in a world far far away", 14)
# => Once upon a...
truncate("这是一串很长很长的中文字符", 14)
# => 这是一串很长很长的中文...

因为程序截取是按照字符个数来截取,但是中文显示时,一个中文会占据两个英文的宽度。

javaeye有人出了一道Quiz来讨论怎么解决这个问题。

2008年6月3日星期二

ActiveResource 源码 -- format

我机器上 ActiveResource gem 包的路径:
/var/lib/gems/1.8/gems/activeresource-2.0.2-/

跟format相关的文件有三个
lib/active_resource/formats.rb
lib/active_resource/formats/json_format.rb
lib/active_resource/formats/xml_format.rb

formats.rb
module ActiveResource
module Formats
# Lookup the format class from a mime type reference symbol. Example:
#
# ActiveResource::Formats[:xml] # => ActiveResource::Formats::XmlFormat
# ActiveResource::Formats[:json] # => ActiveResource::Formats::JsonFormat
def self.[](mime_type_reference)
ActiveResource::Formats.const_get(mime_type_reference.to_s.camelize + "Format")
end
end
end

require 'active_resource/formats/xml_format'
require 'active_resource/formats/json_format'
可以看到,format.rb其实是对另外两个文件的统一包装。
其中的const_get方法比较有意思。const_get方法是Module类提供的方法,意思是取Mudule中某常量的值。
ActiveResource::Formats.const_get()返回的是一个Module类。
我想这说明了两点:常量的值可以是任何对象。Module中嵌套的Module名也是常量,Module名是对Module类的引用。

json_format.rb和xml_format.rb没有太多好说的,都是调用ActiveSupport里的to_xxx和from_xxx方法。
xml_format.rb里的decode方法值得一提:
def decode(xml)
from_xml_data(Hash.from_xml(xml))
end

private
# Manipulate from_xml Hash, because xml_simple is not exactly what we
# want for ActiveResource.
def from_xml_data(data)
if data.is_a?(Hash) && data.keys.size == 1
data.values.first
else
data
end
end
可以看到,在decode方法里又调用了一次from_xml_data方法,为什么要这样呢?
因为ActiveSupport里Hash的from_xml方法是调用xml_simple的方法来实现的,而这个xml_simple返回的Hash往往在最外面还包了一层:

{"records"=>[{"name"=>"Matz", "id"=>1}, {"name"=>"David", "id"=>2}]}
{"hash"=>{"name"=>"David", "id"=>2}}

而实际上我们要的是里面的value:

[{"name"=>"Matz", "id"=>1}, {"name"=>"David", "id"=>2}]
{"name"=>"David", "id"=>2}

所以要用from_xml_data把数据再剥开一次。

ActiveSupport里Hash的from_xml方法定义如下(在activesupport/lib/active_support/core_ext/hash/conversions.rb里):
def from_xml(xml)
# TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
undasherize_keys(typecast_xml_value(XmlSimple.xml_in(xml,
'forcearray' => false,
'forcecontent' => true,
'keeproot' => true,
'contentkey' => '__content__')
))
end
哈哈,DHH自己都说了要Refactor这个方法,不过估计也就只写了个TODO在这里,一直没Refactor过。

另外有个问题,为什么这里可以直接调用XmlSimple的方法呢?
首先要在文件里require一下:
require 'xml_simple'
其次在vendor目录下已经引入了XmlSimple:
activesupport/lib/active_support/vendor/xml_simple.rb

没错,XmlSimple就只有这样一个文件,不信你去把gem安装的XmlSimple打开来看吧,在我机器上的路径是:
/var/lib/gems/1.8/gems/xml-simple-1.0.11/
一千多行的文件,XmlSimple的作者可真能折腾啊。

2008年1月16日星期三

Hash中去掉不需要的key

Ruby中,Hash本身的delete是很方便,但也有不足的地方:
一次只能删掉一个key,而且返回值是对应key的value,而不是hash,无法做这样的链式操作:hash.delete(:key_1).delete(:key_2)
delete本身是破坏性的方法,会改变hash的值,而有时候我不想改变,只想得到一个新的hash。

于是自己实现一个:
def remove_keys(hash, *keys)
new_hash = hash.dup
keys.each {|k| new_hash.delete(k)}
new_hash
end

后来去查了一下Rails源代码,发现已经有现成的方法了,在activesupport/lib/active_support/core_ext/hash/except.rb中
# Returns a new hash without the given keys.
def except(*keys)
rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
reject { |key,| rejected.include?(key) }
end

# Replaces the hash without only the given keys.
def except!(*keys)
replace(except(*keys))
end

感觉写框架的就是不一样,考虑到了很多,不像我那个,自己写自己用,确保按照正确方式用就行了,异常使用方式就没去处理(其实我那个方法一般是不会有什么异常使用方式的,因为dup和delete方法在HashWithIndifferentAccess类中已经被重写了,DHH已经帮我预先处理了异常使用方式)。
这里为什么要先判断一下能否响应convert_key方法呢?这就涉及到上次讲的HashWithIndifferentAccess,convert_key源代码如下:
def convert_key(key)
key.kind_of?(Symbol) ? key.to_s : key
end
像params这种,里面的key存的全都是string,你指定删除symbol的key,如果程序实现直接去删的话,就会不起作用(没有去掉指定的key)
然而,我将源代码改为:
def except(*keys)
rejected = keys
reject { |key,| rejected.include?(key) }
end
运行测试,还是全都通过了,看来测试代码没有覆盖到这种情况。

找到测试代码:
def test_except
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x', :b => 'y' }

# Should return a new hash with only the given keys.
assert_equal expected, original.except(:c)
assert_not_equal expected, original

# Should replace the hash with only the given keys.
assert_equal expected, original.except!(:c)
assert_equal expected, original
end

改为:
def test_except
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x', :b => 'y' }

# Should return a new hash with only the given keys.
assert_equal expected, original.except(:c)
assert_not_equal expected, original

# Should replace the hash with only the given keys.
assert_equal expected, original.except!(:c)
assert_equal expected, original

original = HashWithIndifferentAccess.new(:a => 'x', :b => 'y', :c => 10)
expected = HashWithIndifferentAccess.new(expected)
assert_equal expected, original.except(:c)
end
哈哈,测试报错了,看来我们刚才添加的测试代码起作用了,TDD的第一步,即写一段测试,使测试failed,已经完成了。
然后要做的就是完成代码使测试通过。这个很简单,把刚才改的代码恢复就可以了。
恢复后,再运行测试,通过了。看来我们刚才加的测试覆盖到了这行代码了:
  rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)

让我们再多想一下,如果去掉上面那行代码,不考虑HashWithIndifferentAccess的情况,那么就下面这一行代码就实现了删除指定key的功能了。
def except(*keys)
reject { |key,| keys.include?(key) }
end
晕死,原来就这么简单呀,到处去找不改变hash本身的delete方法,其实reject就是:

Same as Hash#delete_if, but works on (and returns) a copy of the hsh. Equivalent to hsh.dup.delete_if.


最后查了一下项目,发现在Rails 1.2.5里面,except方法是在vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb中定义的。
准确说是Rails 1.2.5里没有这个方法,而是由will_paginate这个分页的plugin提供的。Rails 2.0把它移到了activesupport里。
以后关于Rails源代码,没有说明具体版本,都默认为Rails 2.0

2008年1月13日星期日

attr_accessor_with_default

实现
activesupport/lib/active_support/core_ext/module/attr_accessor_with_default.rb
测试
activesupport/test/core_ext/module/attr_accessor_with_default_test.rb

实现方法:
def attr_accessor_with_default(sym, default = nil, &block)
raise 'Default value or block required' unless !default.nil? || block
define_method(sym, block_given? ? block : Proc.new { default })
module_eval(<<-EVAL, __FILE__, __LINE__)
def #{sym}=(value)
class << self; attr_reader :#{sym} end
@#{sym} = value
end
EVAL
end

define_method,即定义一个方法,方法名为#{sym},另外接受一个block作为其方法体,此处为*直接*返回一个默认值。
这里就是在定义get方法。
怪就怪在后面的set方法的定义,为什么还要先写一行 class << self; attr_reader :#{sym} end 呢?
因为前面的get方法是直接返回一个默认值,并没有生成一个名为@#{sym}的实例变量,所以这一行的作用是用Ruby提供的attr_reader方法,来重新生成一个get方法,这个新的get方法里,自然就有名为@#{sym}的实例变量了。
然后再是标准的set方法体:@#{sym} = value
三个方法的生成过程为:
由 define_method(sym, block_given? ? block : Proc.new { default }) 生成:
def foo
default
end
由class << self; attr_reader :#{sym} end 生成:
def foo
@foo
end
def foo=(value)
@foo = value
end
所以一旦调用了set方法,前面的get方法被后面的get方法覆盖。

为了证实,可以修改实现文件和测试文件:
def attr_accessor_with_default(sym, default = nil, &block)
raise 'Default value or block required' unless !default.nil? || block
define_method(sym, block_given? ? block : Proc.new { default })
module_eval(<<-EVAL, __FILE__, __LINE__)
def #{sym}=(value)
class << self; attr_reader :#{sym} end
@#{sym} = value
# @#{sym} = value # 注释掉该行,使得set方法不赋值,但仍然生成一个新的get方法,并生成名为@#{sym}的实例变量
end
EVAL
end
def test_default_arg
@target.attr_accessor_with_default :foo, :bar
p @instance.foo # 执行set方法前,调用get方法
assert_equal(:bar, @instance.foo)
@instance.foo = 'foo' # 此处赋值为'foo',而非原来的nil
p @instance.foo # 执行set方法后,再调用get方法
# assert_nil(@instance.foo) # 注释掉该行,以免因为我们的修改而使测试出错
end

运行测试,打印出结果:

...:bar
nil...


也就是说,第一次调用get方法,是按照预想的返回一个默认值 :bar,此时还是旧的get方法。第二次调用时,返回的是新的get方法里的实例变量@foo,而@foo并未被赋值,所以返回值是nil(前一行的@instance.foo = 'foo'并没有起到赋值的作用,因为我们在实现文件里已经把相应的行注释掉了)

HashWithIndifferentAccess

Rails里,常用params[:sym]来取页面的参数,其实从页面上传过来的都是字符串(hash的key和value都是字符串),Rails为了让我们能够按照习惯用symbol来取值,就做了一些改变
在activesupport/lib/active_support/core_ext/hash/indifferent_access.rb里。
所有的key都被强制转换成了string,也就是说:
h = HashWithIndifferentAccess.new  #=>  {}
h[:a] = "a" #=> "a"
h #=> {"a" => "a"}

具体的转换体现在下面的代码:
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
alias_method :regular_update, :update unless method_defined?(:regular_update)

def []=(key, value)
regular_writer(convert_key(key), convert_value(value))
end

def update(other_hash)
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
self
end
先给父类的[]=方法和update方法取个别名,然后再覆盖这两个方法。

然而进一步查看其源代码,却发现它对stringify_keys!和symbolize_keys!这两个方法的测试不正确,没有覆盖到。因为我把对应现代码注释掉,再运行测试,居然都通过了,没有任何报错提示。
源代码如下:
def stringify_keys!; self end
def symbolize_keys!; self end
测试代码如下:
def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash
h = HashWithIndifferentAccess.new
h[:first] = 1
h.stringify_keys!
assert_equal 1, h['first']
h = HashWithIndifferentAccess.new
h['first'] = 1
h.symbolize_keys!
assert_equal 1, h[:first]
end

应该改为:
def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash
h = HashWithIndifferentAccess.new
h[:first] = 1
h.stringify_keys!
assert_equal 1, h['first']
h = HashWithIndifferentAccess.new
h['first'] = 1
h.symbolize_keys!
assert_not_equal :first, h.index(1)
# assert_equal 1, h[:first]
end

个人认为stringify_keys!方法在这里是无法测试的,因为该类里key全都转换成string了,那么这个"Destructively convert all keys to strings."(父类方法的注释说明)的方法,其调用前后,对象没有任何改变,无法编写对应的测试。这里覆盖父类方法,可能的原因是基于效率的考虑,让其不再调用父类的stringify_keys!方法(因为已经全都是string了,再调用一次,再判断一次是浪费)。

还有个问题,HashWithIndifferentAccess类只保证了key都是按string来存储的,但为什么h[:a]这样用symbol还能取到呢?它并没有覆盖父类的[]方法。
经过跟同事的讨论,有了结果:
def default(key = nil)
if key.is_a?(Symbol) && include?(key = key.to_s)
self[key]
else
super
end
end
它覆盖了default方法,而ruby实现里[]方法会去调用default方法。具体的ruby底层实现机制我并不了解,只是通过改源代码和其测试代码,发现了default方法跟[]方法相关。