Objective-C Programming Chapter 7 (Part 1)

More on classes & Property

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

The id type

แต่ก่อนอื่นจะขอแนะนำตัวแปรแบบใหม่ที่เคยได้เคยเกริ่นไว้ตั้งแต่ช่วงแรกแล้วว่า เป็นตัวแปรแบบชนิดพิเศษ นั่นก็คือตัวแปร id เราเรียกตัวแปรชนิดนี้ว่าเป็นตัวแปรแบบ dynamic typing ตัวแปรที่ผ่านๆมาไม่ว่าจะเป็น int , float หรือ char ล้วนแล้วแต่เป็นแบบ static typing ทั้งสิ้นกล่าวคือเมื่อประกาศตัวแปรชนิดนั้นแล้วจะไม่สามารถเปลี่ยนเป็นชนิดอื่นได้อื่น แต่ตัวแปรแบบ dynamic typing จะต่างออกไป เพราะความพิเศษของมันอยู่ตรงที่ ตัวแปรชนิดนี้สามารถเปลี่ยนเป็นอ็อบเจกต์ได้ทุกชนิด พูดอีกอย่างก็คือตัวแปรนี้ใช้แทนอ็อบเจกต์ใดๆก็ได้ ลองดูตัวอย่างโปรแกรมต่อไปนี้

Program 7.1

Ractangle.h

Ractangle.m

Triangle.h

Triangle.m

main.m

Program 7.1 Output

Tri area: 100 
Rec area: 200

โปรแกรมที่ได้เขียนไป เริ่มด้วยการประกาศคลาส Rectangle , Triangle และส่วนของเมนโปรแกรมก็ประกาศออบเจ็กต์ของคลาสทั้งสองนั่นก็คือ tri และ rect นอกจากนี้เราได้ประกาศตัวแปร area และตัวแปรชนิด id โดยให้ชื่อว่า obj ดังนั้นตัวแปร obj นี้ก็จะสามารถใช้เป็นออบเจ็กต์ใดๆก็ได้ จากตัวอย่างโค้ดของโปรแกรมเราในบรรทัดที่ 18 ได้กำหนดค่า obj ให้เท่ากับ tri

เมื่อตัวแปร obj ถูกกำหนดให้เป๊นอ็อบเจกต์ tri ซึ่งเป็น instance ของคลาส Triangle ฉะนั้นแล้วเราก็สามารถที่จะเรียกเมธอดใดๆของ Triangle ผ่านทาง obj ได้เช่นกันเพราะถือว่าเป็นอ็อบเจกต์เดียวกัน แม้ว่าตัวแปร obj จะไม่ได้ประกาศว่าเป็นออบเจ็กต์ของคลาส Triangle ก็ตาม ดังนั้นการส่งเมสเซจหา obj

ก็เท่ากับเป็นการส่งเมสเซจไปยังอ็อบเจกต์ tri

เพราะเราได้กำหนดให้ obj เป็นอ็อบเจกต์ tri นั่นเอง และเมื่อสิ้นสุดการทำงานบรรทัดนี้ ตัวแปร obj ก็ได้ถูกเปลี่ยนให้เป็น rec ซึ่งเป็นอ็อบเจกต์ของคลาส Rectangle

การที่ทำแบบนี้ได้ ก็เพราะว่าตัวแปร obj สามารถเปลี่ยนไปเป็นอ็อบเจกต์ชนิดใดๆก็ได้ ไม่ได้ถูกจำกัดไว้กับคลาสใดคลาสหนึ่ง จากนั้นก็เรียกเมธอด area เพื่อคำนวนของค่าพื้นที่ของสี่เหลี่ยมได้อย่างถูกต้อง แล้วโปรแกรมรู้ได้อย่างไรว่าเมื่อส่งเมสเซจ area ไปยัง obj จะเป็นการส่งเมสเซจของคลาสไหนระหว่าง Rectangle และ Triangle เนื่องจากคลาสทั้งสองมีเมธอดชื่อเดียวกัน ? คำตอบก็คือเพราะว่าภาษา Objective-C เป็นภาษาแบบ dynamic runtime การตัดสินใจว่าจะส่งเมสเซจไปยังอ๊อบเจกต์ใดๆไม่ได้เกิดขึ้นตอนคอมไพล์ (Compile Time) แต่โปรแกรมจะตัดสินใจในช่วงที่กำลังทำงาน (Run time) ลักษณะการทำงานแบบนี้ศัพท์ทางเทคนิคเรียกว่า dynamic binding

Selector

อย่างที่ทราบกันอยู่แล้วว่าการเรียกใช้เมธอดของอ็อบเจกต์จะใช้วิธีการส่ง message หาอ็อบเจกต์นั้น (บางครั้งหนังสือเล่มนี้อาจจะเขียนว่า เรียกเมธอด แต่ขอให้เข้าใจว่าคือการส่งเมสเซจ) การส่งเมสเซจไม่ใช่การเรียกเมธอดหรือฟังก์ชั่นโดยตรงเหมือนภาษา C , C++ หรือ Java  แต่ในภาษา Objective-C จะเลือกเมธอดให้ทำงานโดยอิงกับสิ่งที่เรียกว่า selector  ถ้าจะอธิบายให้เห็นภาพ selector อาจจะเปรียบเสมือนซองจดหมาย ที่ด้านในมีคำสั่งการทำงาน เมื่อผู้รับเปิดซองก็ทำตามคำสั่งนั้น หากไม่สามารถทำตามคำสั่งนั้นได้ ก็จะเกิดข้อผิดพลาด invalid selector นั่นเอง การประกาศตัวแปรที่เป็น selector ทำได้ด้วยการประกาศให้เป็นตัวแปรชนิด SEL ดังเช่นตัวอย่าง

จากโค้ดได้ประกาศ selector ชื่อ mySelector โดยกำหนดให้เป็นเมธอด dosomeThing ในกรณีที่เมธอดนั้นรับพารามิเตอร์เราต้องเพิ่มเครื่องหมาย : ต่อท้าย เช่น

ก็ต้องประกาศ selector ดังนี้

เบื้องหลังกลไกลการทำงานของเมสเซจคือเมื่อโปรแกรมถูกคอมไพล์ โค้ดส่วนที่เป็นการส่งเมสเซจ เช่น

จะถูกแปลงให้เป็นโค้ดคล้ายการเรียกฟังชั่นคำสั่งภาษา C ดังนี้

นอกจากการนี้แล้วเราสามารถให้อ็อบเจกต์ของเราทำงานตาม selector ที่กำหนดได้ด้วยเมธอด performSelector: และยังใช้เมธอด respondsToSelector: สอบถามอ็อบเจกต์ได้ด้วยว่าสามารถทำงานตาม selector ที่กำหนดได้หรือไม่ เพื่อให้เข้าใจการใช้ selector มากขึ้น เราจะเขียนโปรแกรมทดลองการใช้งาน selector กันสักโปรแกรม โดยโปรแกรมของเราจะยังใช้คลาส Triangle และ Rectangle จากโปรแกรม 7.1 และให้เพิ่มคลาส Circle ในโปรเจคใหม่ดังนี้

Program 7.2

Circle.h

Circle.m

main.m

เมนโปรแกรมได้ประกาศ selector คือ setValue และ print หลังจากนั้นก็ประกาศอ็อบเจกต์ของคลาสทั้งสาม ต่อมาเราได้กำหนดค่าให้กับกับวงกลม cir มีรัศมี 10 และกำหนดค่าให้สามเหลี่ยม tri มีความกว้าง 10 ยาว 10 ส่วนในบรรทัดที่ 22 เป็นการกำหนดค่าให้กับอ็อบเจกต์เช่นเดียวกันแต่ใช้ฟังชั่น objc_msgSend เพื่อทำการส่งเมสเสจ setValue ให้กับอ็อบเจกต์ rect พร้อมกับค่าอีกสองค่าคือ 20 และ 10 ตามลำดับ ซึ่งโค้ดบรรทัดนี้จะเหมือนกับการเขียนโค้ด

ตั้งแต่บรรทัดที่ 24 จะเป็นโค้ดที่ใช้ในการถามอ็อบเจกต์นั้นว่ารองรับ selector ที่กำหนดให้ได้หรือไม่ ในกรณีที่รองรับก็ให้ selector นั้นทำงาน เมื่อกลับไปดูคลาส Triangle และ Rectangle จะพบว่าไม่มีเมธอด printData ดังนั้นแล้ว tri และ rect จะไม่สามารถทำตาม selector ที่ให้ได้ โปรแกรมจึงแสดงผลดังนี้

Program 7.2 Output

Circle radius: 10.000000 area: 314.159271

การใช้ selector สำหรับบทนี้อาจจะยังไม่เห็นประโยชน์มากนัก แต่ในบทหลังๆเราจะได้เห็นการประยุกต์ใช้อีกหลายตัวอย่าง และจะได้เห็นความสามารถของความเป็น dynamic runtime ในภาษา Objective-C มากขึ้น

Property

โดยปกติแล้วเมื่อเราประกาศคลาส ตัวแปรที่อยู่ภายในคลาส (instance variable) จะถูกกำหนดให้เป็นแบบ private หรือถูกปกปิดจากภายนอกโดยอัตโนมัติ เพราะฉะนั้นเราจึงต้องประกาศเมธอดเพื่อใช้ในการกำหนดค่าและแสดงค่าของตัวแปรภายในคลาสหรือที่เรียกว่า setter/getter ดังตัวอย่าง

Student.h

Student.m

จากตัวอย่างจะพบว่าถ้าหากจะให้อ๊อบเจ็กต์อื่นใช้ instance variable ของคลาสได้นั้น เราต้องประกาศเมธอดถึงสองเมธอด นั่นคือเมธอธอดที่ใช้กำหนดค่าตัวแปร (setter) และเมธอดที่ใช้ขอค่าตัวแปร (getter) ยิ่งคลาสมีจำนวน instance variable มากเท่าไหร่จำนวนเมธอดก็จะเพิ่มมากตามไปด้วย โชคดีที่เราไม่ต้องทุ่มเทเวลากับการเขียนโค้ด setter/getter เพราะหลังจากที่ภาษา Objective-C ได้ปรับปรุงมาถึง Objective-C 2.0 ก็ได้เพิ่มความสามารถในการสร้าง setter/getter ให้กับ instance variable โดยอัตโนมัติ  วิธีการคือประกาศตัวแปรนั้นให้เป็นพร๊อพเพอร์ตี้ (property) ของคลาส โดยการเขียน @property (property directive) ในส่วน interface พร้อมกับกำหนดคุณสมบัติของพร๊อพเพอร์ตี้ (attribute) จากนั้นเขียน @synthesize (synthesize directive) ในส่วน implementation เพื่อบอกให้คอมไพเลอร์สร้างโค้ด getter/setter ให้อัตโนมัติ

Student.h ( property )

Student.m ( property )

ส่วนการเรียกใช้ property ก็ไม่ได้ยุ่งยากอะไรเพียงแค่ใช้เครื่องหมายจุด . และตามด้วยชื่อของ property ดังเช่นตัวอย่าง

Property Attribute

จากโค้ดของคลาส Student ที่ผ่านมาเราได้ประกาศพร๊อพเพอร์ตี้ 2 ตัวคือ mathMidTerm และ mathExamination พร้อมกับกำหนดคุณสมบัติของพร๊อพเพอร์ตี้ให้เป็นแบบ assign , readwrite หมายความว่าเมื่อกำหนดค่าให้กับพร๊อพเพอร์ตี้นั้นจะเป็นการกำหนดค่าตัวแปรโดยตรง และพร๊อพเพอร์ตี้นี้อ่านและเขียนข้อมูลได้ นอกจากนี้ยังมี attribute อื่นๆดังนี้

readwrite กำหนดให้พร๊อพเพอร์ตี้อ่านและเขียนข้อมูลได้
readonly กำหนดให้พร๊อพเพอร์ตี้อ่านข้อมูลได้อย่างเดียว ไม่สามารถแก้ไขได้
assign เมธอด getter/setter ที่ถูกสร้างขึ้นจะมีการทำการงานเหมือนกับการให้ค่าโดยตรง เช่น x = 20 ปกติจะใช้กับค่าที่เป็น primitive type เช่น int , float
retain หากกำหนดพร๊อพเพอร์ตี้แบบนี้ setter ที่ถูกสร้างขึ้นจะ release ออบเจ็กต์เดิมก่อนและหลังจากนั้นจะ retain ออบเจ็กต์ใหม่ ลักษณะการทำงานเหมือนโค้ดตัวอย่างดังนี้

copy เมื่อกำหนดคุณสมบัติแบบ copy เมธอด setter ที่สร้างขึ้นจะ release ออบเจ็กต์เดิมและหลังจากนั้นจะ copy ออบเจ็กต์ใหม่ มีลักษณะการทำงานเหมือนโค้ดตัวอย่างดังนี้

nonatomic เมื่อโปรแกรมทำงานแบบ multithread (จะอธิบายเทรดอย่างละเอียดอีกครั้งในบทหลัง ) getter/setter ที่ถูกสร้างขึ้นจะไม่รับประกันว่าค่าที่ได้จะครบถ้วน เช่น thread A กำลังอ่านค่าและ thread B เข้ามาเปลี่ยนแปลงค่า อาจจะเกิดปัญหาว่าอ็อบเจกต์ที่ใช้งานหายหายไปก่อนที่ thread A จะได้รับ เนื่องจากการกระทำของ B
atomic เมื่อโปรแกรมทำงานแบบ multithread จะไม่เกิดปัญหาเช่นเดียวกัน nonatomic เพราะเมธอด getter/setter ที่ถูกสร้างขึ้นจะรับประกันว่าการขอค่าจะได้รับครบถ้วนจาก getter หรือเมื่อกำหนดค่าก็จะกระทำผ่าน setter เสมอ อย่างไรก็ตาม atomic ไม่ได้หมายความว่า thread safe และข้อเสียของ atomic คือทำงานได้ช้ากว่า nonatomic
เมื่อประกาศพร๊อพเพอร์ตี้จะมี attribute ที่ถูกกำหนดโดยอัตโนมัติไว้อยู่แล้วคือ atomic , assign , readwrite เราไม่ต้องเขียนโค้ด 3 attribute นี้ก็ได้ นอกจากนี้ในคอมไพล์เลอร์ LLVM รุ่นใหม่ ( มากับ XCode 4.2 ) ไม่จำเป็นต้องเขียน @synthesize ก็ได้เพราะคอมไพล์เลอร์จะสร้างให้อัตโนมัติ คุณสมบัติต่างๆของ attribute ยังไม่หมดเพียงเท่านี้ แต่เป็นส่วนที่เกี่ยวข้องกับ Automatic Reference Counting (ARC) ซึ่งจะอธิบายเพิ่มเติมในบทหลังๆ
นอกจากนี้การประกาศพร๊อพเพอร์ตี้เราไม่จำเป็นต้องประกาศให้ชื่อของพร๊อพเพอร์ตี้เป็นชื่อเดียวกับ instance variable ก็ได้ เพียงแค่เราต้องกำหนดให้พร๊อพเพอร์ตี้ให้เท่ากับตัวแปรที่เราต้องการ  ดังเช่นตัวอย่าง
Student.h

Student.m

ตัวอย่างโค้ดจะเห็นว่า เราได้กำหนดให้ชื่อของพร๊อพเพอร์ตี้ ไม่เหมือนกับชื่อของตัวแปร แต่ในส่วนของ synthesize เราได้กำหนดให้พร๊อพเพอร์ตี้เท่ากับตัวแปรที่ต้องการ

ส่วนการเรียกใช้งานก็ยังคงเป็นรูปแบบเดิมไม่ได้มีอะไรพิเศษเพิ่มเติม

อย่างที่ได้กล่าวไปว่าการใช้ @property และ @synthesize นั้นเป็นการบอกให้คอมไพเลอร์สร้างเมธอดให้อัตโนมัติ ดังนั้นถ้าไม่ต้องการจะเรียกใช้ผ่านเครื่องหมายจุด  แต่ต้องการจะเรียกเมธอด setter และ getter ที่คอมไพลเลอร์สร้างให้ก็สามารถทำได้เช่นกัน ดังตัวอย่าง

@dynamic

ปกติเราจะใช้ @synthesize เพื่อบอกให้คอมไพล์เลอร์สร้างเมธอด getter/setter ให้กับเรา แต่ในบางครั้งเราไม่ต้องการจะให้คอมไพล์เลอร์สร้างให้ เราสามารถใช้ @dynamic บอกกับคอมไพล์เลอร์ว่าเราได้เตรียมเมธอด setter/getter ไว้ให้แล้ว เช่น จากโค้ด @synthesize

หากเปลี่ยนใช้ @dynamic ก็จะมีโค้ดดังนี้

ถึงแม้ว่าเราต้องเขียนโค้ดเอง แต่ก็มีข้อดีคือหากเราต้องการความยืดหยุ่นในการจัดค่าด้วยตัวเอง และยังต้องการความสะดวกในการเขียนโค้ดแบบพร๊อพเพอร์ตี้เราก็สามารถใช้ @dynamic ช่วยจัดการปัญหาตรงนี้ได้ การใช้ @dynamic นี้จะพบเห็นได้บ่อยเมื่อใช้ Core Data

Self Keyword

self เป็น keyword เพื่ออ้างอิงถึงตัวเอง การใช้ self มักจะใช้เพื่อเรียกเมธอดภายในตัวคลาสเอง เช่น ถ้าหากเราต้องการจะคำนวนค่าเฉลี่ย ของคะแนนกลางภาคและปลายภาคก็อาจจะเพิ่มเมธอด getAverageScore:

ต่อมาเราต้องการจะเขียนเมธอด printAllScore: เพื่อใช้แสดงคะแนนกลางภาค ปลายภาค และคะแนนเฉลี่ย ตัวแปรต่างๆในคลาสเช่น _mathMidTerm และ _mathExamination เข้าใช้ได้โดยตรงอยู่แล้ว แต่ในกรณีคะแนนเฉลี่ยจะต่างออกไปเพราะเป็นค่าจากการคำนวน การเขียนโค้ดคำนวนใหม่ในเมธอด printAllScore: ก็ไม่ใช่เรื่องที่ดีนัก การแก้ปัญหาที่ดีกว่านั้นก็คือการเรียกใช้เมธอด getAverageScore: ของตัวคลาสเอง ดังเช่นตัวอย่าง

นอกจากการเรียกเมธอดแล้วเรายังสามารถที่จะใช้ self เพื่ออ้างถึงพร๊อพเพอร์ตี้ได้เช่นกัน ดังตัวอย่าง

มาถึงตรงนี้อาจจะเกิดข้อสงสัยว่า มันต่างอะไรกับการเรียกใช้ instance variable โดยตรงๆ เพราะยังไงก็สามารถเข้าถึงได้โดยตรงอยู่แล้ว ดังเช่น

ถึงแม้ว่าสองวิธีการนี้จะเป็นการกำหนดค่ากับตัวแปรเหมือนกัน แต่การทำงานแตกต่างกัน เพราะถ้าหากเราใช้พร๊อพเพอร์ตี้จะเป็นการใช้เมธอด setter/getter ที่คอมไพเลอร์สร้างขึ้น (หรือเรากำหนดเอง) ส่วนอีกวิธีเป็นกำหนดค่าไปยังตัวแปรโดยตรง ถึงแม้ว่าเราจะกำหนดค่าโดยตรงให้กับตัวแปรได้ แต่ในบางกรณีเช่นการทำงานในแบบ multithread เราจำเป็นต้องกำหนดค่าผ่านพร๊อพเพอร์ตี้เพื่อไม่ให้เกิดปัญหาระหว่าง thread เป็นต้น

More about Inheritance

เนื้อหาในส่วนนี้จะอธิบายเพิ่มเติมเกี่ยวกับการสืบทอดคลาสของภาษา Objective-C เพิ่มเติม เพราะก่อนหน้านี้ได้อธิบายเกี่ยวกับการสืบทอดคลาสและ subclass เพียงคร่าว ยังไม่ได้ใช้ประโยชน์ของการสืบทอดแบบเต็มที่  จากตัวอย่างที่ผ่านๆมาการประกาศคลาสเราจะประกาศโดยการสืบทอดคลาส NSObject ซึ่งเป็นคลาสต้นแบบของทุกๆคลาส (Root Class) ดังเช่นโค้ดตัวอย่าง

จริงๆแล้วเราไม่จำเป็นต้องใช้ NSObject เป็น root class ก็ได้ แต่ที่เราต้องใช้คลาสนี้ก็เพราะว่า NSObject เป็นคลาสพื้นฐานมีคำสั่งและเมธอดเบื้องต้นที่จำเป็นสำหรับทุกคลาส เช่นการจองหน่วยความจำ การเปรียบเทียบชนิดออบเจ็กต์ เป็นต้น

chapter7_3

จากนิยามของการสืบทอดในบทที่ 4 ได้บอกว่าคลาสที่เป็นต้นกำเนิดจะเรียกว่า superclass หรือ parent และเราจะเรียกคลาสที่สืบทอดว่า subclass หรือ child และเมื่อไหร่ก็ตามที่ประกาศคลาสใหม่โดยสืบทอดจากคลาสอื่น คลาสที่สร้างขึ้นมาใหม่จะมีคุณสมบัติเหมือนกับ superclass หรืออาจจะบอกได้ว่าคลาสที่สร้างขึ้นมาใหม่จะมีเมธอดเช่นเดียวกัน หรือมีตัวแปรในคลาสเหมือนกันกับ superclass เพื่อให้ง่ายต่อความเข้่าใจเราจะเขียนคลาสและโปรแกรมง่ายๆสักโปรแกรม

ElectronicDevice.h

ElectronicDevice.m

คลาส ElectronicDevice ที่ได้ประกาศไปมีตัวแปรแค่ตัวเดียวและก็มีเมธอดไว้สำหรับแสดงค่า price อย่างง่ายๆ ต่อไปให้ประกาศคลาสใหม่ขึ้นมาชื่อ Computer ดังตัวอย่าง

Computer.h

Computer.m

เมื่อดูโค้ดของคลาสคอมพิวเตอร์ เราได้ได้กำหนดให้คลาส Computer สืบทอดคลาส ElectronicDevice

คลาส Computer เราเรียกว่าคลาสลูก (sub class หรือ child) ส่วน ElectronicDevice เรียกว่าคลาสแม่ (super class หรือ parent) คลาสลูกจะได้รับคุณสมบัติทุกประการเหมือนคลาสแม่ นั่นหมายความว่าคลาส Computer นั้นมีเมธอดและสมาชิกในคลาสเช่นเดียวกันกับคลาส ElectronicDevice

chapter7_4

จะเห็นว่าการสืบทอดคลาสนั้นมีประโยชน์มากทีเดียว ส่วนโปรแกรมหลักที่จะเขียนก็มีโค้ดดังตัวอย่างต่อไปนี้

main.m

Program 7.3 Output

Device 
price 5000.0

Computer
speed 450 mHz
price 12000.0

จากโค้ดบรรทัดที่ 19 จะเห็นว่า computer นั้นสามารถเรียกใช้ printPrice ของคลาสแม่ได้เลย รวมถึงการใช้ตัวแปรต่างๆภายในคลาสก็ทำได้เช่นกัน โปรดจำไว้ว่าการ inheritance จะเป็นลักษณะบนลงล่าง ดังนั้นคลาสที่ทำการสืบทอดจะมีเมธอดและตัวแปรต่างๆของคลาสแม่มาด้วยเสมอ เช่น Computer ก็จะได้ทุกๆอย่างจากคลาส ElectronicDevice รวมถึง NSObject เพราะเนื่องจาก ElectronicDevice ได้สืบทอด NSObject มาอีกที ฉะนั้นแล้วการสืบทอดถึงแม้จะมีข้อดีมากมาย แต่ก็มีข้อเสียคือคลาสจะมีขนาดใหญ่ขึ้นเรื่อยๆ

 

ปล. ต่อ part 2

ส่วน Source code โหลดได้ที่ github

3 thoughts on “Objective-C Programming Chapter 7 (Part 1)”

  1. ขอบคุณมากครับ แต่ขอแจ้งคำผิดนิดนึง @synthesize เขียนเป็น @systhesize หลายที่เลยครับ ^_^

Leave a Reply