10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา iOS ไม่รู้ว่าพวกเขากำลังทำอยู่

เผยแพร่แล้ว: 2022-03-11

อะไรจะแย่ไปกว่าการที่ App Store ปฏิเสธแอพบั๊กกี้ ก็ยอมรับได้. เมื่อบทวิจารณ์ระดับหนึ่งดาวเริ่มเข้ามา แทบจะเป็นไปไม่ได้เลยที่จะกู้คืน สิ่งนี้ทำให้บริษัทเสียเงินและพัฒนางานของพวกเขา

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

ด้วยความต้องการนักพัฒนา iOS ที่พุ่งสูงขึ้นอย่างต่อเนื่อง วิศวกรหลายคนจึงเปลี่ยนมาใช้การพัฒนาอุปกรณ์พกพา (ส่งแอพใหม่มากกว่า 1,000 รายการไปยัง Apple ทุกวัน) แต่ความเชี่ยวชาญของ iOS ที่แท้จริงนั้นขยายไปไกลกว่าการเข้ารหัสพื้นฐาน ด้านล่างนี้คือข้อผิดพลาดทั่วไป 10 ข้อที่นักพัฒนา iOS ตกเป็นเหยื่อ และคุณจะหลีกเลี่ยงได้อย่างไร

85% ของผู้ใช้ iOS ใช้ระบบปฏิบัติการเวอร์ชันล่าสุด ซึ่งหมายความว่าพวกเขาคาดหวังว่าแอปหรือการอัปเดตของคุณจะไร้ที่ติ
ทวีต

ข้อผิดพลาดทั่วไปหมายเลข 1: ไม่เข้าใจกระบวนการอะซิงโครนัส

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

 @property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 }]; [self.tableView reloadData]; // 2 } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }

เมื่อมองแวบแรก ทุกอย่างลงตัว: เราดึงข้อมูลจากเซิร์ฟเวอร์แล้วอัปเดต UI อย่างไรก็ตาม ปัญหาคือการดึงข้อมูลเป็นกระบวนการ แบบอะซิงโครนัส และจะไม่ส่งคืนข้อมูลใหม่ทันที ซึ่งหมายความว่าจะมีการเรียก reloadData ก่อนรับข้อมูลใหม่ เพื่อแก้ไขข้อผิดพลาดนี้ เราควรย้ายบรรทัด #2 ต่อจากบรรทัด #1 ภายในบล็อก

 @property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 [weakSelf.tableView reloadData]; // 2 }]; } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }

อย่างไรก็ตาม อาจมีบางสถานการณ์ที่โค้ดนี้ยังคงทำงานไม่เป็นไปตามที่คาดไว้ ซึ่งนำเราไปสู่ ​​...

ข้อผิดพลาดทั่วไปหมายเลข 2: การเรียกใช้รหัสที่เกี่ยวข้องกับ UI บนเธรดอื่นที่ไม่ใช่คิวหลัก

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

ไลบรารียอดนิยมส่วนใหญ่ เช่น Alamofire, AFNetworking และ Haneke ได้รับการออกแบบมาเพื่อเรียก completionBlock ในการบล็อกบนคิวหลักหลังจากทำงานแบบอะซิงโครนัส อย่างไรก็ตาม คุณไม่สามารถพึ่งพาสิ่งนี้ได้เสมอ และมันง่ายที่จะลืมส่งรหัสของคุณไปยังคิวที่ถูกต้อง

เพื่อให้แน่ใจว่ารหัสที่เกี่ยวข้องกับ UI ทั้งหมดของคุณอยู่ในคิวหลัก อย่าลืมส่งไปที่คิวนั้น:

 dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });

ข้อผิดพลาดทั่วไปหมายเลข 3: ความเข้าใจผิดเกี่ยวกับการทำงานพร้อมกันและมัลติเธรด

การทำงานพร้อมกันอาจเปรียบได้กับมีดที่คมจริงๆ: คุณสามารถกรีดตัวเองได้ง่ายๆ ถ้าคุณไม่ระวังหรือมีประสบการณ์เพียงพอ แต่จะมีประโยชน์และประสิทธิผลอย่างยิ่งเมื่อคุณรู้วิธีใช้อย่างถูกต้องและปลอดภัย

คุณสามารถพยายามหลีกเลี่ยงการใช้การทำงานพร้อมกันได้ แต่ไม่ว่าคุณจะสร้างแอปประเภทใด มีโอกาสสูงมากที่คุณไม่สามารถทำได้หากไม่มีแอปดังกล่าว การทำงานพร้อมกันอาจมีประโยชน์อย่างมากสำหรับการสมัครของคุณ สะดุดตา:

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

แต่ข้อดีของการทำงานพร้อมกันไม่ได้มาโดยปราศจากความซับซ้อนและมีศักยภาพในการทำให้เกิดข้อบกพร่องที่ร้ายแรง เช่น สภาพการแข่งขันที่ยากต่อการแพร่พันธุ์

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

กรณีที่ 1

 final class SpinLock { private var lock = OS_SPINLOCK_INIT func withLock<Return>(@noescape body: () -> Return) -> Return { OSSpinLockLock(&lock) defer { OSSpinLockUnlock(&lock) } return body() } } class ThreadSafeVar<Value> { private let lock: ReadWriteLock private var _value: Value var value: Value { get { return lock.withReadLock { return _value } } set { lock.withWriteLock { _value = newValue } } } }

รหัสมัลติเธรด:

 let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something }

เมื่อมองแวบแรก ทุกอย่างจะซิงค์และปรากฏราวกับว่าควรทำงานตามที่คาดไว้ เนื่องจาก ThreadSaveVar ล้อมตัว counter และทำให้เธรดปลอดภัย น่าเสียดายที่สิ่งนี้ไม่เป็นความจริง เนื่องจากสองเธรดอาจถึงบรรทัดที่เพิ่มขึ้นพร้อมกันและ counter.value == someValue จะไม่กลายเป็นจริงตามผลลัพธ์ เพื่อเป็นการแก้ปัญหาชั่วคราว เราสามารถสร้าง ThreadSafeCounter ซึ่งจะคืนค่าของมันหลังจากเพิ่มค่า:

 class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }

กรณีที่ 2

 struct SynchronizedDataArray { private let synchronizationQueue = dispatch_queue_create("queue_name", nil) private var _data = [DataType]() var data: [DataType] { var dataInternal = [DataType]() dispatch_sync(self.synchronizationQueue) { dataInternal = self._data } return dataInternal } mutating func append(item: DataType) { appendItems([item]) } mutating func appendItems(items: [DataType]) { dispatch_barrier_sync(synchronizationQueue) { self._data += items } } }

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

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

ข้อผิดพลาดทั่วไปหมายเลข 4: ไม่ทราบข้อผิดพลาดของวัตถุที่ไม่แน่นอน

Swift มีประโยชน์มากในการหลีกเลี่ยงข้อผิดพลาดเกี่ยวกับประเภทค่า แต่ยังมีนักพัฒนาจำนวนมากที่ใช้ Objective-C วัตถุที่เปลี่ยนแปลงได้นั้นอันตรายมากและอาจนำไปสู่ปัญหาที่ซ่อนอยู่ได้ เป็นกฎที่รู้จักกันดีว่าควรส่งคืนวัตถุที่ไม่เปลี่ยนรูปจากฟังก์ชัน แต่นักพัฒนาส่วนใหญ่ไม่รู้ว่าทำไม ลองพิจารณารหัสต่อไปนี้:

 // Box.h @interface Box: NSObject @property (nonatomic, readonly, strong) NSArray <Box *> *boxes; @end // Box.m @interface Box() @property (nonatomic, strong) NSMutableArray <Box *> *m_boxes; - (void)addBox:(Box *)box; @end @implementation Box - (instancetype)init { self = [super init]; if (self) { _m_boxes = [NSMutableArray array]; } return self; } - (void)addBox:(Box *)box { [self.m_boxes addObject:box]; } - (NSArray *)boxes { return self.m_boxes; } @end

โค้ดด้านบนนี้ถูกต้อง เนื่องจาก NSMutableArray เป็นคลาสย่อยของ NSArray ดังนั้นสิ่งที่สามารถผิดพลาดกับรหัสนี้?

สิ่งแรกและชัดเจนที่สุดคือนักพัฒนารายอื่นอาจเข้ามาทำสิ่งต่อไปนี้:

 NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }

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

อย่างไรก็ตาม กรณีนี้เลวร้ายกว่ามากและแสดงให้เห็นพฤติกรรมที่ไม่คาดคิด:

 Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes];

ความคาดหวังที่นี่คือ [newChildBoxes count] > [childBoxes count] แต่ถ้าไม่ใช่จะเป็นอย่างไร จากนั้นคลาสไม่ได้ออกแบบมาอย่างดีเพราะมันเปลี่ยนค่าที่ส่งคืนแล้ว หากคุณเชื่อว่าความไม่เท่าเทียมกันไม่ควรเป็นจริง ให้ลองทดลองกับ UIView และ [view subviews]

โชคดีที่เราสามารถแก้ไขโค้ดของเราได้โดยการเขียน getter ใหม่จากตัวอย่างแรก:

 - (NSArray *)boxes { return [self.m_boxes copy]; }

ข้อผิดพลาดทั่วไปหมายเลข 5: ไม่เข้าใจว่า iOS NSDictionary ทำงานอย่างไรภายใน

หากคุณเคยทำงานกับคลาสแบบกำหนดเองและ NSDictionary คุณอาจตระหนักว่าคุณไม่สามารถใช้คลาสของคุณได้หากคลาสนั้นไม่สอดคล้องกับ NSCopying เป็นคีย์พจนานุกรม นักพัฒนาส่วนใหญ่ไม่เคยถามตัวเองว่าทำไม Apple ถึงเพิ่มข้อจำกัดนั้น เหตุใด Apple จึงคัดลอกคีย์และใช้สำเนานั้นแทนวัตถุดั้งเดิม

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

ขั้นตอนที่ 1: คำนวณ hash(Key) ขั้นตอนที่ 2: ตามแฮช จะมองหาสถานที่สำหรับวางวัตถุ โดยปกติทำได้โดยใช้โมดูลัสของค่าแฮชกับความยาวของพจนานุกรม ดัชนีผลลัพธ์จะถูกใช้เพื่อเก็บคู่คีย์/ค่า ขั้นตอนที่ 3: หากไม่มีวัตถุอยู่ในตำแหน่งนั้น มันจะสร้างรายการที่เชื่อมโยงและจัดเก็บบันทึกของเรา (วัตถุและคีย์) มิฉะนั้น จะผนวกเรกคอร์ดต่อท้ายรายการ

ตอนนี้ มาอธิบายว่าบันทึกถูกดึงมาจากพจนานุกรมอย่างไร:

ขั้นตอนที่ 1: คำนวณ hash(Key) ขั้นตอนที่ 2: ค้นหาคีย์ด้วยแฮช หากไม่มีข้อมูล จะคืนค่า nil ขั้นตอนที่ 3: หากมีรายการที่เชื่อมโยง รายการจะวนซ้ำผ่าน Object จนกระทั่ง [storedkey isEqual:Key]

ด้วยความเข้าใจในสิ่งที่เกิดขึ้นภายใต้ประทุนนี้สามารถสรุปได้สองประการ:

  1. ถ้าแฮชของคีย์เปลี่ยนไป เรกคอร์ดควรถูกย้ายไปยังรายการที่เชื่อมโยงอื่น
  2. คีย์ควรไม่ซ้ำกัน

ลองตรวจสอบสิ่งนี้ในชั้นเรียนง่าย ๆ :

 @interface Person @property NSMutableString *name; @end @implementation Person - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[Person class]]) { return NO; } return [self.name isEqualToSting:((Person *)object).name]; } - (NSUInteger)hash { return [self.name hash]; } @end

ตอนนี้ลองนึกภาพ NSDictionary ไม่คัดลอกคีย์:

 NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;

โอ้! เรามีการพิมพ์ผิดที่นั่น! มาแก้ไขกันเถอะ!

 p.name = @"Jon Snow";

จะเกิดอะไรขึ้นกับพจนานุกรมของเรา? เมื่อชื่อเปลี่ยนไป ตอนนี้เรามีแฮชที่ต่างออกไป ตอนนี้วัตถุของเราอยู่ในตำแหน่งที่ไม่ถูกต้อง (ยังคงมีค่าแฮชแบบเก่าเนื่องจากพจนานุกรมไม่ทราบเกี่ยวกับการเปลี่ยนแปลงข้อมูล) และไม่ชัดเจนจริงๆว่าเราควรใช้แฮชใดเพื่อค้นหาข้อมูลในพจนานุกรม อาจมีกรณีที่เลวร้ายยิ่ง ลองนึกภาพถ้าเรามี “จอน สโนว์” ในพจนานุกรมของเราอยู่แล้วด้วยคะแนน 5 พจนานุกรมจะลงเอยด้วยค่าที่แตกต่างกันสองค่าสำหรับคีย์เดียวกัน

อย่างที่คุณเห็น มีปัญหามากมายที่อาจเกิดขึ้นจากการมีคีย์ที่ไม่แน่นอนใน NSDictionary แนวทางปฏิบัติที่ดีที่สุดเพื่อหลีกเลี่ยงปัญหาดังกล่าวคือการคัดลอกอ็อบเจ็กต์ก่อนจัดเก็บ และทำเครื่องหมายคุณสมบัติเป็น copy การปฏิบัตินี้จะช่วยให้คุณรักษาชั้นเรียนของคุณอย่างสม่ำเสมอ

ข้อผิดพลาดทั่วไปหมายเลข 6: การใช้สตอรี่บอร์ดแทน XIB

นักพัฒนา iOS ใหม่ส่วนใหญ่ทำตามคำแนะนำของ Apple และใช้กระดานเรื่องราว เป็นค่าเริ่มต้น สำหรับ UI อย่างไรก็ตาม มีข้อเสียมากมายและมีข้อดี (เป็นที่ถกเถียงกัน) เพียงเล็กน้อยในการใช้สตอรี่บอร์ด

ข้อเสียของสตอรี่บอร์ดรวมถึง:

  1. เป็นการยากที่จะแก้ไขกระดานเรื่องราวสำหรับสมาชิกในทีมหลายคน ในทางเทคนิค คุณสามารถใช้สตอรี่บอร์ดได้หลายแบบ แต่ข้อดีเพียงอย่างเดียวในกรณีนี้คือทำให้มีซีเควนซ์ระหว่างคอนโทรลเลอร์บนสตอรีบอร์ดได้
  2. ตัวควบคุมและชื่อภาคต่อจากสตอรีบอร์ดเป็นสตริง ดังนั้นคุณต้องป้อนสตริงเหล่านั้นใหม่ทั้งหมดตลอดทั้งโค้ดของคุณ (และวันหนึ่งคุณ จะ พัง) หรือรักษารายการค่าคงที่กระดานเรื่องราวจำนวนมาก คุณสามารถใช้ SBConstants ได้ แต่การเปลี่ยนชื่อบนกระดานเรื่องราวยังไม่ใช่เรื่องง่าย
  3. สตอรี่บอร์ดบังคับให้คุณออกแบบแบบไม่แยกส่วน ขณะทำงานกับสตอรีบอร์ด มีแรงจูงใจเพียงเล็กน้อยที่จะทำให้ความคิดเห็นของคุณนำกลับมาใช้ใหม่ได้ ซึ่งอาจเป็นที่ยอมรับได้สำหรับผลิตภัณฑ์ที่ใช้งานได้ขั้นต่ำ (MVP) หรือการสร้างต้นแบบ UI อย่างรวดเร็ว แต่ในแอปพลิเคชันจริง คุณอาจต้องใช้มุมมองเดียวกันหลายครั้งในแอปของคุณ

ข้อดี Storyboard (เป็นที่ถกเถียง):

  1. สามารถดูการนำทางของแอปทั้งหมดได้อย่างรวดเร็ว อย่างไรก็ตาม แอปพลิเคชันจริงสามารถมีตัวควบคุมมากกว่าสิบตัว ซึ่งเชื่อมต่อกันในทิศทางที่ต่างกัน สตอรีบอร์ดที่มีการเชื่อมต่อดังกล่าวดูเหมือนลูกบอลเส้นด้าย และไม่มีความเข้าใจในระดับสูงเกี่ยวกับกระแสข้อมูล
  2. ตารางคงที่ นี่เป็นข้อได้เปรียบที่แท้จริงเพียงอย่างเดียวที่ฉันนึกออก ปัญหาคือ 90 เปอร์เซ็นต์ของตารางสแตติกมีแนวโน้มที่จะเปลี่ยนเป็นตารางไดนามิกระหว่างการพัฒนาแอป และ XIB สามารถจัดการตารางไดนามิกได้ง่ายขึ้น

ข้อผิดพลาดทั่วไปหมายเลข 7: การเปรียบเทียบวัตถุและตัวชี้ที่สับสน

ในขณะที่เปรียบเทียบวัตถุสองชิ้น เราสามารถพิจารณาความเท่าเทียมกันสองอย่าง: ตัวชี้และความเท่าเทียมกันของวัตถุ

ความเท่าเทียมกันของตัวชี้เป็นสถานการณ์ที่ตัวชี้ทั้งสองชี้ไปที่วัตถุเดียวกัน ใน Objective-C เราใช้ตัวดำเนินการ == เพื่อเปรียบเทียบพอยน์เตอร์สองตัว ความเท่าเทียมกันของวัตถุเป็นสถานการณ์ที่วัตถุสองชิ้นเป็นตัวแทนของวัตถุสองชิ้นที่เหมือนกันทางตรรกะ เช่นเดียวกับผู้ใช้คนเดียวกันจากฐานข้อมูล ใน Objective-C เราใช้ isEqual หรือดีกว่านั้น พิมพ์เฉพาะ isEqualToString , isEqualToDate ฯลฯ ตัวดำเนินการเพื่อเปรียบเทียบสองอ็อบเจ็กต์

พิจารณารหัสต่อไปนี้:

 NSString *a = @"a"; // 1 NSString *b = @"a"; // 2 if (a == b) { // 3 NSLog(@"%@ is equal to %@", a, b); } else { NSLog(@"%@ is NOT equal to %@", a, b); }

อะไรจะพิมพ์ออกมาที่คอนโซลเมื่อเรารันโค้ดนั้น? เราจะได้ a is equal to b เนื่องจากทั้งวัตถุ a และ b ชี้ไปที่วัตถุเดียวกันในหน่วยความจำ

แต่ตอนนี้ขอเปลี่ยนบรรทัดที่ 2 เป็น:

 NSString *b = [[@"a" mutableCopy] copy];

ตอนนี้เราได้ a is NOT equal to b เนื่องจากตัวชี้เหล่านี้ชี้ไปที่วัตถุต่างๆ แม้ว่าวัตถุเหล่านั้นจะมีค่าเท่ากันก็ตาม

ปัญหานี้สามารถหลีกเลี่ยงได้โดยใช้ isEqual หรือพิมพ์ฟังก์ชันเฉพาะ ในตัวอย่างโค้ดของเรา เราควรแทนที่บรรทัดที่ 3 ด้วยโค้ดต่อไปนี้เพื่อให้ทำงานได้อย่างถูกต้องเสมอ:

 if ([a isEqual:b]) {

ข้อผิดพลาดทั่วไปหมายเลข 8: การใช้ค่าฮาร์ดโค้ด

มีปัญหาหลักสองประการเกี่ยวกับค่าฮาร์ดโค้ด:

  1. มักไม่ชัดเจนว่าพวกเขาเป็นตัวแทนของอะไร
  2. ต้องป้อนใหม่ (หรือคัดลอกและวาง) เมื่อจำเป็นต้องใช้ในหลายตำแหน่งในโค้ด

พิจารณาตัวอย่างต่อไปนี้:

 if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];

172800 หมายถึงอะไร? ทำไมมันถูกใช้? อาจไม่ชัดเจนว่าสิ่งนี้สอดคล้องกับจำนวนวินาทีใน 2 วัน (มี 24 x 60 x 60 หรือ 86,400 วินาทีในหนึ่งวัน)

แทนที่จะใช้ค่าแบบกำหนดค่าตายตัว คุณสามารถกำหนดค่าโดยใช้คำสั่ง #define ตัวอย่างเช่น:

 #define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

#define เป็นมาโครตัวประมวลผลล่วงหน้าที่แทนที่คำจำกัดความที่มีชื่อด้วยค่าในโค้ด ดังนั้น หากคุณมี #define ในไฟล์ส่วนหัวและนำเข้าจากที่ใดที่หนึ่ง ค่าที่กำหนดไว้ทั้งหมดในไฟล์นั้นจะถูกแทนที่ด้วย

ใช้งานได้ดี ยกเว้นประเด็นเดียว เพื่อแสดงปัญหาที่เหลือ ให้พิจารณารหัสต่อไปนี้:

 #define X = 3 ... CGFloat y = X / 2;

คุณคิดว่าค่าของ y จะเป็นอย่างไรหลังจากรันโค้ดนี้ ถ้าคุณบอกว่า 1.5 คุณคิดผิด y จะเท่ากับ 1 ( ไม่ใช่ 1.5) หลังจากรันโค้ดนี้ ทำไม? คำตอบคือ #define ไม่มีข้อมูลเกี่ยวกับประเภท ดังนั้น ในกรณีของเรา เรามีการแบ่งค่า Int สองค่า (3 และ 2) ซึ่งส่งผลให้ Int (เช่น 1) ถูกโยนไปยัง Float

สิ่งนี้สามารถหลีกเลี่ยงได้โดยใช้ค่าคงที่แทนซึ่งโดยนิยามแล้วให้พิมพ์:

 static const CGFloat X = 3; ... CGFloat y = X / 2; // y will now equal 1.5, as expected

ข้อผิดพลาดทั่วไปหมายเลข 9: การใช้คำหลักเริ่มต้นในคำสั่งสวิตช์

การใช้คีย์เวิร์ด default ในคำสั่ง switch อาจนำไปสู่จุดบกพร่องและการทำงานที่ไม่คาดคิด พิจารณารหัสต่อไปนี้ใน Objective-C:

 typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }

รหัสเดียวกันที่เขียนใน Swift:

 enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } }

รหัสนี้ทำงานตามที่ตั้งใจไว้ ทำให้เฉพาะผู้ใช้ที่เป็นผู้ดูแลระบบเท่านั้นที่สามารถเปลี่ยนระเบียนอื่นๆ ได้ อย่างไรก็ตาม อะไรจะเกิดขึ้นเราได้เพิ่มผู้ใช้ประเภทอื่น "ผู้จัดการ" ที่ควรจะสามารถแก้ไขบันทึกได้เช่นกัน หากเราลืมอัปเดตคำสั่ง switch นี้ โค้ดจะคอมไพล์ แต่จะไม่ทำงานตามที่คาดไว้ อย่างไรก็ตาม หากนักพัฒนาใช้ค่า enum แทนคีย์เวิร์ดเริ่มต้นตั้งแต่เริ่มต้น การกำกับดูแลจะถูกระบุในเวลารวบรวม และสามารถแก้ไขได้ก่อนที่จะทำการทดสอบหรือใช้งานจริง นี่เป็นวิธีที่ดีในการจัดการกับสิ่งนี้ใน Objective-C:

 typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular, UserTypeManager }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: case UserTypeManager: return YES; case UserTypeRegular: return NO; } }

รหัสเดียวกันที่เขียนใน Swift:

 enum UserType { case Admin, Regular, Manager } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Manager: fallthrough case .Admin: return true case .Regular: return false } }

ข้อผิดพลาดทั่วไปหมายเลข 10: การใช้ NSLog สำหรับการบันทึก

นักพัฒนา iOS หลายคนใช้ NSLog ในแอปเพื่อบันทึก แต่ส่วนใหญ่แล้วนี่เป็นข้อผิดพลาดร้ายแรง หากเราตรวจสอบเอกสารประกอบของ Apple สำหรับคำอธิบายฟังก์ชัน NSLog เราจะเห็นว่าง่ายมาก:

 void NSLog(NSString *format, ...);

อะไรที่อาจผิดพลาดกับมัน? ในความเป็นจริงไม่มีอะไร อย่างไรก็ตาม หากคุณเชื่อมต่ออุปกรณ์กับตัวจัดระเบียบ Xcode คุณจะเห็นข้อความแก้ไขข้อบกพร่องทั้งหมดที่นั่น ด้วยเหตุผลนี้เพียงอย่างเดียว คุณ ไม่ ควรใช้ NSLog ในการบันทึก: ง่ายต่อการแสดงข้อมูลภายในที่ไม่ต้องการและดูเหมือนไม่เป็นมืออาชีพ

วิธีที่ดีกว่าคือการแทนที่ NSLogs ด้วย CocoaLumberjack ที่กำหนดค่าได้หรือเฟรมเวิร์กการบันทึกอื่นๆ

สรุป

iOS เป็นแพลตฟอร์มที่ทรงพลังและมีการพัฒนาอย่างรวดเร็ว Apple พยายามอย่างหนักอย่างต่อเนื่องเพื่อแนะนำฮาร์ดแวร์และคุณสมบัติใหม่ๆ สำหรับ iOS ในขณะที่ยังขยายภาษา Swift อย่างต่อเนื่อง

การพัฒนาทักษะ Objective-C และ Swift จะทำให้คุณเป็นนักพัฒนา iOS ที่ยอดเยี่ยม และมอบโอกาสในการทำงานในโครงการที่ท้าทายโดยใช้เทคโนโลยีล้ำสมัย

ที่เกี่ยวข้อง: คู่มือนักพัฒนา iOS: จาก Objective-C ถึงการเรียนรู้ Swift