Ruby Koans

学习 Ruby 语言的途径有太多太多, 你可以选择一本经典书籍慢慢看, 也可以找在线的资源(类似那种十五分钟入门的), 甚至有很多 Ruby 语言使用者是从 Ruby On Rails 学起的(例如我).

不知道从什么时候开始, Ruby 官网文档里出现一篇Ruby Koans, 采用单元测试填空题的方式强化基础知识. 网站访问不了可以直接访问源代码库.

说真的, 做完这套题目以后, 我对 Ruby 有了重新的认识, 这样说有点太官方了, 其实是我的自信心受到了粉碎性打击然后重组.

将代码下载回来, 运行 ruby path_to_enlightenment.rb, 就会开始单元测试过程, 因为题目的答案还没有填写, 所以测试肯定是通不过的, 碰到问题就会停下来, 然后提醒你哪个文件的第几行出了问题.

最先出现问题的文件会是 about_asserts.rb, 这里是练习一下断言的最基本用法, 以及几道 1+1=2 之类的问题. __处就是要你填空的地方, 如果不填会提醒你 <"FILL ME IN">.

about_nil.rb
1
2
3
4
5
6
def test_nil_has_a_few_methods_defined_on_it
# nil对应空字符串, 所以 "#{nil}" 才不会影响输出.
assert_equal '', nil.to_s
# 这个就很新鲜, 之前没有注意到啊.
assert_equal 'nil', nil.inspect
end
about_objects.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test_some_system_objects_always_have_the_same_id
# false, true, nil 三者都有固定的 object_id, 为什么会取 0 2 4 这三个值呢? 请看下文.
assert_equal 0, false.object_id
assert_equal 2, true.object_id
assert_equal 4, nil.object_id
end

def test_small_integers_have_fixed_ids
# 整型的 object_id 等于 2n+1, 所以负数的 object_id 也是负的.
# 从 0 开始闲置的 object_id 就只有 0 2 4 等等. 所以之前特殊的 object_id 由此得来.
assert_equal 1, 0.object_id
assert_equal 3, 1.object_id
assert_equal 5, 2.object_id
assert_equal 201, 100.object_id
end

# 其它对象的 object_id 是随机产生并且绝不重复的(包括 Float), object_id 也是判断对象是否为同一个对象的依据.
about_arrays.rb
1
2
3
4
5
6
7
8
9
10
11
12
def test_slicing_arrays
array = [:peanut, :butter, :and, :jelly]

# 这种用法对应于 Array#slice(start, length)
# 当 length 超过数组长度时忽略.
assert_equal [:and, :jelly], array[2,20]
# 当 length 为 0 时返回空数组
assert_equal [], array[4,0]
assert_equal [], array[4,100]
# 当 start 超过数组长度时返回 nil.
assert_equal nil, array[5,0]
end

array[4,0] 返回 [], 而 array[5,0] 却返回 nil. 为什么? Array 文档把这个称为 special cases. 从源代码里顺着 rb_ary_aref 方法继续下去就是 rb_ary_subseq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rb_ary_subseq(VALUE ary, long beg, long len)
{
VALUE klass;

// 如果长度大于数组长度, 返回 nil.
if (beg > RARRAY_LEN(ary)) return Qnil;
if (beg < 0 || len < 0) return Qnil;

if (RARRAY_LEN(ary) < len || RARRAY_LEN(ary) < beg + len) {
len = RARRAY_LEN(ary) - beg;
}
klass = rb_obj_class(ary);
// 如果长度等于 0, 返回空数组.
if (len == 0) return ary_new(klass, 0);

return ary_make_partial(ary, klass, beg, len);
}

不知道 Matz 当年是不是晃神了, 还是有意为之, 进而造成了下标 4 和 5 的不一致性, 反正这已经是即定事实了.

about_strings.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def test_flexible_quotes_can_handle_multiple_lines
long_string = %{# \n
It was the best of times,# \n
It was the worst of times.# \n
}

assert_equal 54, long_string.size
end

def test_here_documents_can_also_handle_multiple_lines
long_string = <<EOS
It was the best of times,# \n
It was the worst of times.# \n
EOS
assert_equal 53, long_string.size
end

# += 操作不会改变原始字符串.
def test_plus_equals_also_will_leave_the_original_string_unmodified
original_string = "Hello, "
hi = original_string
there = "World"
hi += there
assert_equal "Hello, ", original_string
end

# << 操作会修改源始字符串的.
def test_the_shovel_operator_modifies_the_original_string
original_string = "Hello, "
hi = original_string
there = "World"
hi << there
assert_equal "Hello, World", original_string
end

# 单引号字符串里也会发生转义.
def test_single_quotes_sometimes_interpret_escape_characters
string = '\\\''
assert_equal 2, string.size
assert_equal %{\\'}, string
end

def test_you_can_get_a_substring_from_a_string
string = "Bacon, lettuce and tomato"
# 可以像数组那样截取字符串分片
assert_equal 'let', string[7,3]
assert_equal 'let', string[7..9]
# 但是如果只取一个字符的话, 返回的是字符的 ASCII 码.
assert_equal 97, string[1]
end

def test_strings_can_be_split
string = "Sausage Egg Cheese"
words = string.split
# 字符串分割的默认参数是 $;
assert_equal ["Sausage", "Egg", "Cheese"], words
end

def test_strings_are_not_unique_objects
a = "a string"
b = "a string"
# 值相等.
assert_equal true, a == b
# 但却不是同一个对象.
assert_equal false, a.object_id == b.object_id
end

这里比较有意思的是 String#split 的默认值 $; . 它是一个全局变量, 它的唯一用途就是作为 split 方法的默认值出现, 它的初始值是 nil. 你可以通过修改这个全局变量来改变 split 方法的行为.

1
2
3
>> $; = 'b'
>> "abaabbbccdcbba".split
=> ["a", "aa", "", "", "ccdc", "", "a"]
about_symbols.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def test_identical_symbols_are_a_single_internal_object
symbol1 = :a_symbol
symbol2 = :a_symbol
# 符号是独一无二的.
assert_equal true, symbol1 == symbol2
assert_equal true, symbol1.object_id == symbol2.object_id
end

# all_symbols 方法可以返回所有符号, 这基中就包括方法名.
def test_method_names_become_symbols
symbols_as_strings = Symbol.all_symbols.map { |x| x.to_s }
assert_equal true, symbols_as_strings.include?("test_method_names_become_symbols")
end

# 在 MRI 版本里, 常量也是符号.
in_ruby_version("mri") do
RubyConstant = "What is the sound of one hand clapping?"
def test_constants_become_symbols
all_symbols = Symbol.all_symbols

assert_equal true, all_symbols.include?(:RubyConstant)
end
end

# 符号可以长的不太一样.
def test_symbols_with_spaces_can_be_built
symbol = :"cats and dogs"

assert_equal symbol, symbol.to_sym
end

# 符号也可以在内部插入值.
def test_symbols_with_interpolation_can_be_built
value = "and"
symbol = :"cats #{value} dogs"

assert_equal symbol, "cats and dogs".to_sym
end

# 符号不是"不可变的"字符串, 它没有字符串的方法.
def test_symbols_do_not_have_string_methods
symbol = :not_a_string
assert_equal false, symbol.respond_to?(:each_char)
assert_equal false, symbol.respond_to?(:reverse)
end

# 连最基本的连接操作都不允许.
def test_symbols_cannot_be_concatenated
assert_raise(NoMethodError) { :cats + :dogs }
end

# 符号可以动态创建, 但是不推荐这样做.
def test_symbols_can_be_dynamically_created
assert_equal :catsdogs, ("cats" + "dogs").to_sym
end
about_regular_expressions.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 字符串可以像这样查找内容.
def test_a_regexp_can_search_a_string_for_matching_content
assert_equal "match", "some matching content"[/match/]
end

# 找不到会返回 nil, 这种方式更像是条件判断.
def test_a_failed_match_returns_nil
assert_equal nil, "some matching content"[/missing/]
end

# 带星号的正则表达的十分贪婪.
def test_asterisk_means_zero_or_more
assert_equal "", "abbcccddddeeeee"[/z*/]
end

为什么会发生的情形? 看一下 Ruby 源代码里都做了什么.

1
2
3
4
5
6
7
8
9
10
# 当 String#slice 的第一个参数类型为 Regexp 时, 调用下面这个函数.
rb_str_subpat(VALUE str, VALUE re, VALUE backref)
{
if (rb_reg_search(re, str, 0, 0) >= 0) {
VALUE match = rb_backref_get();
int nth = rb_reg_backref_number(match, backref);
return rb_reg_nth_match(nth, match);
}
return Qnil;
}

事实上, 是 "abbcccddddeeeee".match(/z*/)[0] 返回了空字符串. 如果换成 "abbcccddddeeeee".match(/missing/) 就返回 nil 了.

1
2
3
4
# 这会儿又表现的十分容易满足, 这简直就是陷阱.
def test_the_left_most_match_wins
assert_equal "a", "abbccc az"[/az*/]
end

此处的表现十分怪异, 事实上 Ruby 正则也是遵从正常的贪婪原则的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>> "abb".match /ab*/
=> #<MatchData "abb">
>> "abb".match /ab*?/
=> #<MatchData "a">
>> "abb".match /ab+?/
=> #<MatchData "ab">
>> "abb".match /ab{0,9}/
=> #<MatchData "abb">

# 只是当出现在字符串末尾时就犯糊涂
>> "aabb".match /ab*/
=> #<MatchData "a">
>> "aabb".match /ab{0,9}/
=> #<MatchData "a">

看来以后星号正则要慎重使用了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# \w 里的连字符是下划线, 不包括减号.
def test_slash_w_is_a_shortcut_for_a_word_character_class
assert_equal "variable_1", "variable_1 = 42"[/[a-zA-Z0-9_]+/]
assert_equal "variable_1", "variable_1 = 42"[/\w+/]
end

# 不但能匹配, 还能选择返回第几个匹配结果.
def test_parentheses_also_capture_matched_content_by_number
assert_equal "Gray", "Gray, James"[/(\w+), (\w+)/, 1]
assert_equal "James", "Gray, James"[/(\w+), (\w+)/, 2]
end

# 每次匹配, 结果都会被记入全局变量中.
def test_variables_can_also_be_used_to_access_captures
assert_equal "Gray, James", "Name: Gray, James"[/(\w+), (\w+)/]
assert_equal "Gray", $1
assert_equal "James", $2
end
about_constants.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Animal
LEGS = 4
end

class MyAnimals
LEGS = 2

class Bird < Animal
def legs_in_bird
LEGS
end
end
end

# 嵌套常量赢过继承常量.
def test_who_wins_with_both_nested_and_inherited_constants
assert_equal 2, MyAnimals::Bird.new.legs_in_bird
end

class MyAnimals::Oyster < Animal
def legs_in_oyster
LEGS
end
end

# 如果定义成这样, 继承变量就赢了.
def test_who_wins_with_explicit_scoping_on_class_definition
assert_equal 4, MyAnimals::Oyster.new.legs_in_oyster
end
about_exceptions.rb
1
2
3
4
5
6
7
8
9
10
class MySpecialError < RuntimeError
end

# 自定义异常完整的继承树.
def test_exceptions_inherit_from_Exception
assert_equal RuntimeError, MySpecialError.ancestors[1]
assert_equal StandardError, MySpecialError.ancestors[2]
assert_equal Exception, MySpecialError.ancestors[3]
assert_equal Object, MySpecialError.ancestors[4]
end
about_blocks.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
# 块中的代码甚至会给局部变量带来副作用.
def test_block_can_affect_variables_in_the_code_where_they_are_created
value = :initial_value
method_with_block { value = :modified_in_a_block }
assert_equal :modified_in_a_block, value
end

def test_blocks_can_be_assigned_to_variables_and_called_explicitly
add_one = lambda { |n| n + 1 }
# 有两种方法调用 lambda 表达式.
assert_equal 11, add_one.call(10)
assert_equal 11, add_one[10]
end
about_scope.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class AboutScope
class String
end

# 当前范围中的类名拥有更高的优先级.
def test_bare_bones_class_names_assume_the_current_scope
assert_equal true, AboutScope::String == String
end

# 当前范围中的类名如果与 Ruby 库中的类名重复, 会造成很多不必要的麻烦.
def test_nested_string_is_not_the_same_as_the_system_string
assert_equal false, String == "HI".class
end

def test_use_the_prefix_scope_operator_to_force_the_global_scope
assert_equal true, ::String == "HI".class
end

MyString = ::String

# 类名其实只是常量而已.
def test_class_names_are_just_constants
assert_equal true, MyString == ::String
assert_equal true, MyString == "HI".class
end

def test_constants_can_be_looked_up_explicitly
assert_equal true, MyString == AboutScope.const_get("MyString")
end
end
about_class_methods.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Dog
end

# 实例也可以定义方法
def test_you_can_define_methods_on_individual_objects
fido = Dog.new
def fido.wag
:fidos_wag
end
assert_equal :fidos_wag, fido.wag
end

# 这个方法只属于这个实例, 不影响类的其它实例.
def test_other_objects_are_not_affected_by_these_singleton_methods
fido = Dog.new
rover = Dog.new
def fido.wag
:fidos_wag
end

assert_raise(NoMethodError) do
rover.wag
end
end

LastExpressionInClassStatement = class Dog; 21; end

# 你总能拿到最后一个表达式的结果.
def test_class_statements_return_the_value_of_their_last_expression
assert_equal 21, LastExpressionInClassStatement
end

SelfInsideOfClassStatement = class Dog; self; end

# 甚至是 self.
def test_self_while_inside_class_is_class_object_not_instance
assert_equal true, Dog == SelfInsideOfClassStatement
end
about_to_str.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CanBeTreatedAsString
def to_s
"string-like"
end

def to_str
to_s
end
end

# to_str 有更进一步的作用.
def test_to_str_allows_objects_to_be_treated_as_strings
assert_equal false, File.exist?(CanBeTreatedAsString.new)
end