Type coercion คือการเปลี่ยน type ของ object เป็น type อื่นพร้อมกับ value เช่น การเปลี่ยน Integer เป็น String ด้วย #to_s หรือ Float เป็น Integer ด้วย #to_i Methods ที่รู้จักน้อยกว่าคือ #to_str และ #to_int ที่บาง objects implement ทำสิ่งเดียวกันตอนแรก แต่มีความแตกต่างบางอย่าง
ใน AppSignal Academy ครั้งนี้ เราจะเจาะลึก explicit casting และ implicit coercion ของ types ใน Ruby พร้อมสัมผัสพักๆ เรื่อง typecasting actors เราจะครอบคลุมความแตกต่างระหว่างทั้งสองวิธี และอภิปรายว่าพวกมันถูกใช้อย่างไร
มาดูก่อนว่าเรา coercion values ไปยัง types ต่างๆ ใน Ruby ได้อย่างไรด้วย explicit casting helpers
Explicit Casting Helpers
Casting helpers ที่พบบ่อยที่สุดคือ #to_s, #to_i, #to_a และ #to_h นี่คือ explicit casting methods ช่วยให้เรา transform value จาก type หนึ่งไปอีก type ได้อย่างง่ายดาย
Explicit helpers มาพร้อมกับ promise ที่ชัดเจน ทุกครั้งที่ #to_s ถูกเรียกบน object มันจะ เสมอ คืนค่า string แม้ object จะไม่ convert เป็น string ได้ดี มันเหมือน casting Michael Keaton เป็น Batman คุณจะได้ Batman แม้ comedy actor อาจไม่เหมาะสมสำหรับบทบาทนั้นโดยเฉพาะ
Ruby เสนอ helper methods เหล่านี้บนเกือบทุก basic object ใน Ruby standard library
:foo.to_s # => "foo" 10.0.to_i # => 10 "10".to_i # => 10Methods เหล่านี้ โดยเฉพาะ #to_s ถูก implement บน most basic types ใน Ruby แม้ casting จะคืนค่าเกือบทุกครั้ง ผลลัพธ์อาจไม่ใช่สิ่งที่เราคาดหวัง
"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 # => "#" การเรียก helpers #to_s, #to_i, #to_a และ #to_h บังคับให้ value ใดๆ กลายเป็น type ที่เลือก พวกมันคืนค่า representation ของ type ที่ถูก coerce ไป ไม่ว่าจะเกิดอะไรกับ value
Implicit Coercion Methods
การเรียก type casting methods บน values ที่ไม่ act เหมือน type ที่เรา cast ไป อาจทำให้เกิด errors หรือ loss of data Ruby ยังเสนอ implicit coercion methods ที่คืนค่าเฉพาะเมื่อ objects act เหมือน type นั้น วิธีนี้เราจึงมั่นใจได้ว่า value act เหมือน type ที่เราต้องการ Implicit coercion methods นี้คือ #to_str, #to_int, #to_ary และ #to_hash
Implicit coercion เหมือน casting Leonard Nimoy เป็นบทบาทไหนก็ตามยกเว้น Spock พวกเขาจะทำงานถ้า character ใกล้เคียงกับ Spock พอ แต่จะ fail ถ้าไม่ใช่ Helper #to_str พยายาม convert เป็น string แต่จะ raise NoMethodError ถ้า object ไม่ implement method และไม่สามารถ 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เห็นได้ว่า Ruby เข้มงวดขึ้นในสิ่งที่ coerce และไม่ coerce ไปยัง requested types ถ้า coercion ไม่ได้ method #to_* จะไม่ถูก implement บน object และการเรียกมันจะ raise NoMethodError
เมื่อใช้ implicit coercions เช่น #to_str เราขอให้ function คืนค่า String object เฉพาะเมื่อ original type ยัง act เหมือน String ด้วย เพราะฉะนั้น #to_str ถูก implement เฉพาะบน String ใน Ruby Standard Library
Ruby ใช้ Implicit Coercion อย่างไร
นอกจากการ precise มากขึ้นในสิ่งที่เราขอระหว่าง coercion implicit coercion ยังมีประโยชน์อะไรบ้าง? ระหว่างนั้น Ruby ใช้ implicit coercions เองใน scenarios เยอะมาก ตัวอย่างเมื่อ combine objects ด้วย +
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ที่นี่ Ruby raise TypeError เพราะไม่สามารถ implicit conversion จาก type Name เป็น String ได้
ถ้าเรา implement #to_str บน class Ruby จะรู้วิธี coerce type Name
# With #to_str class Name def to_str @name end end "Hello " + Name.new("world!") # => "Hello world!"สิ่งเดียวกันทำงานได้กับ Arrays และ #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]แต่ #to_ary ถูกใช้ใน scenarios มากกว่านี้ เราสามารถใช้มัน destructure Array เป็น variables แยกกัน
options = Options.new options << :first options << :second options << :third first, second, third = options first # => :first second # => :second third # => :thirdมันยังทำ conversion ของ object เข้าเป็น block parameters
[options].each do |(first, second)| first # => :first second # => :second endมี scenarios อื่นๆ ที่ implicit coercion methods ถูกใช้ เช่น #to_hash กับ ** นี้ coerce value เป็น hash ด้วย #to_hash ก่อนส่งให้ method parse_options
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}Enforcing Types
Ruby ยังเสนอ coercion methods ที่ resilient มากขึ้นเมื่อ type เป็น unknown และเราอยาก ensure เราได้ correct type มีสำหรับทุก basic type (String(...), Integer(...), Float(...), Array(...), Hash(...), ฯลฯ)
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 IntegerMethod String(...) ลองเรียก #to_str บน value ก่อน และเมื่อ fail จะเรียก #to_s ไม่ใช่ทุก objects ที่ define #to_str method การตรวจสอบด้วยทั้ง implicit coercion (#to_str) และ explicit (#to_s) casting methods เพิ่มโอกาสที่ String conversion จะสำเร็จและคุณจะได้ value ที่ต้องการ โดยการเรียก implicit coercion ก่อน เราจะได้ผลลัพธ์ที่มีค่าเหมือนเดิมแต่เป็น coerced type มากกว่า ไม่ใช่ something like "#
class MyString def initialize(value) @value = value end def to_str @value end end s = MyString.new("hello world") s.to_s # => "#" s.to_str # => "hello world" String(s) # => "hello world" คุณควร implement implicit casting methods เฉพาะ objects ที่ act เหมือน to be coerced type เช่น #to_str สำหรับ String class ของคุณเอง
นอกจากลอง implicit coercion ก่อน helper String(...) ยังตรวจสอบ returned type #to_str เป็นแค่ method ที่สามารถคืนค่า type ใดๆ ได้ แม้ non-Strings เพื่อ ensure เราได้ value ของ requested type String(...) raise TypeError ถ้า types ไม่ match
class MyString def to_str nil end end s = MyString.new("hello world") s.to_s # => "#" s.to_str # => nil String(s) # => "#" ที่นี่ เราเห็นว่า Ruby ข้ามผลลัพธ์ของ #to_str เพราะมันคืน nil ซึ่งไม่ใช่ String-type แทนที่จะ fallback ไปที่ผลลัพธ์ของ #to_s
ถ้า #to_s ก็คืน nil และไม่ใช่ correct 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)แม้จะน่าเชื่อถือกว่าในการ enforce type coercion แต่โปรดทราบว่า casting helper methods (String(...), Integer(...), ฯลฯ) ปกติช้ากว่าเล็กน้อย เพราะต้องทำ checks มากขึ้นบน given value
สรุป
เมื่อคุณอยาก ensure ว่าคุณ deal กับ right type ของ data สำหรับ object type coercion เป็น process ที่มีประโยชน์ ในบทความนี้ เรารีเฟรชความรู้ explicit casting helpers เช่น #to_s, #to_i, #to_a และ #to_h เรายังดู instances ที่ implicit helpers เช่น #to_str, #to_int, #to_ary และ #to_hash มีประโยชน์และถูกใช้โดย Ruby เองอย่างไร
หวังว่าคุณจะพบ overview นี้มีประโยชน์และสนุกกับ actor typecasting analogy เหมือนเดิม บอกเราได้ถ้ามี topic ที่อยากให้เรา cover ถ้ามีคำถามหรือความคิดเห็น อย่าลังเล drop us a line @AppSignal