หน้าเว็บ

วันศุกร์ที่ 12 กันยายน พ.ศ. 2557

เทคนิคการแปลงข้อมูลโดยใช้ Type Conversion

R
uby มีความสามารถที่เรียกว่า built-in conversion ที่ช่วยให้เมธอดสามารถ เปลี่ยน (convert) อ็อบเจกต์ที่ป้อนเข้ามา ให้เป็นค่าของข้อมูลพื้นฐานอย่างเช่น ตัวเลข สตริงหรืออาร์เรย์ ตามที่เมธอดนั้นคาดหวังได้<br /> คำอธิบายข้างต้นอาจทำให้ไม่เห็นภาพ ลองดูตัวอย่างตามนี้ ครับ<br /> สมมติว่าเรามี อาร์เรย์ grade ซึ่งประกอบด้วยชื่อระดับราคาและความหรูหราของโรงแรม ตั้งแต่ โมเต็ล จนถึง โรงแรมระดับลักเซอร์รี่ เราต้องการแสดงข้อความที่ระบุชื่อของโรงแรมจากอ็อบเจกต์ hotel และพร้อมบอกระดับชั้น ตามจำนวนของดาว ซึ่งแทนด้วย rating ที่เป็น attribute ของ hotel โดย rating จะเป็นตัวเลข มีค่าตั้งแต่ 0 ถึง 5 เราสามารถสร้างอ็อบเจกต์ hotel ง่ายๆ ได้โดยใช้ Struct ดังนี้

Hotel = Struct.new(:name, :rating, :address)
h1 = Hotel.new("Sweet Inn", 1, "Bangkok")
h2 = Hotel.new("Centrara Grand", 5, "Phuket")

กำหนดให้ระดับมาตรฐานและบริการ (ความไฮโซ) ของ hotel อยู่ในอาร์เรย์ที่ชื่อ grade

grade = ["Motel", "Bed and breakfasts", "Contel", "Boutique", "Resort", "Luxury"]

เราสามารถแสดงข้อความที่ต้องการได้ดังนี้



[h1, h2].each do |hotel|
  puts "#{hotel.name} at #{hotel.address} is rating as #{grade[hotel.rating]} hotel."
end

# = > Sweet Inn at Bangkok is rating as Bed and breakfasts hotel
# = > Centrara Grand at Phuket is rating as Luxury hotel

ข้อความข้างต้นที่ได้นั้นเป็นไปตามที่เราต้องการแล้ว
แต่เมื่อลองพิจารณาที่โค้ดดูอีกครั้ง ก็พบว่าตรง grade[hotel.rating] นั้น ค่าของ hotel.rating ต้องเป็นตัวเลขเท่านั้น ซึ่งทำหน้าที่เป็นตัวชี้ลำดับของสมาชิกภายในอาร์เรย์ grade

ในกรณีแบบนี้ เราสามารถกำหนดให้อ็อบเจกต์ hotel ทำหน้าที่ตัวชี้ สมาชิกของอาร์เรย์ด้วยตัวของมันเอง โดยไม่ต้องเรียกใช้เมธอด rating หรือเรียกอีกอย่างว่าการทำ type conversion

ก่อนอื่นให้เราเพิ่มเมธอด to_int ให้กับ Hotel ดังนี้

Hotel = Struct.new(:name, :rating, :address) do
  def to_int
    rating
  end
end

เพียงเท่านี้ เราก็สามารถหาค่าของระดับชั้นจากอาเรย์ grade ได้จากอ็อบเจกต์ hotel ได้ตรงๆ ดังนี้

grade[h1] # => Motel
grade[h2] # => Luxury

ทำไมอาร์เรย์ถึงยังสามารถคืนค่าออกมาได้อย่างถูกต้อง ?
ค่าที่อยู่ใน [ ] ควรที่จะเป็นตัวเลขตรงๆ แทนที่จะเป็นอ็อบเจกต์ไม่ใช่หรือ ?

คำตอบอยู่ที่กระบวนการจัดการกับประเภทของข้อมูลที่เกิดขึ้นภายในคลาสมาตรฐานของ Ruby ซึ่งจะเห็นได้จากคลาส Array จากตัวอย่างที่ผ่านมา คลาสอาร์เรย์จะพยายามแปลงข้อมูลให้อยู่ในรูปที่ตัวมันเองคาดหวังเอาไว้ มันจึงส่งเมสแสส to_int ให้กับอ็อบเจกต์ใดๆ ก็ตามที่ถูกป้อนเข้ามาในเมธอด [] เสมอ ถ้าอ็อบเจกต์ที่ป้อนเข้ามเป็นอ็อบเจกต์ของคลาส Fixnum (คลาสสำหรับตัวเลขจำนวนเต็ม) เมื่อส่งเมสแสส to_int ไปให้ ค่าที่ได้กลับก็จะยังคงเป็นตัวเลขจำนวนเต็ม อาร์เรย์ก็จะทำงานได้ตามปกติ แต่ถ้าอ็อบเจกต์ที่ถูกป้อนเข้ามาในเมธอด [] เป็นสตริงหรืออ็อบเจกต์จากคลาสอื่นๆ สิ่งที่เกิดขึ้นก็คือ เราจะเจอกับ error message เนื่องจากอ็อบเจกต์ที่ป้อนเข้ามาไม่ตอบสนองกระบวนการแปลงข้อมูลไปเป็นตัวเลขภายในคลาสอาร์เรย์ นั่นคือไม่มีการตอบสนองกับเมสแสส to_int ที่คลาสอาร์เรย์ส่งให้กับอ็อบเจกต์นั่นเอง
เมื่อลองทดสอบพฤติกรรมดังกล่าวโดยใช้ irb คุณจะเห็นว่าข้อความ error จะระบุชัดเลยว่า argument ที่ป้อนให้กับเมธอด [] ของอาร์เรย์นั้นไม่ตอบสนองกับ to_int

>> a = ["cat", "bat", "rat"]
=> ["cat", "bat", "rat"]
>> a[0]
=> "cat"
>> a["zero"]
TypeError: no implicit conversion of String into Integer
        from (irb):8:in `[]'
        from (irb):8
        from C:/Ruby200/bin/irb:12:in

ทีนี้กลับมาดูที่กรณีตัวอย่างของ อาเรย์ grade กับอ็อบเจกต์ Hotel อีกครั้ง ถ้าเราป้อนอ็อบเจกต์ที่าสามารถตอบสนองกับเมสแสส to_int ได้ โดยคืนค่าเป็นตัวเลขจำนวนเต็มออกมาตามที่อาร์เรย์คาดหวังไว้แล้วล่ะก็ เมธอด [] ของอาร์เรย์ก็จะสามารถทำงานได้อย่างปกติ ดังที่เห็นจากการป้อนอ็อบเจ็กต์ h1 และ h2 ให้กับเมธอด [] ของอาร์เรย์ grade ข้างต้นนั่นเอง

เราเรียกเมธอดที่ทำหน้าที่ในการแปลงประเภทของข้อมูลในลักษณะนี้ว่าเมธอดแบบ implicit ตัวอย่างเช่น to_int (คืนค่าเป็นอ็อบเจกต์ของคลาส Integer), to_str (คืนค่าเป็นอ็อบเจกต์ของคลาส String), to_ary (คืนค่าเป็นอ็อบเจกต์ของคลาส Array), to_path (คืนค่าเป็นอ็อบเจกต์ของคลาส String), to_io (คืนค่าเป็นอ็อบเจกต์ของคลาส IO) เป็นต้น


Implicit vs. Explicit
Ruby มีเมธอดอีกประเภทหนึ่งที่ทำหน้าที่แปลงข้อมูลเหมือนกัน เรียกว่าเมธอดแบบ Explicit ได่แก่ to_s, to_i, to_a เมธอด Explicit ทั้งสามตัวนี้ทำหน้าที่แปลงข้อมูลไปเป็นข้อมูลประเภท String, Integer และ Array ตามลำดับ แต่การใช้งานจะต่างกันกับ to_str, to_int และ to_ary

สำหรับเมธอดแบบ Explicit จะใช้แปลงค่าของอ็อบเจกต์ที่เราสนใจไปเป็นค่าของข้อมูลในประเภทที่เราต้องการ เช่น to_s เพื่อแปลงให้เป็น String และ to_i เพื่อแปลงเป็น Integer เมธอดแบบ Explicit อย่าง to_s หรือ to_i โดยทั่วไปมักจะถูกกำหนดไว้เป็นหนึ่งในเมธอดของคลาสต่างๆ เพื่อคืนค่าสตริงหรือตัวเลขสำหรับอ็อบเจกต์ของคลาสนั้นๆ เช่นคลาส String มีเมธอด to_i เอาไว้แปลงค่าของสตริงที่เป็นตัวเลขอย่าง "3".to_i จะได้ค่าของตัวเลข 3 ออกมา

แต่สำหรับเมธอดแบบ Implicit อย่างเช่น to_str หรือ to_int ทำหน้าที่แปลงข้อมูลเหมือนกันก็จริงแต่มันจะถูกกำหนดอยู่ในคลาสของตัวเองเท่านั้น เช่น to_int จะเรียกใช้ได้จากอ็อบเจกต์ของคลาส Integer นั้น ถ้าเราใช้ "3".to_int สิ่งที่ได้จะเป็น error message เพราะ คลาสสตริงไม่ได้กำหนดเมธอด to_int เอาไว้ (มีแต่ to_i) และเนื่องจากคลาสมาตรฐานของ Ruby มักมีการเรียกใช้เมธอดแบบ Implicit กับอ็อบเจกต์ที่เป็น argument ของเมธอดในคลาสมาตรฐานนั้นๆ ทำให้ตรงนี้เป็นช่องให้โปรแกรมเมอร์สามารถที่จะเลือกได้ว่าต้องการให้อ็อบเจกต์จากคลาสที่ตนสร้างขึ้นตอบสนองกับเมธอดของคลาสมตรฐานโดยการ override เมธอดแบบ implicit

สรุปความแตกต่างก็คือเมธอดแบบ Explicit นั้นส่วนใหญ่มีการกำหนดอยู่ในคลาสทั่วๆ ไป ใช้เพื่อแปลงค่าของอ็อบเจกต์จากคลาสเดิมไปเป็นค่าของประเภทข้อมูลที่ต้องการ โดยคลาสเดิมกับคลาสของประเภทของข้อมูลที่ต้องการนั้นเป็นคนละคลาสกันเลย ส่วนเมธอดแบบ Implicit นั้นใช้เพื่อทดสอบว่าอ็อบเจกต์นั้นๆ ตอบสนองออกมาเป็นข้อมูลประเภทที่คาดหวังไว้หรือไม่ คลาสมาตรฐานของ Ruby มักจะส่งเมสแสส ที่เป็นชื่อของเมธอดแบบ Implicit เพื่อทดสอบว่ามันกำลังทำงานกับประเภทของข้อมูลที่มันคาดหวังเอาไว้

ลองดูตัวอย่างและความแตกต่างระหว่างเมธอด Explicit และ Implicit อีกทีจากตัวอย่างต่อไปนี้

>> 3.to_s
=> "3"
>> 3.to_str
NoMethodError: undefined method `to_str' for 3:Fixnum
        from (irb):31
        from C:/Ruby200/bin/irb:12:in `
' >> [1,2,3].to_s => "[1, 2, 3]" >> [1,2,3].to_str NoMethodError: undefined method `to_str' for [1, 2, 3]:Array from (irb):34 from C:/Ruby200/bin/irb:12:in `
' >> "3".to_str => "3"

จะเห็นว่า to_s ซึ่งเป็น Explicit เมธอดนั้นมักจะถูกกำหนด (implement) อยู่ในคลาสต่างๆ ในขณะที่ to_str คู่ของมันซึ่งเป็น Implicit เมธอดจะถูกกำหนดอยู่ในคลาสที่เป็นข้อมูลประเภทเดียวกันเท่านั้น (ที่นี้คือคลาส String) จะไม่มีการกำหนดอยู่ในคลาสอื่น (เราต้องไปกำหนดเองถ้าต้องการ)

ลองดูเมธอด + ของคลาส String ในตัวอย่างต่อมา

>> "2" + "3"
=> "23"
>> "2" + 3
TypeError: no implicit conversion of Fixnum into String
        from (irb):36:in `+'
        from (irb):36
        from C:/Ruby200/bin/irb:12:in `
'
เมธอด + คาดหวังว่า argument ของมันต้องเป็นสตริงและคลาสตริงก็มีการตรวจสอบและพยายามแปลงข้อมูลโดยการส่งเมสแสส to_str ซึ่งเป็นเมธอดแบบ Implicit ดังนั้น "2" + 3 จึงเกิด error เพราะเราไม่ได้กำหนดเมธอด to_str ให้กับคลาส Integer แต่ถ้าเรามีอ็อบเจกต์ที่ตอบสนองกับ to_str อ็อบเจเราก็สามารถป้อนอ็อบเจกต์นั้นให้กับเมธอด + ของสตริงได้ดังนี้

>> class Team
>> def initialize(name)
>> @name = name
>> end
>> def to_str
>> @name
>> end
>> end
=> nil
>> team = Team.new("Arsenal")
=> #
>> "2" + team
=> "2Arsenal"

เมื่อลองส่งเมแสส to_s ให้กับ team จะได้สตริงเป็น object id ของ team นั้นเป็นเพราะเมธอด to_s ถูกกำหนดมากับคลาส Class ตั้งแต่แรกแล้ว แต่สำหรับเมธอด to_str เรากำหนดให้กับ team เป็นการเฉพาะ

ก็พอสมควรนะครับสำหรับ post นี้หวังว่าจะได้ idea สำหรับนำไปปรับใช้ในโค้ดได้บ้าง
สำหรับท่านที่ยังคงสนใจรายละเอียดของเรื่อง Built In Conversion ผมแนะนำให้หาอ่านได้จาก Confident Ruby ของ Advi Grimn ซึ่งถือว่าเป็นหนังสือที่ดี และอยากแนะนำครับ

ไม่มีความคิดเห็น:

แสดงความคิดเห็น