Những tính năng mới trong Ruby 2.4
Thực thi Regular Expression nhanh hơn với Regexp#match? Ruby 2.4 thêm một phương thức mới là #match? cho regular expression. Phương thức này nhanh gấp 3 lần so với tất cả các phương thức khác trong class Regexp ở Ruby 2.3: require 'benchmark/ips' Benchmark . ips do | bench | ...
Thực thi Regular Expression nhanh hơn với Regexp#match?
Ruby 2.4 thêm một phương thức mới là #match? cho regular expression. Phương thức này nhanh gấp 3 lần so với tất cả các phương thức khác trong class Regexp ở Ruby 2.3:
require 'benchmark/ips' Benchmark.ips do |bench| EMPTY_STRING = ' WHITESPACE = " " CONTAINS_TEXT = ' hi ' PATTERN = /A[[:space:]]*z/ bench.report('Regexp#match?') do PATTERN.match?(EMPTY_STRING) PATTERN.match?(WHITESPACE) PATTERN.match?(CONTAINS_TEXT) end bench.report('Regexp#match') do PATTERN.match(EMPTY_STRING) PATTERN.match(WHITESPACE) PATTERN.match(CONTAINS_TEXT) end bench.report('Regexp#=~') do PATTERN =~ EMPTY_STRING PATTERN =~ WHITESPACE PATTERN =~ CONTAINS_TEXT end bench.report('Regexp#===') do PATTERN === EMPTY_STRING PATTERN === WHITESPACE PATTERN === CONTAINS_TEXT end bench.compare! end # >> Warming up -------------------------------------- # >> Regexp#match? 160.255k i/100ms # >> Regexp#match 44.904k i/100ms # >> Regexp#=~ 71.184k i/100ms # >> Regexp#=== 71.839k i/100ms # >> Calculating ------------------------------------- # >> Regexp#match? 2.630M (± 4.0%) i/s - 13.141M in 5.004929s # >> Regexp#match 539.361k (± 3.9%) i/s - 2.694M in 5.002868s # >> Regexp#=~ 859.713k (± 4.2%) i/s - 4.342M in 5.060080s # >> Regexp#=== 872.217k (± 3.5%) i/s - 4.382M in 5.030612s # >> # >> Comparison: # >> Regexp#match?: 2630002.5 i/s # >> Regexp#===: 872217.5 i/s - 3.02x slower # >> Regexp#=~: 859713.0 i/s - 3.06x slower # >> Regexp#match: 539361.3 i/s - 4.88x slower
Khi các bạn gọi các method như Regexp#===, Regexp#=~ hoặc Regexp#match, Ruby sẽ gán kết quả MatchData vào biến toàn cục $~:
/^foo (w+)$/ =~ 'foo bar' # => 0 $~ # => #<MatchData "foo bar" 1:"bar"> /^foo (w+)$/.match('foo baz') # => #<MatchData "foo baz" 1:"baz"> $~ # => #<MatchData "foo baz" 1:"baz"> /^foo (w+)$/ === 'foo qux' # => true $~ # => #<MatchData "foo qux" 1:"qux">
Regexp#match? chỉ trả về kết quả là boolean và bỏ qua việc tạo đối tượng MatchData hoặc update lại biến toàn cục:
/^foo (w+)$/.match?('foo wow') # => true $~ # => nil
Phương thức sum mới cho module Enumerable
Ở phiên bản mới này, bạn có thể gọi phương thức #sum cho bất kỳ đối tượng Enumerable nào:
[1, 2, 3, 4, 5].sum # => 15
Phương thức #sum có thể nhận thêm một tham số tùy chọn và nó có giá trị default là 0. Tham số này là khởi đầu của việc tính tổng, cho nên [].sum sẽ trả về 0.
Nếu các bạn gọi hàm #sum trên một mảng các đối tượng không phải là số (Integer) thì bạn phải tự thiết lập giá trị khởi đầu này:
class ShoppingList attr_reader :items def initialize(*items) @items = items end def +(other) ShoppingList.new(*items, *other.items) end end eggs = ShoppingList.new('eggs') # => #<ShoppingList:0x007f952282e7b8 @items=["eggs"]> milk = ShoppingList.new('milks') # => #<ShoppingList:0x007f952282ce68 @items=["milks"]> cheese = ShoppingList.new('cheese') # => #<ShoppingList:0x007f95228271e8 @items=["cheese"]> eggs + milk + cheese # => #<ShoppingList:0x007f95228261d0 @items=["eggs", "milks", "cheese"]> [eggs, milk, cheese].sum # => #<TypeError: ShoppingList can't be coerced into Integer> [eggs, milk, cheese].sum(ShoppingList.new) # => #<ShoppingList:0x007f9522824cb8 @items=["eggs", "milks", "cheese"]>
Như bạn đã thấy, nếu không tự thiết lập giá trị khởi đầu đúng với đối tượng trong Enumerable thì phương thức #sum sẽ hiểu là 0 + ShoppingList instance và từ đó sinh ra lỗi TypeError như trên.
Phương thức mới để kiểm tra thư mục hoặc file rỗng
Trong Ruby 2.4, bạn có thể kiểm tra xem thư mục hoặc file có rỗng hay không bằng cách sử dụng các module File và Dir:
Dir.empty?('empty_directory') # => true Dir.empty?('directory_with_files') # => false File.empty?('contains_text.txt') # => false File.empty?('empty.txt') # => true
Phương thức File.empty? tương đương với phương thức File.zero? đã có sẵn trong các phiên bản Ruby trước:
File.zero?('contains_text.txt') # => false File.zero?('empty.txt') # => true
Lấy các named capture từ kết quả trả về của Regexp
Trong Ruby 2.4, bạn có thể gọi phương thức #named_captures trên đối tượng kết quả trả về của một Regexp và nó sẽ cho bạn một hash chứa named captures và dữ liệu matching của nó:
pattern = /(?<first_name>John) (?<last_name>w+)/ pattern.match('John Backus').named_captures # => { "first_name" => "John", "last_name" => "Backus" }
Ruby 2.4 còn thêm phương thức #values_at để lấy ra named capture mà bạn quan tâm:
pattern = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/ pattern.match('2016-02-01').values_at(:year, :month) # => ["2016", "02"]
Phương thức Integer#digits
Nếu bạn muốn truy cập vào một vị trí nào đó trong một số (Integer) thì bạn có thể gọi phương thức Integer#digits:
123.digits # => [3, 2, 1] 123.digits[0] # => 3 # Equivalent behavior in Ruby 2.3: 123.to_s.chars.map(&:to_i).reverse # => [3, 2, 1]
Cải tiến interface của Logger
Trong Ruby 2.3, việc thiết lập Logger khá là phức tạp:
logger1 = Logger.new(STDOUT) logger1.level = :info logger1.progname = 'LOG1' logger1.debug('This is ignored') logger1.info('This is logged') # >> I, [2016-07-17T23:45:30.571508 #19837] INFO -- LOG1: This is logged
Với Ruby 2.4, hàm khởi tạo của Logger nhận các options này:
logger2 = Logger.new(STDOUT, level: :info, progname: 'LOG2') logger2.debug('This is ignored') logger2.info('This is logged') # >> I, [2016-07-17T23:45:30.571556 #19837] INFO -- LOG2: This is logged
Phân tích (parse) các CLI option thành Hash
Việc phân tích các cờ của command line với OptionParser cần rất nhiều đoạn code để có thể đưa nó vào một hash:
require 'optparse' require 'optparse/date' require 'optparse/uri' config = {} cli = OptionParser.new do |options| options.define('--from=DATE', Date) do |from| config[:from] = from end options.define('--url=ENDPOINT', URI) do |url| config[:url] = url end options.define('--names=LIST', Array) do |names| config[:names] = names end end
Với Ruby 2.4, bạn có thể cung cấp một tham số keyword là :into khi phân tích các cờ của command line:
require 'optparse' require 'optparse/date' require 'optparse/uri' cli = OptionParser.new do |options| options.define '--from=DATE', Date options.define '--url=ENDPOINT', URI options.define '--names=LIST', Array end config = {} args = %w[ --from 2016-02-03 --url https://blog.blockscore.com/ --names John,Daniel,Delmer ] cli.parse(args, into: config) config.keys # => [:from, :url, :names]
Cải tiến về tốc độ đối với phương thức max, min của Array
Array trong Ruby 2.4 được định nghĩa các phương thức #min và #max riêng của mình. Điều này làm cho tốc độ của 2 phương thức này trên các Array được cải thiện đáng kể:
Array#min: 35.1 i/s Enumerable#min: 21.8 i/s - 1.61x slower
Đơn giản hóa các lớp Integer
Với các phiên bản Ruby trước đây, các bạn sẽ phải làm việc với các kiểu số khác nhau:
# Find classes which subclass the base "Numeric" class: numerics = ObjectSpace.each_object(Module).select { |mod| mod < Numeric } # In Ruby 2.3: numerics # => [Complex, Rational, Bignum, Float, Fixnum, Integer, BigDecimal] # In Ruby 2.4: numerics # => [Complex, Rational, Float, Integer, BigDecimal]
Ở Ruby 2.4, Fixnum và Bignum sẽ được Ruby xử lý thay bạn. Nó sẽ giúp bạn tránh được một số bug như sau:
def categorize_number(num) case num when Fixnum then 'fixed number!' when Float then 'floating point!' end end # In Ruby 2.3: categorize_number(2) # => "fixed number!" categorize_number(2.0) # => "floating point!" categorize_number(2 ** 500) # => nil # In Ruby 2.4: categorize_number(2) # => "fixed number!" categorize_number(2.0) # => "floating point!" categorize_number(2 ** 500) # => "fixed number!"
Nếu các bạn vẫn sử dụng các constant Bignum và Fixnum trong source code của mình thì cũng không vấn đề gì. Các constant này đều có giá trị là Integer:
Fixnum # => Integer Bignum # => Integer
Tham số mới hỗ trợ các phương thức float
Trong ruby 2.4, #round, #ceil, #floor, và #truncate đều nhận thêm một tham số làm tròn
4.55.ceil(1) # => 4.6 4.55.floor(1) # => 4.5 4.55.truncate(1) # => 4.5 4.55.round(1) # => 4.6
Những phương thức này cũng hoạt động trên cả Integer.
Case sensitivity với các kí tự Unicode
Hãy xem xét đến string sau:
My name is JOHN. That is spelled J-Ο-H-N
Khi gọi #downcase ở Ruby 2.3, kết quả của nó sẽ là:
my name is john. that is spelled J-Ο-H-N
Lý do là bởi "J-Ο-H-N" trong string trên được viết bởi các ký tự Unicode
Với Ruby 2.4 thì việc này đã được xử lý:
sentence = "uff2a-u039f-uff28-uff2e" sentence # => "J-Ο-H-N" sentence.downcase # => "j-ο-h-n" sentence.downcase.capitalize # => "J-ο-h-n" sentence.downcase.capitalize.swapcase # => "j-Ο-H-N"
Tùy chọn mới để thiết lập độ dài của một string
Khi tạo một string, bạn có thể định nghĩa một option :capacity. Nó sẽ cho Ruby biết là bao nhiêu bộ nhớ sẽ được sử dụng cho string của bạn. Điều này sẽ làm tăng performance của bạn khi mà nó giúp Ruby tránh việc khởi tạo bộ nhớ liên tục khi mà bạn tăng kích thước của string:
require 'benchmark/ips' Benchmark.ips do |bench| bench.report("Without capacity") do append_me = ' ' * 1_000 template = String.new 100.times { template << append_me } end bench.report("With capacity") do append_me = ' ' * 1_000 template = String.new(capacity: 100_000) 100.times { template << append_me } end bench.compare! end # >> Warming up -------------------------------------- # >> Without capacity 1.690k i/100ms # >> With capacity 3.204k i/100ms # >> Calculating ------------------------------------- # >> Without capacity 16.031k (± 7.4%) i/s - 160.550k in 10.070740s # >> With capacity 37.225k (±18.0%) i/s - 362.052k in 10.005530s # >> # >> Comparison: # >> With capacity: 37225.1 i/s # >> Without capacity: 16031.3 i/s - 2.32x slower
Sửa lại chức năng matching đối với Symbol
Trong Ruby 2.3, Symbol#match trả về vị trí match của Regexp khi mà String#match lại trả về đối tượng MatchData. Ruby 2.4 đã sửa lại điều này:
# Ruby 2.3 behavior: 'foo bar'.match(/^foo (w+)$/) # => #<MatchData "foo bar" 1:"bar"> :'foo bar'.match(/^foo (w+)$/) # => 0 # Ruby 2.4 behavior: 'foo bar'.match(/^foo (w+)$/) # => #<MatchData "foo bar" 1:"bar"> :'foo bar'.match(/^foo (w+)$/) # => #<MatchData "foo bar" 1:"bar">
Cải tiến việc thông báo các Exception cho threading
Nếu bạn gặp phải exception trong một thread thì thông thường, Ruby sẽ che đi exception này:
# parallel-work.rb puts 'Starting some parallel work' thread = Thread.new do sleep 1 fail 'something very bad happened!' end sleep 2 puts 'Done!'
$ ruby parallel-work.rb
Starting some parallel work
Done!
Nếu bạn muốn raise exception cho toàn bộ process khi có exception xảy ra trong một thread, thì bạn có thể dùng Thread.abort_on_exception = true. Nếu thêm dòng này vào đoạn code trên thì output sẽ như sau:
$ ruby parallel-work.rb Starting some parallel work parallel-work.rb:9:in 'block in <main>': something very bad happened! (RuntimeError)
Trong Ruby 2.4, bạn có thể đưa ra thông báo lỗi mà không cần phải dừng hẳn chương trình bằng cách sử dụng Thread.report_on_exception = true. Output:
$ ruby parallel-work.rb Starting some parallel work #<Thread:0x007ffa628a62b8@parallel-work.rb:6 run> terminated with exception: parallel-work.rb:9:in 'block in <main>': something very bad happened! (RuntimeError) Done!
Bài viết được dịch từ New feature in Ruby 2.4