Computer >> Máy Tính >  >> Lập trình >> Ruby

#to_s hoặc #to_str? Truyền rõ ràng so với ép buộc ngầm các kiểu trong Ruby

É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#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#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#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#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#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.