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

这是我们从两年项目经验中获得的ruby小技巧的第二部分.击这里查看第一部分,第一部分包含代码块(Blocks)和区间(Ranges)

 拆分和重构(Destructuring)

你或许会对ruby的合成/拆分符印象深刻,例子如下:


attrs = [:data, :cache]
    attr_accessor *attrs  # destructure array into an arguments list 
    private *attrs

    def hyphenate(*words) # restructure arguments list into an array 
      words.join("-")
    end

同样,可以在赋值时进行这样的操作,下一个例子中,将拆分一个区间(Range),同时,将拆分后中间的元素收集到body中合成一个数组


head, *body, tail = *(1..10)
    head  #=> 1
    body  #=> [2, 3, 4, 5, 6, 7, 8, 9]
    tail  #=> 10

当进行并行赋值时,如果右值是一个数组,则会被自动拆分。当你有一个方法返回一个数组时,这种方式很有效。


family, port, host, address = socket.peeraddr

你同样可以利用这种方式获取数组的第一个元素。


 family, = socket.peeraddr

然而,可以使用实例方式first以更优雅的方式来获


    family = socket.peeraddr.first

你可以利用哈希(Hash)values_at实例方法来返回一个数组


  first, last = params.values_at(:first_name, :last_name)

在传递代码块参数的时候将发生隐式拆分。


names = ["Arthur", "Ford", "Trillian"]
    ids = [42, 43, 44]
    id_names = ids.zip(names)   #=> [[42, "Arthur"], [43, "Ford"], [44, "Trillian"]]
    id_names.each do |id, name|
      puts "user #{id} is #{name}"
    end

使用括号你可以更深的层次拆分。


id_names = [[42, ["Arthur", "Dent"]], [43, ["Ford", "Prefect"]], [44, ["Tricia", "McMillan"]]]
    id_names.each do |id, (first_name, last_name)|
        puts "#{id}\t#{last_name}, #{first_name[0]}."
    end

方法在传递参数的时候这种方式同样可行!


def euclidean_distance((ax, ay), (bx, by))
      Math.sqrt((ax - bx)**2 + (ay - by)**2)
    end

    euclidean_distance([1, 5], [4, 2])   #=> 4.242640687119285

Ruby,你可以通过两种方式实现这种拆分的功能.第一种方式,你可以通过‘*’作用与一个对象。调用对象的to_a实例方法进行拆分


class Point 
        attr accessor :x, :y 
        def initialize(x, y)
          @x, @y = x, y
        end 

        def to_a
          [x, y]
        end 
    end 

    point = Point.new(6, 3)
    x, y = *point
    x   #=> 6
    y   #=> 3

第二种方式,对于隐式拆分,需要使用to_ary实例方法。你可以选择性的实现这个方法,由于它会使你的对象在你没有预料到的地方突然表现出像数组(Array)一样的行为


class Point
      attr_accessor :x, :y
      def initialize(x, y)
        @x, @y = x, y
      end

      def to_ary
        [x, y]
      end
    end

    point = Point.new(6, 3)
    x, y = point
    x   #=> 6
    y   #=> 3

    points = [Point.new(1, 5), Point.new(4, 2)]
    points.each do |x, y|
      ...
    end

    # using our distance method from an earlier example
    euclidean_distance(Point.new(1, 5), Point.new(4, 2))    #=> 4.242640687119285

最后一种使用‘*’符的特殊情形,是在子类覆写的方法中使用super调用父类的同名方法

事实上,你没有必要写一大堆为了增加附加行为的参数,你只需要传递一个‘*’,在方法中仅仅使用super,就可以把所有参数传递给父类方法。


class LoggedReader < Reader
      def initialize(*)
        super
        @logger = Logger.new(STDOUT)
      end

      def read(*)
        result = super
        @logger.info(result)
        result
      end
    end

 

转换方法(Conversion methods)

我们刚刚看到,可以通过显式或隐式的方法将一个对象转换为一个数组(Array).。这些方法可以被认为是严格或非严格的转化方法,其他的类型同样也有一些这样的方法

非严格的转换方法你或许知道一些,,例如#to_a, #to_i,#to_s以及Ruby2.0中的#to_h和少数的其他方法。大多数类型都具有这些方法,包括nil


{:foo => 1, :bar => 2}.to_a   # => [[:foo, 1], [:bar, 1]]
    nil.to_a                      # => []
    "3".to_i                      # => 3
    "foo".to_i                    # => 0 
    nil.to_i                      # => 0
    [1, 2, 3].to_s                # => "[1, 2, 3]"
    nil.to_s                      # => ""
    nil.to_h                      # => {}

有时,这些非严格的转换方法会使你犯某些错误,例如,传递一些非法的数据使系统崩溃,返回一些错误的结果,或者产生一些意想不到的错误。


USERS = ["Arthur", "Ford", "Trillian"]

    def user(id)
      USERS[id.to_i]
    end 

    user(nil)     # => "Arthur" # oops!

对于一些类型Ruby提供了更加严格的转化方法,例如,#to_ary, #to_int, 以及#to_str

这些方法只在对严格转换敏感的特定类中实现,例如,实例方法#to_int只对于数字类可用。


def user(id)
      USERS[id.to_int]
    end 

    user(nil)     # => NoMethodError: undefined method 'to_int' for nil:NilClass

这种方式更好一点,我们在合适的地方获得了一个异常,但是这个异常并不能很好表达我们的意图。

另外,由于String类没有实现实例方法#to_int,对于String类我们需要覆写原始的方法。

但是Ruby拥有另一系列转换方法,这些方法更加智能。这些方法中,最有用的方法是Array()Integer()(同时还有其他转换方法例如Float())。其他一些方法例如String()Hash()使用情况较少,他们仅仅是代理到实例方法#to_s和实例方法#to_h


def user(id)
      USERS[Integer(id)]
    end 

    user(nil)     # => TypeError: can't convert nil into Integer

现在我们的例子将抛出一个异常可以很好展示我们的意图,另外我们也可以使用字符串。

这也是Integer()方法更加优雅的地方。


 "1".to_i        # => 1
    Integer("1")    # => 1

    "foo".to_i      # => 0
    Integer("foo")  # => ArgumentError: invalid value for Integer(): "foo"

Array()同样非常有用,当它不能将参数转换成一个数组时,它将把参数放到一个空数组中。


Array([1, 2])
    Array(1)

偶尔,你需要定义一个方法接受一个对象或者一个对象数组,你可以使用Array()而避免明确的类型检查。


def project_cost(hours, developer)
      developers = Array(developer)
      avg_rate = developers.inject(0) {|acc, d| acc + d.rate } / developers.length
      hours * avg_rate
    end

正如上面提到的实例方法#to_ary,这类转换方法中的一些被Ruby解释器内部使用,这些方法是严格转换方法,同样也可以作为隐式转换方法。由于它们是严格的所以可以被作为隐式转换方法所使用。

这些方法在Ruby中被广泛的使用,例如实例方法#to_int用来将传递给Array#[]的参数转换为int,当raise的参数不是一个Exception对象时,实例方法#to_str用来将被用来转换。


class Line
      def initialize(id)
        @id = id 
      end 
      def to_int
        @id 
      end 
    end 

    line = Line.new(2)
    names = ["Central", "Circle", "District"]
    names[line]     # => "District"

    class Response
      def initialize(status, message, body)
        @status, @message, @body = status, message, body
      end 

      def to_str
        "#{@status} #{@message}"
      end
    end 

    res = Response.new(404, "Not Found", "")
    raise res       # => RuntimeError: 404 Not Found

最后一个要提到的转换方法是#to_proc。这个方法在一个方法的参数中使用一元符‘&’时将被调用。

在一个方法调用中,‘&’将一个Proc对象转换为一个block参数。


 sum = Proc.new {|a, b| a + b }

    (1..10).inject(&block)    # => 55

如果‘&’直接作用于一个参数,则会隐式调用实例方法#to_proc将操作数转变为一个block

Ruby代码中,一种非常常见的做法是将其作用于一个Symbol对象。


   ["foo", "bar", "baz"].map(&:upcase)   #=> ["FOO", "BAR", "BAZ"]

这中方式内建于Ruby1.9以后的版本,对于以前的版本你可以像这样自己实现:


class Symbol
      def to_proc
        # call the method named by this Symbol on the supplied object
        Proc.new {|obj| obj.send(self) }
      end 
    end

下面的例子使用实例方法#to_proc‘&’来从一个数组(Array)初始化Point对象。


class Point
      attr_accessor :x, :y
      def initialize(x, y)
        @x, @y = x, y
      end

      def self.to_proc
        Proc.new {|ary| new(*ary)}
      end
    end

    [[1, 5], [4, 2]].map(&Point)   #=> [#, #]

接下来进入第三部分…

伯乐在线注:@geekerzp 正在翻译第 3 部分和第 4 部分,各位敬请期待 🙂