Why Y

Metaprogramming Ruby Notes 1

这些天在看《ruby元编程》。其中有一个关于block的小测验,要模仿C#中的using关键字。

在Ruby中,using可以通过内核方法写一个。内核方法即添加到Kernel模块中的方法,因此可以在程序的任何地方使用,有点像一个关键字。书中给的答案可以通过测试,但是仔细研究发现书中实现的using貌似有些问题,而且是大问题。实现using的目的在于在block中使用资源,使用完毕后自动将资源释放。因此using后面的block一定有一个参数代表资源,using的实现也应该将资源传递给block,所以正确的实现应该如下。

using.rb
1
2
3
4
5
6
7
module Kernel
  def using(resource)
    yield resource
  ensure
    resource.dispose
  end
end

书中的实现在yield后没有resource,无法将资源传递给block。不能在block中使用资源,写这玩意儿有啥用?我看的版本是中文版,不清楚原版是否也有这个错误。另外,书中使用的是begin ... ensure ... end,在用def定义方法时begin是可以省略的。

之所以会出现这个问题,我认为是单元测试的用例写的不够严格。下面是与书中原测试等价的测试用例。

using_test.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
require 'test/unit'
require './using.rb'

class Resource
  def dispose
    @dispose = true
  end
  def disposed?
    @dispose
  end
end

class TestUsing < Test::Unit::TestCase
  def setup
    @r = Resource.new
  end

  def test_disposes_of_resources
    using(@r) { }
    assert @r.disposed?
  end

  def test_disposes_of_resources_in_case_of_exception
    assert_raises(Exception) do
      using(@r) do
        raise Exception
      end
    end
  end
end

可以看到,两个测试在block中都没有使用资源。测试的不严谨导致了这个using的大bug。至少应该再添加一个测试,比如:

using_test_add.rb
1
2
3
4
5
def test_with_block_argument
  using(@r) do |r|
    r.disposed?
  end
end

添加这个测试后,书中原代码会无法通过测试,因此可以发现这一bug。

后记

刚写完上面的,正在想怎么总结结尾的时候,一个想法忽然在脑海中闪现出来:我是否真的理解了书中设计的using的用法。按照我自己的想法,using的参数是一个实现了dispose方法的资源,后面的block是单形参的。这样using将资源传递给block,在block中使用形参代表资源。后来想了想,using后面的block其实也可以访问到外面的资源,没有必要设置一个参数来传递资源。比如:

using_test.rb
1
2
3
4
5
6
7
8
def test_using
  using(@r) do
    # something using @r
    # ...
    @r.disposed?
  end
  assert @r.disposed?
end

改完测试,运行通过,那这篇博文怎么办… 删还是不删,这是一个问题…

后记的后记

终于想出一个不用删掉这篇文章的理由,那就是通过参数传递资源的方式要灵活那么一点。书上的写法因为没有使用参数,所以using后的block必须定义在要立即使用资源的地方,当然C#中的using大约也是这样使用的。但如果资源是通过block的参数传递的,那么这个block就可以通过 Proc.new 或者 lambda 定义在任何地方。比如:

using_define_anywhere.rb
1
2
3
4
5
6
7
def test_define_anywhere
  p = Proc.new do |r|
    r.disposed?
  end
  using(@r, &p)
  assert @r.disposed?
end

Xfce桌面按钮扩展

前几天更新了一下 Debian 系统,下载了几百兆的更新包,当时用的没问题挺好。昨天再开机后发现 xfc 的面板有问题,最大的变化是窗口按钮不再是扩展的,也就是它的长度会随着打开窗口的数量而变化。我用的面板是垂直的,窗口按钮下面还有其他的项目,原来后面这些项目都是从屏幕底端倒着排列的,中间剩下的空间全都是窗口按钮。现在窗口按钮初始的长度几乎为零,后面的项目紧接着排在后面,这样屏幕底端变成空的了,不喜欢这样的布局。Google 这个问题后在 xfce 官网的关于找到了答案。

xfce

Ruby StringScanner

想写一个简单的解释器,源代码用字符串表示。本来想自己写一个 scanner,后来发现 Ruby 库中有一个 StringScanner 类,但不是用 Ruby 写的,不能做源码浏览了。在这里记录一下 StringScanner 的用法。

<b> strscan </b>

要用 StringScanner 类需要 require 'strscan' ,以前老是忘了这个,这次一定要记住。

按照 StringScanner 官方文档的说法,它的用处是对字符串进行词法扫描操作。我用的最多的是 scan 方法,它与 String#scan 最大的不同是内部维护一个 position 变量,使得每次扫描都从position开始而不是开头。

常用方法

StringScanner.new 构造一个 StringScanner 对象,参数是待扫描的字符串。

StringScanner.new
1
s = StringScanner.new '(+ a 2)'

StringScanner#eos? 判断是否扫描完毕。

StringScanner#scan 参数 pattern 为一个正则表达式,尝试从当前位置与 pattern 匹配,如果成功那么 scanner 前进到下一个待扫描位置并返回匹配的字符串,前进的数目与匹配字符串的长度相等。匹配失败的话返回 nil

StringScanner#scan
1
2
3
4
5
6
s = StringScanner.new('test string')
p s.scan(/\w+/)   # -> "test"
p s.scan(/\w+/)   # -> nil
p s.scan(/\s+/)   # -> " "
p s.scan(/\w+/)   # -> "string"
p s.scan(/./)     # -> nil

StringScanner#peek 接受一个数字类型的参数 len,返回 string[pos, len],也就是即将被扫描的字符串,不推进扫描指针。

StringScanner#checkscan 类似,但是不推进扫描指针。