Type coercion (ép kiểu) là việc thay đổi kiểu của object sang kiểu khác, cùng với giá trị. Ví dụ: đổi Integer thành String với #to_s hoặc Float thành Integer với #to_i. Các phương thức ít biết hơn #to_str và #to_int mà một số object implement làm giống hệt ở nhìn đầu, nhưng có sự khác biệt.
Trong bài AppSignal Academy này, chúng ta sẽ đi sâu vào explicit casting (ép kiểu tường minh) và implicit coercion (ép kiểu ngầm) trong Ruby, nhắc nhẹ về typecasting diễn viên. Chúng ta sẽ cover sự khác biệt giữa hai phương pháp và thảo luận cách sử dụng.
Hãy trước xem cách thường coerce giá trị sang kiểu khác trong Ruby với explicit casting helpers.
Explicit Casting Helpers (Helper ép kiểu tường minh)
Các casting helper phổ biến nhất là #to_s, #to_i, #to_a và #to_h. Đây là explicit casting method. Chúng giúp dễ dàng transform giá trị từ kiểu này sang kiểu khác.
Explicit helpers đi kèm lời hứa rõ ràng. Khi #to_s được gọi trên object, nó luôn trả về string, ngay cả khi object không convert sang string tốt. Giống như cast Michael Keaton làm Batman. Bạn sẽ có Batman, dù diễn viên hài không đặc biệt phù hợp vai.
Ruby cung cấp helper methods này trên hầu hết basic object trong standard library.
:foo.to_s # => "foo"
10.0.to_i # => 10
"10".to_i # => 10
Các method này, đặc biệt #to_s, được implement trên hầu hết basic type trong Ruby. Mặc dù casting gần như luôn trả về giá trị, kết quả có thể không như mong đợi.
"foo10".to_i # => 0
[1, 2, 3].to_s # => "[1, 2, 3]"
{ :foo => :bar }.to_s # => "{:foo=>:bar}"
{ :foo => :bar }.to_a # => [[:foo, :bar]]
Object.to_s # => "Object"
Object.new.to_s # => "#<Object:0x00007f8e6d053a90>"
Gọi #to_s, #to_i, #to_a, #to_h ép buộc bất kỳ giá trị nào sang kiểu chọn. Chúng trả về representation của kiểu bị ép bất kể giá trị bị ảnh hưởng thế nào.
Implicit Coercion Methods (Phương thức ép kiểu ngầm)
Gọi type casting method trên giá trị không act như kiểu ta ép có thể gây error hoặc mất data. Ruby cũng cung cấp implicit coercion method chỉ trả về giá trị khi object act như kiểu đó. Cách này ta chắc chắn giá trị act như kiểu mong muốn. Các implicit coercion method là #to_str, #to_int, #to_ary và #to_hash.
Implicit coercion giống cast Leonard Nimoy làm bất kỳ vai nào trừ Spock. Sẽ work nếu nhân vật đủ gần Spock, fail nếu không. Helper #to_str cố gắng convert sang string, nhưng raise NoMethodError nếu object không implement method và không thể implicitly coerce.
10.to_int # => 10
10.0.to_int # => 10
require "bigdecimal"
BigDecimal.new("10.0000123").to_int # => 10
# Unsuccessful coercions
"10".to_int # => NoMethodError
"foo10".to_int # => NoMethodError
[1, 2, 3].to_str # => NoMethodError
{ :foo => :bar }.to_str # => NoMethodError
{ :foo => :bar }.to_ary # => NoMethodError
Object.to_str # => NoMethodError
Object.new.to_str # => NoMethodError
Ta thấy Ruby strict hơn về gì coerce được và không. Nếu coercion không thể, method #to_* không implement trên object và gọi nó raise NoMethodError.
Khi dùng implicit coercion, ví dụ #to_str, ta yêu cầu function trả về String object, chỉ khi kiểu gốc cũng act như String. Vì vậy #to_str chỉ implement trên String trong Ruby Standard Library.
Ruby dùng Implicit Coercion như thế nào
Ngoài việc precise hơn trong coercion, implicit coercion còn dùng cho gì? Ruby dùng implicit coercion khá nhiều scenario. Ví dụ, khi combine object với +.
name = "world!"
"Hello " + name # => "Hello world!"
# Without #to_str
class Name
def initialize(name)
@name = name
end
end
"Hello " + Name.new("world!") # => TypeError: no implicit conversion of Name into String
Ở đây, Ruby raise TypeError vì không thể implicit conversion từ type Name sang String.
Nếu implement #to_str trên class, Ruby biết cách coerce type Name.
# With #to_str
class Name
def to_str
@name
end
end
"Hello " + Name.new("world!") # => "Hello world!"
Cũng vậy với Array và #to_ary.
class Options
def initialize
@internal = []
end
def <<(value)
@internal << value
end
end
options = Options.new
options << :foo
[:some_prefix] + options # => TypeError: no implicit conversion of Options into Array
class Options
def to_ary
@internal
end
end
[:some_prefix] + options # => [:some_prefix, :foo]
Nhưng #to_ary dùng trong nhiều scenario hơn. Ta dùng để destructure Array thành biến riêng.
options = Options.new
options << :first
options << :second
options << :third
first, second, third = options
first # => :first
second # => :second
third # => :third
Nó cũng convert object thành block parameter.
[options].each do |(first, second)|
first # => :first
second # => :second
end
Có nhiều scenario khác dùng implicit coercion, như #to_hash với **. Coerce giá trị thành hash với #to_hash trước khi pass vào method parse_options.
class Options
def to_hash
# Create hash from Options Array
Hash[*@internal]
end
end
def parse_options(opts)
opts
end
options = Options.new
options << :key
options << :value
parse_options(**options) # => {:key=>:value}
Enforcing Types (Buộc kiểu)
Ruby cũng cung cấp coercion method bền bỉ hơn khi kiểu unknown và ta muốn chắc chắn lấy đúng type. Có cho mỗi basic type (String(...), Integer(...), Float(...), Array(...), Hash(...), etc.).
String(self) # => "main"
String(self.class) # => "Object"
String(123456) # => "123456"
String(nil) # => ""
Integer(123.999) # => 123
Integer("0x1b") # => 27
Integer(Time.new) # => 1204973019
Integer(nil) # => TypeError: can't convert nil into Integer
Method String(...) trước thử gọi #to_str trên giá trị, fail thì gọi #to_s. Không phải object nào define #to_str, nên check cả implicit coercion (#to_str) và explicit (#to_s) tăng cơ hội String conversion thành công và lấy giá trị mong muốn. Bằng cách gọi implicit coercion trước, ta dễ lấy kết quả cùng giá trị nhưng kiểu coerced, không phải kiểu "#<Object:0x00007f8e6d053a90>".
class MyString
def initialize(value)
@value = value
end
def to_str
@value
end
end
s = MyString.new("hello world")
s.to_s # => "#<MyString:0x...>"
s.to_str # => "hello world"
String(s) # => "hello world"
Chỉ implement implicit casting method cho object act như kiểu cần coerce, ví dụ #to_str cho String class của bạn.
Ngoài việc thử implicit coercion trước, helper String(...) cũng check kiểu trả về. #to_str chỉ là method có thể trả về bất kỳ kiểu, kể cả non-String. Đảm bảo lấy giá trị đúng type, String(...) raise TypeError nếu type không match.
class MyString
def to_str
nil
end
end
s = MyString.new("hello world")
s.to_s # => "#<MyString:0x...>"
s.to_str # => nil
String(s) # => "#<MyString:0x...>"
Ở đây, Ruby bỏ qua kết quả #to_str vì trả về nil, không phải String-type. Thay vào đó fallback về #to_s.
Nếu #to_s cũng trả nil và không đúng type, String(...) raise TypeError.
class MyString
def to_str
nil
end
def to_s
nil
end
end
s = MyString.new("hello world")
s.to_s # => nil
s.to_str # => nil
String(s) # => TypeError: can't convert MyString to String (MyString#to_s gives NilClass)
Mặc dù reliable hơn enforce type coercion, lưu ý casting helper (String(...), Integer(...), etc.) thường chậm hơn vì phải check nhiều hơn trên giá trị.
Kết luận
Khi muốn chắc chắn deal với đúng kiểu data cho object, type coercion là quá trình hữu ích. Bài này refresh kiến thức explicit casting helper như #to_s, #to_i, #to_a, #to_h. Cũng xem khi nào implicit helper như #to_str, #to_int, #to_ary, #to_hash hữu ích và Ruby dùng chúng như thế nào.
Hy vọng overview type coercion này hữu ích và bạn thấy thú vị analogy typecasting diễn viên. Như thường, cho biết nếu có topic muốn cover. Nếu có câu hỏi hoặc comment, đừng ngần ngại drop line @AppSignal.