10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา iOS ไม่รู้ว่าพวกเขากำลังทำอยู่
เผยแพร่แล้ว: 2022-03-11อะไรจะแย่ไปกว่าการที่ App Store ปฏิเสธแอพบั๊กกี้ ก็ยอมรับได้. เมื่อบทวิจารณ์ระดับหนึ่งดาวเริ่มเข้ามา แทบจะเป็นไปไม่ได้เลยที่จะกู้คืน สิ่งนี้ทำให้บริษัทเสียเงินและพัฒนางานของพวกเขา
ปัจจุบัน iOS เป็นระบบปฏิบัติการมือถือที่ใหญ่เป็นอันดับสองของโลก นอกจากนี้ยังมีอัตราการนำไปใช้ที่สูงมาก โดยมีผู้ใช้มากกว่า 85% ในเวอร์ชันล่าสุด อย่างที่คุณคาดหวัง ผู้ใช้ที่มีส่วนร่วมสูงมีความคาดหวังสูง หากแอปหรือการอัปเดตของคุณไม่มีข้อบกพร่อง คุณจะได้ยินเกี่ยวกับมัน
ด้วยความต้องการนักพัฒนา iOS ที่พุ่งสูงขึ้นอย่างต่อเนื่อง วิศวกรหลายคนจึงเปลี่ยนมาใช้การพัฒนาอุปกรณ์พกพา (ส่งแอพใหม่มากกว่า 1,000 รายการไปยัง Apple ทุกวัน) แต่ความเชี่ยวชาญของ iOS ที่แท้จริงนั้นขยายไปไกลกว่าการเข้ารหัสพื้นฐาน ด้านล่างนี้คือข้อผิดพลาดทั่วไป 10 ข้อที่นักพัฒนา 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]
ด้วยความเข้าใจในสิ่งที่เกิดขึ้นภายใต้ประทุนนี้สามารถสรุปได้สองประการ:
- ถ้าแฮชของคีย์เปลี่ยนไป เรกคอร์ดควรถูกย้ายไปยังรายการที่เชื่อมโยงอื่น
- คีย์ควรไม่ซ้ำกัน
ลองตรวจสอบสิ่งนี้ในชั้นเรียนง่าย ๆ :
@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 อย่างไรก็ตาม มีข้อเสียมากมายและมีข้อดี (เป็นที่ถกเถียงกัน) เพียงเล็กน้อยในการใช้สตอรี่บอร์ด
ข้อเสียของสตอรี่บอร์ดรวมถึง:
- เป็นการยากที่จะแก้ไขกระดานเรื่องราวสำหรับสมาชิกในทีมหลายคน ในทางเทคนิค คุณสามารถใช้สตอรี่บอร์ดได้หลายแบบ แต่ข้อดีเพียงอย่างเดียวในกรณีนี้คือทำให้มีซีเควนซ์ระหว่างคอนโทรลเลอร์บนสตอรีบอร์ดได้
- ตัวควบคุมและชื่อภาคต่อจากสตอรีบอร์ดเป็นสตริง ดังนั้นคุณต้องป้อนสตริงเหล่านั้นใหม่ทั้งหมดตลอดทั้งโค้ดของคุณ (และวันหนึ่งคุณ จะ พัง) หรือรักษารายการค่าคงที่กระดานเรื่องราวจำนวนมาก คุณสามารถใช้ SBConstants ได้ แต่การเปลี่ยนชื่อบนกระดานเรื่องราวยังไม่ใช่เรื่องง่าย
- สตอรี่บอร์ดบังคับให้คุณออกแบบแบบไม่แยกส่วน ขณะทำงานกับสตอรีบอร์ด มีแรงจูงใจเพียงเล็กน้อยที่จะทำให้ความคิดเห็นของคุณนำกลับมาใช้ใหม่ได้ ซึ่งอาจเป็นที่ยอมรับได้สำหรับผลิตภัณฑ์ที่ใช้งานได้ขั้นต่ำ (MVP) หรือการสร้างต้นแบบ UI อย่างรวดเร็ว แต่ในแอปพลิเคชันจริง คุณอาจต้องใช้มุมมองเดียวกันหลายครั้งในแอปของคุณ
ข้อดี Storyboard (เป็นที่ถกเถียง):
- สามารถดูการนำทางของแอปทั้งหมดได้อย่างรวดเร็ว อย่างไรก็ตาม แอปพลิเคชันจริงสามารถมีตัวควบคุมมากกว่าสิบตัว ซึ่งเชื่อมต่อกันในทิศทางที่ต่างกัน สตอรีบอร์ดที่มีการเชื่อมต่อดังกล่าวดูเหมือนลูกบอลเส้นด้าย และไม่มีความเข้าใจในระดับสูงเกี่ยวกับกระแสข้อมูล
- ตารางคงที่ นี่เป็นข้อได้เปรียบที่แท้จริงเพียงอย่างเดียวที่ฉันนึกออก ปัญหาคือ 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: การใช้ค่าฮาร์ดโค้ด
มีปัญหาหลักสองประการเกี่ยวกับค่าฮาร์ดโค้ด:
- มักไม่ชัดเจนว่าพวกเขาเป็นตัวแทนของอะไร
- ต้องป้อนใหม่ (หรือคัดลอกและวาง) เมื่อจำเป็นต้องใช้ในหลายตำแหน่งในโค้ด
พิจารณาตัวอย่างต่อไปนี้:
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 ที่ยอดเยี่ยม และมอบโอกาสในการทำงานในโครงการที่ท้าทายโดยใช้เทคโนโลยีล้ำสมัย