从实战项目总结的Ruby小技巧(第一部分)

从我们在Global Personals项目中使用Github并且following Github Flow开始到现在已经将近两年的时间。在这段时间中,我们以很高的频率提交了上千次的pull请求,虽然没有太多如何改善或提高程序的建议和想法,但是我仍获得了如此广泛和珍贵的经验。其中,有一些建议是和项目相关的,同时,也包含了大量可以在团队内分享的Ruby开发小技巧。

由于我担心将从这个项目中获得和学习到的如此珍贵的技巧和经验所遗忘,于是我挑出了其中最好的最有价值的部分和大家分享,同时进行了一点小小的扩展。每个人都有自己的工作方式和风格,所以我会简洁明了地和大家阐述。并不是每部分内容对每个人来说都是新的,但是希望你在这里可以或多或少都有所收获。

我将文章分为了几块内容,以免你一口气读完5000个字,同时将它们归类为几个部分以便于参考。

  • 1. 代码块(Blocks) 和 区间(Ranges)
  • 2. 重构(Destructuring) 和 转换方法(Conversion Methods)
  • 3. 异常(Exceptions)和模块(Modules)
  • 4. 调试(Debugging),项目结构(Project Layout)和文档(Documentation)
  • 5. 其他(Odds and Ends)

 

让我们进入第一部分。

 

代码块(Blocks)

代码块是Ruby非常重要的一部分,你随处都见到它们被广泛使用。如果你没有使用,那么你将发现许多人使用方法关联代码块,甚至仅仅是让代码结构变得清晰而已。

代码块有三种主要的作用:循环(looping),初始化和销毁(setup and teardown),以及回调和延迟执行(callbacks or deferred action)。

下面这个例子演示了如何使用代码块循环输出菲波那切数列。它使用block_given?方法判断是否关联了一个代码块,否则将从当前方法返回一个枚举器。

yield关键字用来在方法中执行一个代码块,它的参数将传递给代码块。当代码块执行完毕,将返回调用方法,并执行下一行代码。方法返回值为在最大数(max)之前的最后一个菲波那切数。


def fibonacci(max=Float::INFINITY)
      return to_enum(__method__, max) unless block_given?
      yield previous = 0 
      while (i ||= 1) < max 
        yield i 
        i, previous = previous + i, i 
      end 
      previous
    end

    fibonacci(100) {|i| puts i }

下一个例子将把设置、销毁以及错误处理等操作代码放到方法中,将方法的主要逻辑放到代码块中。通过这种方式,样板代码就不需要在多个地方重复,另外当你需要改变错误处理代码时,只需要做少量的修改。

yield语句的返回结果,即代码块的返回值,将保存到一个局部变量中。这样,可以将代码块的执行结果作为方法的返回值。


require "socket"

    module SocketClient
      def self.connect(host, port)
        sock = TCPSocket.new(host, port)
        begin
          result = yield sock
        ensure
          sock.close
        end 
        result
      rescue Errno::ECONNREFUSED
      end 
    end 

    # connect to echo server, see next example 
    SocketClient.connect("localhost", 55555) do |sock|
      sock.write("hello")
      puts sock.readline
    end

下一个例子不会使用yield关键字。这里有另外一种使用代码块的方法:将‘&’作为方法最后一个参数的前缀,将把关联代码块作为一个Proc对象保存到此参数当中。Proc对象拥有一个call实例方法,可以用来执行代码块,传递给call方法的参数将作为代码块的参数。在这个例子中,你可以保存代码块最为一个回调稍后执行,或者在你需要的时候执行延迟的操作。


require "socket"

    class SimpleServer
      def initialize(port, host="0.0.0.0")
        @port, @host = port, host
      end 

      def on_connection(&block)
        @connection_handler = block
      end 

      def start 
        tcp_server = TCPServer.new(@host, @port)
        while connection = tcp_server.accept
          @connection_handler.call(connection)
        end 
      end 
    end 

    server = SimpleServer.new(5555)
    server.on_connection do |socket|
      socket.write(socket.readline)
      socket.close
    end 
    server.start

区间(Ranges)

区间在Ruby代码中也很常见,通常区间的形式是(0..9),包含0到9之间的所有数字,包括9。也有另一种形式(0…10),包含0到10之间的所有数字,不包括10,即和前一种形式都包含0到9之间的所有数字,包括9。这种形式并不常见,但有时却非常有用。

有时候你会看到像这样的代码:


  random_index = rand(0..array.length-1)

使用不包含结尾的区间将更加整洁:


  random_index = rand(0...array.length)

有时候,使用不包含结尾的区间将更加简洁和直观。例如,eb_first…march_first可以更加简单的计算出今年二月的天数,同时,1…Float::INFINITY可以更加直观的表示出所有正整数,由于无穷大infinity不是一个数字。

区间是优秀的数据结构,因为它允许你定义一个巨大的集合而不需要在内存中实实在在的创造出整个集合。你必须小心你所使用的方法,因为某些区间操作可能导致整个集合被创建。

实例方法each显而易见会创造出整个区间,但这通常只会发生在每次使用第一个对象的时候,例如(1..Float::INFINITY).each {|i| puts i },在没有输出任何信息之前,事实上不可能用尽所有可用内存。区间对象中,mixin Enumerable所获得的方法依赖于each方法,所以它们也具有相同的行为。

区间有include?和cover?两个实例方法来测试一个值是否属于区间。include?方法使用each方法迭代整个区间来检测值是否存在于区间中,cover?方法只是简单比较值是否大于区间的开头,并且小于等于区间的结尾(对于不包含结尾元素的区间是小于区间的结尾)。这两个方法是不可以等价互换的,可能由于区间建立方式的不同和排序方式的不同,而导致意象不到的结果。


  ("a".."z").include?("ab")     # => false
  ("a".."z").cover?("ab")       # => true

Ruby中许多类都可以进行区间操作,同样的,你也可以很容易地让自定义的类进行区间操作。

首先,你需要在类中实现称之为‘太空船’<=>操作符的方法。在这个方法中,如果other参数大于self返回-1,如果小于返回1,如果相等则返回0。一般情况下,如果比较是不合法的则返回nil。

下面的例子中,简单的代理了String#casecmp方法,这个实例方式是一个大小写不敏感的字符串比较方法,返回上面叙述的格式。


 class Word
      def initialize(string)
        @string = string
      end 

      def <=>(other)
        return nil unless other.is_a?(self.class)
        @string.casecmp(other.to_s)
      end 

      def to_s
        @string
      end 
    end

这样的话,你便可以创建一个区间,通过实例方法cover?测试成员关系,但这通常不是一种非常好的做法。


    dictionary = (Word.new("aardvark)..Word.new("xylophone")
    dictionary.cover?(Word.new("derp"))   # => true

如果你想要迭代整个区间,生成一个数组,或者是使用实例方法include?测试成员关系,你需要实现succ方法,这个方法产生序列中的下一个对象。


 class Word
      DICTIONARY = File.read("/usr/share/dict/words").each_line.map(&:chomp)
      DICTIONARY_INDEX = (0...DICTIONARY.length)
      include Comparable

      def initialize(string, i=nil)
        @string, @index = string, i
      end

      def <=>(other)
        return nil unless other.is_a?(self.class)
        @string.casecmp(other.to_s)
      end

      def succ
        i = index + 1
        string = DICTIONARY[i]
        self.class.new(string, i) if string
      end

      def to_s
        @string
      end

      private

      def index
        return @index if @index
        if DICTIONARY_INDEX.respond_to?(:bsearch) # ruby >= 2.0.0
          @index = DICTIONARY_INDEX.bsearch {|i| @string.casecmp(DICTIONARY[i])}
        else
          @index = DICTIONARY.index(@string)
        end
      end
    end

你会注意到我同时也mixin了Comparable模块,这样在定义了实例方法<=>之后,类也拥有了实例方法==(同时也拥有了实例方法>,<等),而不是继承自Object类。

现在我们的区间将变得更加强大


  dictionary = (Word.new("aardvark")..Word.new("xylophone"))
    dictionary.include?(Word.new("derp"))                  #=> false

    (Word.new("town")..Word.new("townfolk")).map(&:to_s)   #=> ["town", "towned", "townee", "towner", "townet", "townfaring", "townfolk"]

下一部分内容将会很快和大家见面,在Twitter上follow我们将会即时收到最新的消息……