Objective-C Programming Chapter 15 (Part1)

Chapter 15

Blocks

บล็อกเป็นส่วนขยายของภาษา C,C++ และ Objective-C ที่ทาง Apple ได้เพิ่มเติมเข้ามา ส่วนขยายนี้เป็นแบบ non standard extension นั่นหมายความว่าไม่ใช่คอมไพลเลอร์ทุกตัวที่จะรองรับ แต่มีเพียงบางตัวเท่านั้นที่สามารถใช้งานได้ เช่น GNU Compiler และ CLang LLVM ถึงแม้ว่าบล็อกจะมีลักษณะคล้ายกับฟังก์ชั่น คือ ทำงานได้เหมือนกับฟังก์ชั่น สามารถรับพารามิเตอร์ได้ แต่ก็มีส่วนที่แตกต่างกันกับฟังก์ชั่นพอสมควร เช่น ไม่จำเป็นต้องตั้งชื่อ หรือ ไม่ต้องระบุชนิดของ return type เป็นต้น ในภาษา Objective-C บล็อกถือเป็นอ็อบเจ็กชนิดหนึ่ง ดังนั้นเราจึงสามารถส่งแมสเซจ หรืออาจจะเก็บบล็อกไว้ใน collection class เช่น NSArray หรือ NSDictionary ได้ บล็อกเป็นส่วนสำคัญในการพัฒนา Cocoa Application และ iOS Application เพราะได้ถูกออกแบบมาเพื่อรองรับเทคโนโลยี Grand Central Dispatch ทำให้การพัฒนาโปรแกรมแบบ Concurrent Programming ทำได้ง่ายขึ้น และยังเหมาะกับการใช้เป็น callback เพราะบล็อกสามารถนำโค้ดการทำงานของ callback และข้อมูลที่จำเป็นไปพร้อมกับบล็อกได้

Function pointer

บล็อกมีความคล้ายคลึงกับ function pointer เป็นอย่างมาก จึงควรที่จะทำความเข้าใจ function pointer เบื้องต้นกันเสียก่อน แล้ว function pointer คืออะไร ? ฟังก์ชั่นพ้อยเตอร์ก็คือพ้อยเตอร์ที่ชี้ไปยังฟังก์ชั่น อาจจะเกิดความสงสัยว่าทำได้ด้วยเหรอ ? ไม่ว่าจะเป็นข้อมูล อ็อบเจ็ก หรืออะไรก็ตาม เมื่ออยู่ในหน่วยความจำก็ย่อมมีตำแหน่งของหน่วยจำเสมอ ฟังก์ชันก็เช่นเดียวกัน การใช้ function pointer มีประโยชน์มากในบางกรณี เป็นต้นว่า เมื่อเขียนโปรแกรมให้รองรับทั้ง iPhone และ iPad โค้ดของโปรแกรมส่วนมากใช้ร่วมกันได้ แต่ก็มีบางส่วนที่ไม่สามารถใช้งานร่วมกันได้ เราอาจจะแก้ปัญหานี้ด้วยการใช้ macro อย่าง #ifdef และเขียนฟังก์ชั่นสำหรับ iPad แยกออกมาจาก iPhone  ดังเช่นโค้ดต่อไปนี้

ถ้าโปรแกรมเรียกใช้ฟังก์ชั่นเพียงไม่กี่ครั้ง ก็คงไม่มีปัญหาอะไร แต่ถ้าหากโปรแกรมเรียกใช้ฟังก์ชั่นหลายๆครั้ง โค้ดของโปรแกรมก็จะยาวขึ้นและเข้าใจยากมากขึ้นไปด้วย

อีกหนึ่งวิธีของการแก้ปัญหาคือการใช้ฟังก์ชั่นพ้อยเตอร์ ซึ่งการประกาศฟังก์ชั่นพ้อยเตอร์มีรูปแบบดังนี้

fpter

โค้ดตัวอย่างได้ประกาศตัวแปร functionName ให้เป็น function pointer การประกาศฟังก์ชั่นพ้อยเตอร์มีข้อบังคับคือ ต้องกำหนดค่าส่งกลับและอากิวเม้นต์ (พารามิเตอร์) ให้ตรงกับฟังก์ชั่นที่เราจะชี้ไป และหลังจากที่เรามีตัวแปรฟังก์ชั่นพ้อยเตอร์แล้ว ก็สามารถที่จะกำหนดให้พ้อยเตอร์ functionName ชี้ไปยังฟังก์ชั่นที่ต้องการได้ ดังเช่นตัวอย่างโปรแกรม 15.1

Program 15.1
main.m

Program 15.1 Output

12
7

โปรแกรม 15.1 ประกาศ func ให้เป็นฟังก์ชั่นพ้อยเตอร์และให้ชี้ไปยังฟังก์ชั่น mul ดังนั้น func ก็เปรียบเสมือนฟังก์ชั่น mul จึงสามารถที่จะใช้ฟังก์ชั่น mul ผ่าน func ได้เช่นกัน ดังที่แสดงในบรรทัดที่ 21 ซึ่งได้ผลลัพธ์คือ 12 ต่อมาได้กำหนดค่าใหม่ให้กับฟังก์ชันพ้อยเตอร์โดยเปลี่ยนให้ชี้ไปยังฟังก์ชัน add และเมื่อเรียก func อีกครั้ง ผลลัพธ์ที่ได้จึงเป็น 7 นั่นเอง  และถ้าหากใช้ฟังก์ชันพ้อยเตอร์เพื่อแก้ปัญหาที่ผ่านมา  เราอาจจะเขียนโค้ดใหม่ได้ดังนี้

เมื่อนำฟังก์ชั่นพ้อยเตอร์มาใช้ก็ทำให้ไม่ต้องเขียน #ifdef ทุกครั้งที่มีการเรียกฟังก์ชั่น func ทำโค้ดของโปรแกรมก็สั้นลง การสลับเปลี่ยนไปมาระหว่าง iPhone กับ iPad ก็แก้ไขเพียงแค่จุดเดียวเท่านั้น ความผิดพลาดก็น้อยลงตามไปด้วย

Callback

โดยปกติเมื่อส่งค่าให้กับฟังก์ชัน จะส่งตัวแปรที่เป็นค่าต่างๆเช่น int หรือ float เป็นต้น นอกจากนี้เรายังสามารถส่งโค้ดการทำงานของโปรแกรมอย่างเช่น ฟังก์ชั่น ให้เป็นพารามิเตอร์ได้เช่นกัน และเรียกฟังก์ชั่นที่ส่งเข้าไปว่า callback เพื่อให้เห็นภาพมากขึ้น เราจะเขียนโปรแกรมให้ปรับราคาสินค้าเป็น 2 เท่า ดังตัวอย่างต่อไปนี้

Program 15.2
main.m

ฟังก์ชั่น newPrice ส่งค่าสองเท่าของจำนวนกลับคืนมา ส่วน updateArray จะปรับราคาสินค้าตาม callback function ที่ได้รับมา ในส่วนของโปรแกรมหลักได้ประกาศอาร์เรย์ที่ประกอบไปด้วย macbook และ macbook air จากนั้นเรียกใช้ฟังก์ชัน updateArray พร้อมกับส่งอาร์เรย์และฟังก์ชัน newPrice เป็น callback ดังนั้นเมื่อฟังก์ชัน updateArray ทำงานก็จะเรียกฟังก์ชัน newPrice อีกทีหนึ่งนั่นเอง โปรแกรมจึงได้ผลลัพธ์ดังนี้

Program 15.2 Output

Macbook 400.00
Macbook Air 600.00

จากโปรแกรมจะเห็นว่าถ้าต้องการจะเปลี่ยนแปลงราคาด้วยฟังก์ชั่นอื่นก็ทำได้ง่ายมาก แค่เปลี่ยน callback ให้เป็นฟังก์ชั่นอื่นเท่านั้นเอง การใช้ callbacks เป็นเรื่องปกติในภาษา C ซึ่งอำนวยความสะดวกหลายๆอย่างดังที่เห็นในโปรแกรมตัวอย่าง แต่ callback ก็ยังมีข้อจำกัดบางประการนั่นคือการคำนวนหลายๆอย่างต้องการการกำหนดค่า แต่ callback ไม่ได้มีความสามารถที่จะส่งค่าเหล่านี้ผ่านเข้าไปได้ ลองจินตนาการว่าถ้าหากต้องการเขียนฟังก์ชันที่สามารถเปลี่ยนค่าที่ใช้ในการคูณได้ เช่น เปลี่ยนจาก 2 เป็น 0.5 เพื่อลดครึ่งราคา โค้ดของฟังก์ชัน newValue ปัจจุบันไม่อาจจะส่งค่า 0.5 ผ่านเข้าไปยัง callback ได้เลย นอกเสียจากจะสร้าง global variable ทางออกของปัญหาสามารถแก้ไขได้ด้วยการใช้ block

Declaring and Creating Blocks

บล็อกมีความคล้ายคลึงกับฟังก์ชั่นเป็นอย่างมาก แต่สิ่งที่แตกต่างกันคือบล็อกใช้ memory stack ร่วมกันกับโค้ดของโปรแกรมที่อยู่ใน scope เดียวกัน นั่นหมายความว่าบล็อกสามารถเข้าถึงตัวแปรภายนอกตัวมันเอง แต่ยังอยู่ใน scope ของคนที่เรียกมันได้  ซึ่งจะได้เห็นภาพชัดเจนมากขึ้นในตัวอย่างต่อไป การสร้าง block มีความคล้ายกันกับฟังก์ชั่น แต่จะใช้สัญลักษณ์พิเศษคือ ^ (Caret) เพื่อบอกจุดเริ่มของบล็อก ตามด้วยการกำหนดพารามิเตอร์ และเขียนส่วนการทำงานภายใน เครื่องหมาย { และ } เหมือนกับการเขียน method และ function  และจบลงด้วย ; เช่นเดียวกับ statement

โค้ดสั้นๆนี้จะเรียกว่า block literal มีการทำงานง่ายๆคือแสดงผลผ่าน NSLog โดยไม่ได้มีพารามิเตอร์ใดๆในการทำงาน กรณีที่ไม่มีพารามิเตอร์เช่นโค้ดตัวอย่าง สามารถละเว้นการเขียนโค้ดส่วนรับพารามิเตอร์ได้ นั่นคือไม่ต้องมีโค้ด ( ) ต่อจาก ^ ก็ทำงานได้เหมือนกัน การประกาศฟังก์ชันทุกครั้งต้องมีชื่อของฟังก์ชันเช่น mul และ add แต่การประกาศ block literal ไม่ต้องกำหนดชื่อให้กับบล็อกหรือเรียกว่า Anonymous Function หรือ lambda เราสามารถประกาศตัวแปรบล็อกได้เช่นเดียวกับฟังก์ชั่นพ้อยเตอร์ และต้องกำหนด return type และ parameter เช่นเดียวกัน

de_block

ดูจากโค้ดตัวอย่างจะเห็นว่าแทบจะเหมือนกับการประกาศฟังก์ชั่นพ้อยเตอร์ แค่เปลี่ยนจาก * มาใช้ ^ แทนเท่านั้นเอง หลังจากที่ได้ประกาศตัวแปรเรียบร้อยแล้ว ก็สามารถกำหนด block literal ได้ดังเช่นโค้ดต่อไปนี้

sq_block

ส่วนที่อยู่ในกรอบคือ block literal โดยรับพารามิเตอร์เป็น int และส่งค่าของผลคูณของ num กลับมา โค้ดของ block literal ไม่ต้องกำหนดชนิดของ return type เพราะบล็อกมีการทำงานแบบ dynamic ชนิดของค่าตอบกลับจะถูกกำหนดตอน runtime ซึ่งต่างกับฟังก์ชั่นที่เป็น static ต้องกำหนด return type ตั้งแต่แรก หากต้องการกำหนด block literal ตั้งแต่เริ่มต้นประกาศตัวแปรก็ทำได้เช่นกัน

เราจะเขียนโปรแกรมง่ายๆสักโปรแกรม โดยจะปรับโค้ดของโปรแกรม 15.1 จากที่ใช้ function pointer ให้เป็นใช้ block แทน ซึ่งมีโค้ดดังต่อไปนี้

Program 15.3
main.m

โปรแกรม 15.3 ได้ประกาศตัวแปร block พร้อมกับกำหนด block literal โดยมีการทำงานคือบวกค่าของพารามิเตอร์ จากนั้นก็เปลี่ยน block literal ใหม่ซึ่งให้ได้ค่าผลคูณกลับมา เมื่อให้โปรแกรม 15.3 ทำงานก็จะได้ผลลัพธ์เหมือนกันกับโปรแกรม 15.1
เมื่อพิจารณาจากโค้ดของโปรแกรมจะเห็นว่าบล็อกสามารถเขียนไว้ด้านใน scope ของฟังก์ชันอื่นได้ ในกรณีนี้คือ main function แต่ในขณะที่ฟังก์ชันไม่สามารถประกาศฟังก์ชันซ้อนเข้ามาแบบนี้ได้

Block’s variables and scope

จากปัญหา callback ที่ไม่สามารถส่งผ่านค่าจากภายนอก scope ได้ เราสามารถใช้บล็อกมาช่วยแก้ปัญหาได้ เพราะบล็อกสามารถใช้ตัวแปรนอก scope ของตัวเองนอกเหนือจากพารามิเตอร์ที่รับเข้ามาได้ การทำงานลักษณะแบบนี้ในภาษาอื่นอาจจะเรียกว่า closure หากเราเรียกใช้อ็อบเจ็กนอกบล็อก อ็อบเจ็กจะถูก retain โดยอัตโนมัติ และจะ release เมื่อจบการทำงาน แต่อย่างไรก็ตามตัวแปรนอกบล็อกจะไม่สามารถแก้ไขได้ เว้นเสียแต่ใช้คีย์เวิร์ด __block  นำหน้าเพื่อบอกให้รู้ว่านี่คือตัวแปรที่ใช้ร่วมกันระหว่างบล็อกและผู้เรียก เราจะเปลี่ยนโค้ดของโปรแกรม 15.2 เพื่อให้สามารถเปลี่ยนค่าที่ใช้คูณได้ดังนี้

Program 15.4
main.m

Program 15.4 Output

Macbook 100.00
Macbook Air 150.00
Macbook 200.00
Macbook Air 300.00

ฟังก์ชั่น updateArray เปลี่ยนจากฟังก์ชั่นพ้อยเตอร์ให้เป็นบล็อก ในส่วนของโปรแกรมก็ประกาศตัวแปร factor โดยมีคีย์เวิร์ด __block นำหน้าเพราะต้องการจะใช้ตัวแปรนี้ร่วมกันกับบล็อก และกำหนดค่าเริ่มต้นคือ 0.5 ในลำดับต่อมาประกาศตัวแปร newPrice ซึ่งเป็นบล็อกที่หาผลคูณระหว่าง num กับ factor เมื่อโปรแกรมทำงานค่าของ Macbook และ Macbook Air จึงเป็น 100 และ 150 ตามลำดับ จากนั้นในบรรทัดที่ 38 ได้ปรับค่า factor ใหม่เป็น 2.0 และเรียกฟังก์ชัน updateArray อีกครั้งค่าของ Macbook และ Macbook Air จึงได้เปลี่ยนเป็นกลับคืนเป็น 200 และ 300 นั่นเอง หากเอาคีร์เวิร์ด __block  ออกและลองให้โปรแกรมทำงานใหม่ ผลลัพธ์จะได้ต่างกันดังนี้

Program 15.4 Output ( without __block )

Macbook 100.00
Macbook Air 150.00
Macbook 50.00
Macbook Air 75.00

จะเห็นว่าบล็อกจะยังใช้ค่าเดิมที่ได้กำหนดมาตั้งแต่ประกาศบล็อกนั่นคือ 0.5

 

 

Using a Block Directly

บล็อกเหมาะกับการใช้เป็น callback เช่น โปรแกรมโหลดข้อมูลจากอินเทอร์เน็ตใช้เวลานาน ที่หน้าจอหลักของโปรแกรมอาจจะมีตัวเลขบอกว่าข้อมูลอ่านได้กี่เปอร์เซ็นแล้ว เมื่ออ่านจบก็ให้ซ่อนตัวเลขนี้ การที่จะให้ตัวเลขซ่อนตัวหลังจากทำงานเสร็จ อาจจะทำได้ด้วยการใช้ delegate และเขียนเมธอดตามที่ protocol ได้กำหนด ดังที่เราเคยเขียนกันไปแล้ว แต่การใช้บล็อกนั้นง่ายกว่า เพราะเราสามารถที่จะส่งบล็อกเพื่อเป็น callback ให้กับฟังก์ชันหรือเมธอดได้ตรงๆ โดยเขียน block literal แบบ inline และส่งผ่านเข้าไปเป็นพารามิเตอร์ เช่น

Program 15.5
main.m

Program 15.5 Output

Macbook 210.00
Macbook Air 310.00

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

literal

ฟังก์ชั่น updateArray รับพารามิเตอร์สองค่าเช่นเดิมโดยค่าแรกคือ list ส่วนพารามิเตอร์ที่สองคือโค้ดของ block literal ที่อยู่ในกรอบสีฟ้าทั้งหมด

 

Blocks as Arguments to Methods

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

การใช้บล็อกเป็นพารามิเตอร์ของเมธอด อาจจะดูผิดแปลกไปจากการใช้บล็อกเป็นพารามิเตอร์ของ function สักเล็กน้อย เพราะเอาชื่อของตัวแปรมาไว้ด้านหลังสุด
arg_met

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

typedef ที่เขียนไปหมายความว่าประกาศให้  PrintBlock เป็น type หรือชนิดของตัวแปรแบบใหม่ที่มีค่าเท่ากับการประกาศบล็อก void(^) (NSString* name, float price ) เมื่อใช้ type ใหม่ที่ได้ประกาศไปโค้ดก็ดูอ่านเข้าใจง่ายกว่า

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

หลังจากที่ได้เขียนเมธอดใหม่เพิ่มเข้าไปในคลาสเรียบร้อยแล้ว ในส่วนของ main program ก็เขียนโค้ดทดสอบการทำงานของเมธอด ดังนี้

Program 15.6
main.m

Program 15.6 Output

Product name: Macbook
Price 200.000000

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

Leave a Reply