Ép buộc kiểu là việc thay đổi kiểu của một đối tượng thành kiểu khác, cùng với giá trị của nó. Ví dụ:thay đổi Số nguyên thành Chuỗi với #to_s
hoặc Float thành Số nguyên với #to_i
. #to_str
có lẽ ít được biết đến hơn và #to_int
Thoạt nhìn, một số đối tượng thực hiện các phương thức này cũng giống như vậy, nhưng có một số khác biệt.
Trong ấn bản này của học viện AppSignal, chúng ta sẽ đi sâu vào việc đúc và ép buộc một cách rõ ràng các loại trong Ruby, đồng thời tìm hiểu sơ lược về các diễn viên đánh máy. Chúng tôi sẽ đề cập đến sự khác biệt giữa cả hai phương pháp và thảo luận về cách chúng được sử dụng.
Trước tiên, hãy xem cách chúng ta thường ép buộc các giá trị thành các kiểu khác nhau trong Ruby với các trình trợ giúp ép kiểu rõ ràng.
Trình trợ giúp Truyền rõ ràng
Các trình trợ giúp truyền phổ biến nhất là #to_s
, #to_i
, #to_a
và #to_h
. Đây là các phương pháp đúc rõ ràng. Chúng giúp chúng tôi dễ dàng chuyển đổi giá trị từ loại này sang loại khác.
Những người trợ giúp rõ ràng đi kèm với một lời hứa rõ ràng. Bất cứ khi nào #to_s
được gọi trên một đối tượng, nó sẽ luôn luôn trả về một chuỗi, ngay cả khi đối tượng không thực sự chuyển đổi thành một chuỗi tốt. Nó giống như chọn Michael Keaton vào vai Người dơi. Bạn sẽ nhận được một người dơi, ngay cả khi một diễn viên hài không đặc biệt phù hợp với vai diễn này.
Ruby cung cấp các phương thức trợ giúp này trên hầu hết mọi đối tượng cơ bản trong thư viện chuẩn Ruby.
:foo.to_s # => "foo"
10.0.to_i # => 10
"10".to_i # => 10
Các phương pháp này, đặc biệt là #to_s
, được thực hiện trên hầu hết các kiểu cơ bản trong Ruby. Mặc dù quá trình truyền hầu như luôn trả về một giá trị, nhưng kết quả có thể không như chúng ta 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
và #to_h
trợ giúp buộc bất kỳ giá trị nào cho kiểu đã chọn. Chúng trả về một đại diện của loại mà nó bị ép buộc bất kể điều gì xảy ra với giá trị.
Phương pháp ép buộc ngầm
Việc gọi các phương thức ép kiểu trên các giá trị không hoạt động như kiểu mà chúng ta đang ép kiểu có thể gây ra lỗi hoặc mất dữ liệu. Ruby cũng cung cấp các phương thức ép buộc ngầm chỉ trả về một giá trị khi các đối tượng hoạt động giống như kiểu. Bằng cách này, chúng ta có thể chắc chắn rằng giá trị hoạt động giống như kiểu chúng ta muốn. Các phương pháp cưỡng chế ngầm này là #to_str
, #to_int
, #to_ary
và #to_hash
.
Sự ép buộc ngầm giống như tuyển chọn Leonard Nimoy vào bất kỳ vai nào trừ Spock. Chúng sẽ hoạt động nếu nhân vật đủ gần Spock, nhưng sẽ thất bại nếu không. #to_str
người trợ giúp cố gắng để chuyển đổi thành một chuỗi, nhưng sẽ tạo ra một NoMethodError
nếu đối tượng không triển khai phương thức và không thể bị ép buộc hoàn toàn.
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
Chúng ta có thể thấy rằng Ruby hiện đã nghiêm ngặt hơn một chút trong những gì nó làm và không ép buộc các loại được yêu cầu. Nếu không thể cưỡng chế, #to_*
phương thức không được triển khai trên đối tượng và việc gọi nó làm tăng NoMethodError
.
Khi sử dụng các biện pháp cưỡng chế ngầm, ví dụ:#to_str
, chúng ta yêu cầu hàm trả về một đối tượng String, chỉ khi kiểu ban đầu cũng hoạt động giống như một String. Vì lý do này, #to_str
chỉ được triển khai trên Chuỗi trong Thư viện Chuẩn Ruby.
Cách Ruby sử dụng sự ép buộc ngầm
Ngoài việc chính xác hơn về những gì chúng ta đang yêu cầu trong một cuộc cưỡng chế, thì sự ép buộc ngầm còn hữu ích cho điều gì khác? Hóa ra Ruby sử dụng chính các cưỡng chế ngầm trong một số tình huống hợp lý. Ví dụ:khi kết hợp các đối tượng 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, chúng ta thấy Ruby tăng một TypeError
vì nó không thể thực hiện chuyển đổi ngầm định từ Name
gõ vào String
.
Nếu chúng tôi triển khai #to_str
trên lớp, Ruby biết cách ép buộc Name
loại.
# With #to_str
class Name
def to_str
@name
end
end
"Hello " + Name.new("world!") # => "Hello world!"
Tương tự hoạt động đối với Mảng 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
được sử dụng trong nhiều trường hợp hơn. Chúng ta có thể sử dụng nó để cấu trúc một Mảng thành các biến riêng biệt.
options = Options.new
options << :first
options << :second
options << :third
first, second, third = options
first # => :first
second # => :second
third # => :third
Nó cũng thực hiện chuyển đổi đối tượng thành các tham số khối.
[options].each do |(first, second)|
first # => :first
second # => :second
end
Có nhiều trường hợp hơn trong đó các phương pháp ép buộc ngầm được sử dụng, chẳng hạn như #to_hash
với **
. Điều này buộc giá trị thành một hàm băm với #to_hash
trước khi chuyển nó đến parse_options
phương pháp.
class Options
def to_hash
# Create a hash from the Options Array
Hash[*@internal]
end
end
def parse_options(opts)
opts
end
options = Options.new
options << :key
options << :value
parse_options(**options) # => {:key=>:value}
Các loại thực thi
Ruby cũng cung cấp các phương thức cưỡng chế linh hoạt hơn khi kiểu là một kiểu không xác định và chúng tôi muốn đảm bảo rằng chúng tôi nhận được đúng kiểu. Có một cho mọi kiểu cơ bản (String(...)
, Integer(...)
, Float(...)
, Array(...)
, Hash(...)
, v.v.).
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
Chuỗi String(...)
phương thức đầu tiên cố gắng gọi #to_str
trên giá trị và khi không thành công, nó sẽ gọi #to_s
của nó phương pháp. Không phải tất cả các đối tượng đều xác định một #to_str
, do đó kiểm tra bằng cả cưỡng chế ngầm (#to_str
) và rõ ràng (#to_s
) phương pháp truyền làm tăng cơ hội chuyển đổi Chuỗi sẽ hoạt động và bạn sẽ nhận được giá trị bạn muốn. Trước tiên, bằng cách kêu gọi ép buộc ngầm, chúng ta có nhiều khả năng nhận được kết quả có cùng giá trị nhưng thuộc loại bị ép buộc chứ không phải là một thứ gì đó giống như "#<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"
Bạn chỉ nên triển khai các phương thức truyền ngầm cho các đối tượng hoạt động giống như kiểu bị ép buộc, ví dụ:#to_str
cho lớp Chuỗi của riêng bạn.
Ngoài việc thử ép buộc ngầm đầu tiên, String(...)
helper cũng kiểm tra kiểu trả về. #to_str
chỉ là một phương thức có thể trả về bất kỳ loại giá trị nào, thậm chí không phải là Chuỗi. Để đảm bảo chúng tôi nhận được giá trị của loại được yêu cầu String(...)
tăng TypeError
nếu các loại không khớp.
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, chúng ta có thể thấy rằng Ruby bỏ qua kết quả của #to_str
vì nó trả về nil
, không thuộc kiểu Chuỗi. Thay vào đó, nó trở lại #to_s
kết quả.
Nếu #to_s
cũng trả về nil
và do đó không thuộc loại chính xác, String(...)
sẽ tạo ra một 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ù chúng có thể đáng tin cậy hơn trong việc thực thi cưỡng chế kiểu, lưu ý rằng các phương thức của trình trợ giúp truyền (String(...)
, Integer(...)
, v.v.) thường chậm hơn một chút vì chúng cần thực hiện nhiều kiểm tra hơn đối với giá trị đã cho.
Kết luận
Khi bạn muốn đảm bảo rằng bạn đang xử lý đúng loại dữ liệu cho một đối tượng, thì kiểu ép buộc là một quá trình hữu ích. Trong bài đăng này, chúng tôi đã làm mới kiến thức của mình về các trình trợ giúp truyền rõ ràng như #to_s
, #to_i
, #to_a
và #to_h
. Chúng tôi cũng đã xem xét các trường hợp khi những người trợ giúp ngầm định như #to_str
, #to_int
, #to_ary
và #to_hash
hữu ích và cách chúng được sử dụng bởi chính Ruby.
Chúng tôi hy vọng bạn thấy tổng quan về kiểu ép buộc này hữu ích và cách bạn tìm thấy phép loại suy diễn viên. Như mọi khi, hãy cho chúng tôi biết nếu có chủ đề bạn muốn chúng tôi đề cập. Nếu bạn có bất kỳ câu hỏi hoặc nhận xét nào, đừng ngần ngại gửi cho chúng tôi một dòng @AppSignal.