Những tính năng mới trong Ruby 2.4
Bài đăng này đã không được cập nhật trong 8 năm
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 = " \n\t\n "
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
All rights reserved