Objective-C Programming Chapter 12 Copying Object

Chapter 12

Copying Object

ในบทนี้เราจะว่าด้วยเรื่องของการ copy object หรือการคัดลอกข้อมูลระหว่างอ็อบเจ็ก และคอนเซ็ปของการ copy ต่างๆเช่น shallow copy และ deep copy และวิธีการก๊อบปี้อ็อบเจ็กของคลาสต่างๆใน Foundation Framework
โดยปกติเมื่อเราประกาศตัวแปรที่เป็น primitive type เช่น int , double การที่เราจะก๊อบปี้ข้อมูลทำได้ง่ายมากเพียงแค่ใช้เครื่องหมาย = ก็สามารถคัดลอกข้อมูลได้แล้ว ดังตัวอย่างโค้ดต่อไปนี้

กำหนดให้ตัวแปร x มีค่าเท่ากับ 10 หลังจากนั้น y ก็ก๊อบปี้ค่าตัวแปร x เพื่อให้ตัวแปรทั้งสองมีค่าเท่ากัน จากนั้นบวกค่าให้กับตัวแปร x ด้วย 20 ส่วนตัวแปร y บวกด้วย 30 เมื่อบวกด้วยคนละค่ากัน แน่นอนว่าตัวแปรทั้งสองย่อมมีค่าไม่เท่ากัน เห็นได้ชัดว่าตัวแปร x และ y นั้นไม่ได้มีผลกระทบอะไรต่อกัน การเปลี่ยนแปลงค่า x ไม่ได้เกี่ยวข้องกับค่า y และในทางกลับกันค่า y ก็ไม่ได้เกี่ยวข้องกับค่า x แต่ประการใด ลองพิจารณาโปรแกรมตัวอย่างต่อไปนี้

โค้ดของโปรแกรมได้ประกาศสองตัวคือตัวแปร hello และ temp ซึ่งได้กำหนดค่าเริ่มต้นให้เท่ากับ “Hello” จากนั้นเราได้ให้ค่า temp = hello ดังนั้นค่าตัวแปร temp ก็ควรจะมีเท่ากับ “Hello” และในบรรทัดต่อมาเราได้เปลี่ยนแปลงค่าของ temp ด้วยการนำสตริง “ World” มาต่อท้าย เมื่อเราแสดงค่าของตัวแปรด้วย NSLog ก็จะได้ผลลัพธ์ดังนี้

Program Output

Hello World
Hello World

จากผลลัพธ์ของโปรแกรมแสดงให้เห็นว่าตัวแปร hello และ temp นั้นต่างก็เป็นคำว่า Hello World ทั้งๆที่เราไม่ได้ไปแก้ไข hello เลยสักนิด ทำไมถึงเป็นแบบนี้ ? ในความเป็นจริงแล้วสิ่งที่เราได้ทำก็คือประกาศตัวแปรที่เป็น class instance หรือ object นั้นเป็นแบบ pointer ฉะนั้นแล้วการเขียนโค้ด

เป็นการก๊อบปี้ตำแหน่งหน่วยความจำ (Memory Address) หรือพูดอีกอย่างว่าให้ temp ชี้ไปยังตำแหน่งเดียวกันกับ hello
reff

เมื่อเป็นตำแหน่งเดียวกัน การแก้ไขตัวแปร temp ย่อมเท่ากับแก้ไข hello เช่นเดียวกัน โปรแกรมตัวอย่างแสดงให้เห็นว่าเราไม่สามารถที่จะใช้เครื่องหมาย = ในการก๊อบปี้ได้ แล้วเราจะก๊อบปี้อ็อบเจ็กได้อย่างไร ?

copy and mutable copy

ปัญหาที่เกิดขึ้นสามารถแก้ไขได้ด้วยการใช้เมธอดก๊อบปี้ที่มีอยู่แล้วของ Foudation Class ซึ่งมีด้วยกันสองเมธอดคือ copy และ mutableCopy สองเมธอดนี้ต่างกันตรงที่ ถ้าเราต้องการจะก๊อบปี้แล้วให้อ็อบเจ็กนั้นสามารถแก้ไขค่าต่างได้ ก็ต้องใช้ mutableCopy เพราะเมธอดนี้จะสร้างอ็อบเจ็กที่เป็นแบบ mutable นั่นเอง

Program 12.1

main.m

Program 12.1 Output

Hello
Hello World

เมื่อคอมไพล์และให้โปรแกรมทำงานก็จะเห็นว่าผลลัพธ์นั้นแตกต่างกับโปรแกรมแรก ตัวแปร hello และ temp ต่างเป็นอิสระต่อกัน เพราะการใช้เมธอด mutableCopy หรือ copy นั้นจะเป็นการสร้างอ็อบเจ็กใหม่โดยมีข้อมูลเหมือนกับตัวต้นแบบ เมื่อเป็นคนละอ็อบเจ็กการเปลี่ยนแปลงค่าของตัวแปรทั้งสองจึงไม่เกี่ยวข้องกัน

พิจารณาตัวอย่างโปรแกรมง่ายๆต่อไปนี้อีกสักโปรแกรม เพื่อทำความเข้าใจเกี่ยวกับการก๊อบปี้อ็อบเจ็กมากขึ้น

Program 12.2

main.m

Program 12.2 Output

B: (
    Apple,
    Mango
)

A: (
    Apple,
    Mango,
    Orange
)

จากโปรแกรมเราได้สร้างอาเรย์ fruits_B ซึ่งประกอบไปด้วย Apple Mango Organge ตามลำดับ จากนั้นเราสร้าง fruits_A ด้วยการก๊อบปี้ fruits_B ตอนนี้อาเรย์ทั้งสองตัวมีสมาชิกเหมือนกัน แต่เมื่อถึงบรรทัดที่ 18 เราได้ลบสมาชิกตัวท้ายสุดของ fruits_B ไป ดังนั้นผลลัพธ์จึงได้ดัง Output ที่แสดงออกมา จะเห็นว่า A และ B มีสมาชิกไม่เท่ากัน เราจะลองพิจารณาโปรแกรมอีกสักหนึ่งโปรแกรมโดยการแก้ไขโค้ดของโปรแกรม 12.2 ให้เป็นดังนี้

Program 12.3

main.m

เราแก้ไขโค้ดเปลี่ยนโค้ดบรรทัดที่ 18 ของโปรแกรม โดยนำสตริง 123 มาต่อท้ายอ็อบเจ็กแรกในอาเรย์ fruits_B ซึ่งนั่นก็คือ “Apple” ดังนั้นเมื่อเราให้แสดงค่าของตัวแปร B จึงควรจะเป็น Apple123 , Mango , Orange แต่ผลลัพธ์ที่ได้กลับเป็นดังนี้

Program 12.3 Output

B: (
    Apple123,
    Mango,
    Orange
)

A: (
    Apple123,
    Mango,
    Orange
)

แน่นอนว่าค่า fruits_B แสดงออกมาได้ถูกต้องตามที่ได้คาดคิดไว้ แต่สิ่งที่ตามก็คือ Apple ใน fruits_A กลายเป็น Apple123 เหมือนกับ fruits_B เหตุใดจึงเป็นเช่นนี้ ? ทั้งๆที่เราก็ได้เมธอด copy ดังนั้นตัวแปร fruits_A และ fruits_B ก็น่าจะเป็นคนตัวกัน แต่เมื่อแก้ไขสมาชิกของอาร์เรย์ fruit_B กลับส่งผลกระทบต่อสมาชิกใน fruit_A

Shallow and Deep copy

ตัวแปร fruits_A นั้นก๊อบปี้ fruits_B มา นั่นก็หมายถึงว่ามันใช้เป็นคนละอ็อบเจ็ก ดังนั้นมันก็ควรจะไม่เกี่ยวกัน ซึ่งก็ถูกต้องแล้ว ดังเช่นในโปรแกรมที่ 12.2 ที่เราได้ลบสมาชิก fruits_B ออกไปก็ไม่เกี่ยวกับ fruits_A แล้วปัญหาที่เกิดขึ้นของโปรแกรม 12.3 นั้นคืออะไร ? ปัญหาที่เกิดขึ้นจริงๆคืออาร์เรย์เป็นคนละตัวกัน แต่สมาชิกของอาร์เรย์ทั้งสองเป็นตัวเดียวกัน เพราะเนื่องจากว่าตัวแปรที่ fruit_A นั้นเก็บไว้มันเป็นเพียง pointer ที่ชี้ไปยัง apple , orange , mango นั่นก็หมายความว่าสมาชิกในอาเรย์ของ fruits_A และ fruits_B ไม่ได้ถูกสร้างขึ้นมาใหม่ แต่เป็นเพียงแค่การ copy reference หรือเก็บ memory address มาแค่นั้น ฉะนั้นการแก้ไขสมาชิกใน fruits_B ย่อมมีผลกระทบโดยตรงต่อสมาชิก fruits_B เพื่อให้เข้าใจมากขึ้นลองพิจารณาภาพประกอบต่อไปนี้

array

จากรูปจะเห็นว่า fruit_A และ fruit_B นั้นเป็นคนละอ็อบเจ็กกัน แต่สมาชิกภายในกลับชี้ไปยังตำแหน่งสตริงเดียวกัน สิ่งที่เกิดขึ้นลักษณะนี้เรียกว่า shallow copy หรือถ้าหากเราต้องการจะก๊อบปี้อ็อบเจ็กที่เราเขียนขึ้นมาเอง ดังเช่น

เมื่อคอมไพล์โปรแกรมจะแจ้ง error บอกว่าไม่เจอ copyWithZone: (เมธอดนี้จะถูกเรียกโดยเมธอด copy อีกที)

*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[MacBook copyWithZone:]: unrecognized selector sent to instance 0x100108a80’
*** First throw call stack:

แล้วเราจะทำอย่างไรเพื่อที่จะให้อาร์เรย์ทั้งสองและสมาชิกแยกขาดกัน หรือทำอย่างให้เราสามารถก๊อบปี้อ็อบเจ็กที่เราสร้างขึ้นเองได้ ? การแก้ปัญหาก็คือเราต้องทำ deep copy  แต่เราต้องเขียนโค้ดในส่วนนี้เอง ไม่เหมือนกับเมธอด copy และ mutable ที่สามารถเรียกใช้ได้เลย วิธีการคือเราจะเขียนโค้ดของโปรโตคอล <NSCopying>  สำหรับคลาสของอ็อบเจ็กนั้นๆ ดังเช่น ตัวอย่างโปรแกรมต่อไปนี้ (คลาส Product จากโปรแกรม 8.7)

Program 12.4

product.h

ในส่วนของ interface เราได้เพิ่ม <NSCopying> เพื่อประกาศว่าเราจะเขียนแคทิกกอรีนี้ หลังจากนั้นก็ต้องเขียนเมธอดที่แคทิกอรี่บังคับนั่นคือ copyWithZone: (ถ้าดูจาก error ก่อนหน้านี้ เราก็พอจะคาดเดาได้ว่า ต้องเขียนเมธอด copyWithZone)

product.m

เมื่อดูโค้ดของ copyWithZone: เราได้ประกาศอ็อบเจ็กขึ้นมาใหม่ ด้วยเมธอด allocWithZone: และส่งพารามิเตอร์ zone ที่ได้รับเข้ามา พารามิเตอร์ zone นี้เป็นตัวกำหนดขอบเขตพื้นส่วนของหน่วยความจำ โดยปกติเราไม่ต้องไปยุ่งเกี่ยวกับ memory zone เลย เว้นแต่ในกรณีที่เราต้องการจะกำหนดขอบเขตพื้นที่หน่วยความจำเอง เช่น เราต้องการให้สมาชิกของคลาสจองพื้นที่ในบริเวณเดียวกัน ซึ่งอาจจะเขียนโค้ดส่วนของ init ได้ดังตัวอย่างต่อไปนี้

ดังนั้นเมื่อเราส่ง zone เป็นพารามิเตอร์ ก็หมายถึงว่าเราต้องการจะให้อ็อบเจ็กใหม่นี้ อยู่ในส่วนเดียวกันกับอ็อบเจ็กที่เราต้องการจะก๊อบปี้นั่นเอง เมื่อเราสร้างอ็อบเจ็กใหม่แล้วจากนั้นก็เรียกเมธอด setName:andPrice: เพื่อกำหนดค่าต่างๆของอ็อบเจ็ก และถ้าหากสังเกตดูจะพบว่าเมธอดที่ส่งค่ากลับเป็นอ็อบเจ็กที่เคยเขียนมาต้องเรียก autorelease แต่เมธอดนี้กลับไม่ต้องเรียก นั่นก็เพราะว่า เมธอด initWithZone: นี้ ถูกเรียกโดย copy ซึ่งจะเพิ่มค่า retain count อยู่แล้ว เพื่อให้เป็นไปตามกฎการนับจำนวน retain เราจึงไม่ต้อง release ในเมธอดนี้ เพราะผู้เรียกจะเป็นคนสั่ง release เอง

main.m

Program 12.4 Output

Computer 100
Computer 100

จากโค้ดของโปรแกรม แสดงให้เห็นว่าอ็อบเจ็ก samsung ก็อปปี้ apple ได้โดยใช้เมธอด copy และอ็อบเจ็กที่ได้ก็เหมือนกันทุกประการ และโปรแกรมก็ทำงานได้สมบูรณ์ แต่อย่างไรก็ตามถ้าหากเราซับคลาส Product เมธอด copy นี้ก็จะถูก inherited ไปด้วย นั่นหมายความว่าเมื่อเราเรียกเมธอด copy ด้วยคลาสใหม่ เราจะไม่ได้คลาสใหม่แต่จะได้ Product แทน การแก้ไขอาจจะทำได้โดยเปลี่ยนโค้ด เพื่อให้แน่ใจว่าเมธอด copy จะสร้างอ็อบเจ็กที่เป็นซับคลาสไม่ใช่คลาสแม่ ซึ่งมีโค้ดเป็นดังนี้

หรือเราอาจจะเขียน copyWithZone: ของซับคลาสขึ้นมาใหม่ก็ได้ แต่เราควรเรียกเมธอด copy ของ super ก่อนด้วยเพื่อให้แน่ใจว่าตัวแปรต่างๆในคลาสแม่ก๊อบปี้อย่างถูกต้อง

Copy NSArray

ในกรณีที่เราใช้ NSArray หรือ NSMutableArray และต้องการจะก๊อบปปี้สมาชิกทั้งหมดมาด้วย เราสามารถใช้เมธอด initWithArray:copyItem: เพื่อช่วยให้เราก๊อบปี้อาร์เรย์ได้อย่างง่ายดาย ดังเช่นตัวอย่าง โปรแกรมต่อไปนี้

Program 12.5

main.m

Program 12.5 Output

A: (
    Apple,
    Mango,
    Orange
)

B: (
    Apple123,
    Mango,
    Orange
)

ในบรรทัดที่ 16 จะเห็นว่าเราได้เรียกใช้เมธอด initWithArray:copyItem: และส่งพารามิเตอร์ fruit_A  และ YES ตามลำดับ หลังจากนั้นก็แก้ไขสมาชิกตัวแรกของอาร์เรย์ B โดยเพิ่ม “123” ต่อท้ายเข้าไป เมื่อคอมไพล์โปรแกรมก็จะเห็นว่าอาร์เรย์ทั้งสองไม่ได้เกี่ยวข้องกันแล้ว

Setter/Getter & Copy Attribute

เมื่อไหร่ก็ตามที่เราเขียน setter หรือ getter เราควรจะพิจารณาถึงวิธีการเก็บค่าของตัวแปรว่ารับเข้ามาแบบใด เพื่อที่จะได้จัดการกับตัวแปรนั้นได้อย่างถูกต้อง เช่นสมมติว่าเขียน เมธอด setter ดังนี้

เมธอด setPrice: สามารถใช้การให้ค่าได้โดยตรง เพราะเป็น primitive type แต่ในกรณี setName นั้นไม่สามารถทำแบบนี้ได้ เพราะจากที่เราได้เรียนรู้ไปแล้วว่า การให้ค่าแบบนี้เป็นการให้ _name ชี้ไปยังตำแหน่งเดียวกับพารามิเตอร์ name ดังนั้นแล้วสิ่งที่ควรจะแก้ไข ก็คือเปลี่ยนไปใช้เมธอด copy ดังนี้

แต่อย่างไรก็ตามการไขแบบนี้ก็ยังจะเกิดปัญหาอยู่เพราะเนื่องจากเมื่อเรียกใช้เมธอด copy สิ่งที่จะเกิดขึ้นก็คือ _name จะเพิ่มค่า retain count และเราต้อง release เพื่อให้จำนวน retain นั้นเท่ากัน ดังนั้นแล้ว เราอาจจะแก้ไขเปลี่ยนโค้ดให้เป็น

จากโค้ดเราตรวจสอบก่อนว่าตัวแปร _name มีค่าเก่าอยู่หรือไม่ ถ้ามีก็ให้ release ก่อนแล้วถึงจะ copy ค่าใหม่

ในบทที่ 7 เราได้พูดถึง copy arrtibute ของ property กันไปบ้าง ถ้าหากเราประกาศพร๊อพเพอร์ตี้แบบ copy เช่น

และเมื่อคอมไพล์เลอร์สร้างโค้ดจาก @synthesize โค้ดจะรูปแบบเดียวกันกับเมธอด setName ที่ได้แก้ไขไป

ในบทนี้เราได้เรียนรู้การก๊อบปี้อ็อบเจ็ก และได้ทำความเข้าใจเพิ่มเกี่ยวกับ shallow copy , deep copy รวมไปถึงความเข้าใจเกี่ยวกับการใช้ attribute แบบ copy ในบทต่อไปเราจะได้เรียนรู้กับ Archiving ซึ่งเป็นการรวมข้อมูลจากหลายอ็อบเจ็กมาไว้ในรูปแบบเดียว

 

โหลด PDF – Chapter 12 – Copying Object
Source code – Git hub

Leave a Reply