Objective-C Programming Chapter 15 (Part2)

Blocks and Cocoa

ในตัวอย่างที่ผ่านๆมา เราได้ใช้ block กันไปบ้าง แต่ยังไม่ได้ใช้เฟรมเวิร์คและ API ต่างของ cocoa และ cocoa touch ซึ่งมีเมธอดที่เกี่ยวข้องกับใช้บล็อกอยู่มากมาย เมธอดเหล่านี้แบ่งออกเป็นกลุ่มใหญ่ๆทั้งหมด 6 กลุ่มด้วยกันคือ Completion handlers , Notification handlers , Error handlers , Enumeration , View animation and transition และ Sorting เพื่อให้เข้าใจมากขึ้นจะเขียนโปรแกรมเพื่อใช้งานบางส่วนจากทั้งหมด ซึ่งโปรแกรมที่จะเขียนต่อไปนี้ เป็นโปรแกรมที่เคยเขียนมาแล้วในบทก่อน นั่นคือจัดเรียงสมาชิกในอาร์เรย์ แต่คราวนี้จะใช้บล็อกในการจัดเรียงสมาชิก

Program 15.7
main.m

อาร์เรย์ wordList เรียกใช้เมธอด sortUsingComparator เพื่อใช้จัดเรียงสมาชิกในอาร์เรย์ และเมื่อดูคำอธิบายของเมธอดจาก document ก็จะพบว่าพารามิเตอร์ที่เมธอดนี้ต้องการคือ NSComparator ซึ่งเป็น block

sortCom

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

NSCompare

การเขียนโปรแกรมด้วย block อาจจะต้องเปิดดู document บ่อยๆ เนื่องจากต้องรู้ด้วยว่า block ต้องการพารามิเตอร์และส่งค่าอะไรกลับมาบ้าง อย่างไรก็ตาม XCode ได้มีตัวช่วยนั่นคือ auto complete เพื่อให้เราเขียนโปรแกรมได้ง่ายขึ้น

auto_com

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

Program 15.7 Output

Object at index 0 is Apple
Object at index 1 is Avocado
Object at index 2 is Banana
Object at index 3 is Mango
Object at index 4 is Orange

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

Blocks Memory Management

โครงสร้างของบล็อกจะถูกสร้างใน stack memory ดังนั้นเมื่อบล็อกได้ออกนอก scope ที่ได้ประกาศไว้ก็จะถูกลบออกโดยอัตโนมัติ ดังนั้นฟังก์ชั่นหรือเมธอดที่ได้ค่าส่งกลับมาเป็นบล็อกย่อมไม่สามารถทำได้โดยตรง เพราะบล็อกได้หลุดออกจาก scope ของฟังก์ชันไป ดังเช่นโค้ดตัวอย่างต่อไปนี้

Program 15.8
main.m

เมื่อคอมไพล์โปรแกรมจะไม่แจ้งเตือน error แต่เมื่อให้โปรแกรมทำงาน โปรแกรมจะเกิดข้อผิดพลาดและปิดตัวลง ปัญหาที่เกิดขึ้นก็คือ ฟังชั่น testBlock ขอบล็อกกับตัวแปร block โดยการเรียกฟังชั่น getBlock แต่เมื่อฟังก์ชั่น getBlock ส่งบล็อกกลับมา บล็อกที่ได้ก็หลุดจาก scope ของฟังก์ชัน getBlock ไปแล้ว ดังนั้นโปรแกรมก็จะลบบล็อกทิ้งเพราะอยู่นอก scope นั่นเอง

ถ้าลองคอมไพล์อาจจะทำงานได้ปกติในโหมด debug เนื่องคอมไพล์เลอร์ได้จัดเรียงโค้ดใหม่ให้เรา แต่ถ้าคอมไพล์แบบ optimize เช่นในโหมด release โปรแกรมจะ crash ทันที

เพื่อที่จะแก้ปัญหานี้จึงต้องก็อบปี้บล็อกไปไว้ที่ heap memory ซึ่งการ copy นี้สามารถทำได้สองวิธีคือใช้ฟังก์ชั่น Block_copy ซึ่งเป็นฟังชั่นภาษา C ดังเช่นโค้ดตัวอย่าง

หรือวิธีที่สองคือใช้เมธอด copy ดังโค้ดต่อไปนี้

อย่างที่ได้เคยบอกไปว่าบล็อกในภาษา Objective-C นั้นเป็นอ็อบเจ็กอย่างหนึ่ง จึงส่งแมสเซจ copy ไปหาบล็อกได้ หรือแม้กระทั่งจะเก็บบล็อกไว้ในอาร์เรย์ก็ทำได้เช่นกัน และหลังจากที่เราได้ copy ไปแล้ว ตามกฎของ retain count ก็ต้อง release เพื่อให้การนับจำนวน retain นั้นสมดุลกัน ไม่เช่นนั้นก็จะเกิดปัญหา memory leak ได้ การที่จะคืนหน่วยความจำก็สามารถทำได้ด้วยการใช้เมธอด Block_release และการใช้เมธอด release หรือ autorelease

หรือ

เราจะเขียนโปรแกรมกันสักโปรแกรม เพื่อให้เข้าใจถึงวิธีการจัดการหน่วยความจำสำหรับ block มากขึ้น

Program 15.9
Product.h

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

Product.m

เมธอดแรกที่ได้เพิ่มเข้ามาคือ setPrintBlock: โค้ดภายในเมธอดจะทำการ copy บล็อกที่ได้รับจากพารามิเตอร์ เพื่อนำมาใช้ในภายหลัง ต่อมาคือ getPrintBlock เมธอดนี้จะส่งบล็อกที่ได้จาก setPrintBlock กลับไปให้ยังผู้ที่เรียกโดยการก็อบปี้บล็อก และเมธอดสุดท้ายคือ printWithName:andPrice เป็นเมธอดที่ใช้ทดสอบบล็อก

main.m

Program 15.9 Output

Product name: New Macbook
Price 400.000000
Product name: New Macbook Air
Price 600.000000

โค้ด main ประกาศอ็อบเจ็กขึ้นมาสองตัวคือ macbook และ macbookAir จากนั้นได้กำหนดบล็อกให้กับ macbook เพื่อใช้แสดงชื่อและราคาของ Product เมื่อได้รับบล็อกเข้ามาแล้ว ตัวแปร printBlock ของอ็อบเจ็ก macbook ก็จะก็อบปี้ไว้ เพื่อให้แน่ใจว่าทำงานได้จริงไม่เกิดปัญหา จึงทดสอบด้วยการเรียกเมธอด printWithName:andPrice ดังที่แสดงในบรรทัดที่ 21 ส่วนในบรรทัดที่ 23 เราต้องการที่จะทดสอบว่าสามารถขอบล็อกได้จากเมธอด จึงเรียก getPrintBlock เพื่อนำบล็อกที่ได้ไปใช้ต่อในอ็อบเจ็ก macbookAir และสุดท้ายก็ทดสอบด้วย printWithName:andPrice ว่าบล็อกที่ได้รับมาใช้งานได้จริง และจากผลลัพธ์โปรแกรมก็ทำงานได้อย่างถูกต้อง

ในการจัดการหน่วยความจำของ block ควรเลือกวิธีใดวิธีหนึ่ง เช่นถ้าใช้เมธอด copy ก็ควรใช้ทั้งโปรแกรม ไม่ควรใช้ปะปนกันกับ ฟังก์ชั่น Block_copy

 

Traps

แม้ว่าบล็อกจะมีความสะดวกมาก แต่การใช้งานไม่ระมัดระวังก็ก่อให้เกิดข้อผิดพลาดได้ โค้ดตัวอย่างต่อไปนี้เป็นสิ่งที่เกิดขึ้นได้บ่อยๆ จากการใช้บล็อก

Out of scope
เนื่องจาก block อยู่บน stack memory และอย่างที่ได้อธิบายไปว่าบล็อกจะถูกทำลายเมื่ออยู่นอก scope ซึ่งเราก็ได้ใช้ copy ป้องกันปัญหาไปแล้ว แต่ลองพิจารณาโค้ดต่อไปนี้ที่ทำงานใน main

จากโค้ดถ้า isSuccess เป็นจริงก็ให้ workingBlock เป็นบล็อกที่แสดงผลว่า Success แต่ถ้าไม่จริงก็ให้ทำงานอีกอย่าง จะเห็นว่าการกำหนดค่าให้กับ workingBlock นั้นอยู่ใน scope ของ main เราไม่ได้สร้างบล็อกในฟังก์ชั่นอื่นแต่อย่างใดซึ่งก็น่าจะทำงานปกติดี แล้วตรงไหนที่เกิดปัญหา ? เมื่อดูจากโค้ดผิวเผินจะเหมือนว่า workingBlock นั้นอยู่ใน main แต่แท้ที่จริงแล้วบล็อกอยู่ใน scope อื่นที่ไม่ใช่ main นั่นก็คือ if-else เนื่องจากว่าการเขียนโค้ด if-else แบบบรรทัดเดียว

คือรูปย่อของการเขียนโค้ด if-else แบบนี้

ดังนั้นการประกาศบล็อกแบบโค้ดตัวอย่าง ก็เหมือนกับประกาศใน scope ของ if-else นั่นเอง เมื่อการทำงานของ if-else จบลง บล็อกก็จะหลุดจาก scope ไปทำให้โปรแกรมทำงานผิดพลาด ดังนั้นเราควรเขียนใหม่เป็น

ใช้การ copy เก็บบล็อกไว้เพื่อให้โปรแกรมทำงานได้อย่างถูกต้อง และต้องไม่ลืมว่าเมื่อ copy แล้วต้อง release ไม่งั้นก็จะเกิด memory leak ตามมาได้ อีกตัวอย่างหนึ่งที่พบได้บ่อยคือการใช้คลาส collection ต่างๆเก็บ block เช่น

คลาส collection โดยปกติเมื่อเพิ่มอ็อบเจ็กเข้าไปก็จะเพิ่ม retain count ให้กับอ็อบเจ็กนั้นโดยอัตโนมัติ และเมื่อคลาสได้ถูกทำลายก็จะส่งแมสเสจ release เพื่อลด retain count ให้กับอ็อบเจ็กที่ได้เก็บไว้ จากโค้ดตัวอย่างบล็อกก็ควรต้องถูก retain โดยดิกชันนารีอัตโนมัติเพราะบล็อกเป็นอ็อบเจ็กชนิดหนึ่ง แต่เนื่องจากว่าบล็อกอยู่ใน stack memory ซึ่งต่างจากอ็อบเจ็กทั่วๆไปที่อยู่ใน heap memory ดังนั้นแล้วดิกชันนารี่จะไม่สามารถ retain บล็อกได้นั่นเอง เมื่อใช้งานก็จะเกิดปัญหา out of scope เช่นเดียวกัน การแก้ปัญหาก็ใช้หลักการเดิมคือต้อง copy บล็อกไปไว้ยัง heap memory ดังโค้ดตัวอย่างต่อไปนี้

Strong Reference Cycle
นอกจากปัญา out of scope แล้ว ปัญหาที่เกิดขึ้นบ่อยๆก็คือปัญหาของ retain cycle ซึ่งเกิดจากการสร้างบล็อกในคลาส และโค้ดของ block ได้ใช้อ้างอิงถึงตัวเอง (self) เช่นการใช้พร้อพเพอร์ตี้หรือเรียกเมธอดของตัวเอง เช่นโค้ดต่อไปนี้

คลาสได้ประกาศให้ printBlock เป็นพร็อพเพอร์ตี้ของคลาส Product โดยกำหนดให้มี attribute เป็น copy นั่นหมายความว่าเมื่อไหร่ก็ตามที่เรากำหนดค่าให้กับ printBlock โปรแกรมจะ copy บล็อกไว้ให้โดยอัตโนมัติ จากนั้นก็เขียนเมธอดเพื่อสร้างบล็อกดังนี้

จะเห็นว่าเมื่อสร้างบล็อกไม่จำเป็นต้องเรียก copy เพราะได้กำหนดให้เป็น attribute อยู่แล้ว แต่การทำงานของบล็อกได้อ้างอิงถึง self หรือตัวมันเอง โดยการเรียกเมธอด doSomeThing และเมื่อไหร่ก็ตามที่บล็อกได้ใช้ตัวแปรจากภายนอกบล็อก สิ่งที่เกิดขึ้นก็คือบล็อกจะ retain ตัวแปรนั้นให้โดยอัตโนมัติ จากตัวอย่างโค้ดตัวแปรนอก scope ของบล็อกที่ไม่ใช่พารามิเตอร์หรือตัวแปรที่ประกาศภายในบล็อกก็คือ self และเมื่อ self เป็นตัวแปรนอกนั่นหมายความว่า self ก็จะถูกบล็อก retain ให้อัตโนมัติ ปัญหาที่ตามมาก็คือเนื่องจาก self เป็นได้ประกาศให้บล็อกเป็นพร็อพเพอร์ตี้และกำหนดให้มี attribute เป็น copy ดังนั้น self ก็เพิ่มค่า retain ให้กับบล็อกอีกรอบหนึ่ง สิ่งที่เกิดขึ้นนี้คือ strong reference ซึ่งจะทำให้ค่า retain count ไม่มีทางเป็น 0 เพราะต่างฝ่ายก็ต่าง retain กันและกันอยู่นั่นเอง
cycle
การแก้ปัญหานี้สามารถทำได้ด้วยการกำหนดให้ self เป็น weak reference ดังเช่นโค้ดตัวอย่าง

สิ่งที่ได้ทำไปก็คือสร้างตัวแปรใหม่ให้อ้างอิงถึง self และประกาศให้ใช้ร่วมกับบล็อกด้วยคีร์เวิร์ด __block นอกจากคีร์เวิร์ด __block นี้จะทำให้บล็อกสามารถแก้ไขตัวแปรที่ใช้ร่วมกันได้ ตัวแปรนั้นจะไม่ถูกบล็อก retain โดยอัตโนมัติ ในกรณีมีการจัดการหน่วยความจำด้วย ARC เราจะใช้ __weak แทน ซึ่งจะได้กล่าวในบทของการใช้ ARC อีกครั้ง

 

Summary

ฟังก์ชั่นพ้อยเตอร์มีประโยชน์มากมาย แต่ติดข้อจำกัดที่ไม่สามารถส่งผ่านค่านอก scope เข้ามาได้ การใช้ block เป็นทางเลือกที่ดีกว่ามาก และในบทนี้เราได้ทำความเข้าใจเกี่ยวกับบล็อกกันไปพอสมควร รวมถึงปัญหาต่างๆที่ควรระวัง อย่างไรก็ตามเรายังไม่ได้เห็นประสิทธิภาพที่แท้จริงของบล็อก ซึ่งจะมีประโยชน์มากเมื่อใช้ร่วมกับ Grand Central Dispatch ซึ่งจะได้ทำความเข้าใจกันในบทต่อไป

 

โหลด PDF ไปอ่านกันได้ครับ Chapter 15

Source code

 

Leave a Reply