Cara menulis di Objective-C pada tahun 2018. Bagian 1

Sebagian besar proyek iOS sebagian atau seluruhnya beralih ke Swift. Swift adalah bahasa yang hebat, dan dengan itu terletak masa depan pengembangan iOS. Tetapi bahasa ini terkait erat dengan toolkit, dan ada kelemahan pada toolkit Swift.


Kompiler Swift masih memiliki bug yang menyebabkannya mogok atau menghasilkan kode yang salah. Swift tidak memiliki ABI yang stabil. Dan, yang sangat penting, proyek Swift telah berlangsung terlalu lama.


Dalam hal ini, proyek yang ada mungkin lebih menguntungkan untuk melanjutkan pengembangan pada Objective-C. Dan Objective-C tidak seperti dulu!


Dalam seri artikel ini, kami akan menunjukkan fitur yang berguna dan peningkatan Objective-C, yang dengannya menjadi jauh lebih menyenangkan untuk menulis kode. Setiap orang yang menulis di Objective-C akan menemukan sesuatu yang menarik untuk diri mereka sendiri.



let dan var


Objective-C tidak lagi perlu menentukan jenis variabel secara eksplisit: dalam Xcode 8, ekstensi bahasa __auto_type muncul, dan sebelum inferensi tipe Xcode 8 tersedia di Objective-C ++ (menggunakan kata kunci auto dengan munculnya C ++ 0X).


Pertama, let tambahkan makro let and var :


 #define let __auto_type const #define var __auto_type 

 //  NSArray<NSString *> *const items = [string componentsSeparatedByString:@","]; void(^const completion)(NSData * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable) = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // ... }; //  let items = [string componentsSeparatedByString:@","]; let completion = ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // ... }; 

Jika Anda biasa menulis const setelah pointer ke kelas Objective-C, itu adalah kemewahan yang tidak dapat diterima, tetapi sekarang deklarasi implisit const (via let ) telah diterima begitu saja. Perbedaannya terutama terlihat ketika menyimpan blok ke variabel.


Bagi kami sendiri, kami mengembangkan aturan untuk menggunakan let dan var untuk mendeklarasikan semua variabel. Bahkan ketika sebuah variabel diinisialisasi ke nil :


 - (nullable JMSomeResult *)doSomething { var result = (JMSomeResult *)nil; if (...) { result = ...; } return result; } 

Satu-satunya pengecualian adalah ketika Anda perlu memastikan bahwa suatu variabel diberi nilai di setiap cabang kode:


 NSString *value; if (...) { if (...) { value = ...; } else { value = ...; } } else { value = ...; } 

Hanya dengan cara ini kita akan mendapatkan peringatan kompiler jika kita lupa memberikan nilai ke salah satu cabang.


Dan akhirnya: untuk menggunakan let dan var untuk variabel tipe id , Anda perlu menonaktifkan peringatan auto-var-id (tambahkan -Wno-auto-var-id ke "Bendera Peringatan Lain" di pengaturan proyek).


Nilai pengembalian jenis blok otomatis


Hanya sedikit orang yang tahu bahwa kompiler dapat menyimpulkan tipe nilai pengembalian blok:


 let block = ^{ return @"abc"; }; // `block`   `NSString *(^const)(void)` 

Sangat nyaman. Terutama jika Anda menulis kode reaktif menggunakan ReactiveObjC . Tetapi ada sejumlah batasan di mana Anda harus secara eksplisit menentukan jenis nilai pengembalian.


  1. Jika ada beberapa return dalam blok yang mengembalikan nilai dari tipe yang berbeda.


     let block1 = ^NSUInteger(NSUInteger value){ if (value > 0) { return value; } else { // `NSNotFound`   `NSInteger` return NSNotFound; } }; let block2 = ^JMSomeBaseClass *(BOOL flag) { if (flag) { return [[JMSomeBaseClass alloc] init]; } else { // `JMSomeDerivedClass`   `JMSomeBaseClass` return [[JMSomeDerivedClass alloc] init]; } }; 

  2. Jika ada return di blok yang mengembalikan nil .


     let block1 = ^NSString * _Nullable(){ return nil; }; let block2 = ^NSString * _Nullable(BOOL flag) { if (flag) { return @"abc"; } else { return nil; } }; 

  3. Jika blok harus mengembalikan BOOL .


     let predicate = ^BOOL(NSInteger lhs, NSInteger rhs){ return lhs > rhs; }; 


Ekspresi dengan operator pembanding di C (dan karenanya di Objective-C) bertipe int . Oleh karena itu, lebih baik untuk menjadikannya aturan untuk selalu secara eksplisit menentukan BOOL jenis pengembalian.


Generik dan for...in


Dalam Xcode 7, generik (atau lebih tepatnya, generik ringan) muncul di Objective-C. Kami harap Anda sudah menggunakannya. Tetapi jika tidak, Anda dapat menonton sesi WWDC atau membacanya di sini atau di sini .


Bagi kami sendiri, kami telah mengembangkan aturan untuk selalu menentukan parameter umum, bahkan jika itu adalah id ( NSArray<id> * ). Dengan demikian, mudah untuk membedakan antara kode lama di mana parameter generik belum ditentukan.


Dengan makro let dan var , kami berharap bahwa kami dapat menggunakannya dalam for...in loop:


 let items = (NSArray<NSString *> *)@[@"a", @"b", @"c"]; for (let item in items) { NSLog(@"%@", item); } 

Tetapi kode seperti itu tidak dikompilasi. Kemungkinan besar, __auto_type tidak didukung for...in , karena for...in hanya bekerja dengan koleksi yang menerapkan protokol NSFastEnumeration . Dan untuk protokol di Objective-C tidak ada dukungan untuk obat generik.


Untuk memperbaiki kelemahan ini, cobalah membuat makro foreach Anda. Hal pertama yang terlintas dalam pikiran: semua koleksi di Foundation memiliki properti objectEnumerator , dan makro mungkin terlihat seperti ini:


 #define foreach(object_, collection_) \ for (typeof([(collection_).objectEnumerator nextObject]) object_ in (collection_)) 

Tetapi untuk NSDictionary dan NSMapTable metode protokol NSMapTable NSFastEnumeration atas kunci, bukan nilai-nilai (Anda perlu menggunakan keyEnumerator , bukan objectEnumerator ).


Kami perlu mendeklarasikan properti baru yang hanya akan digunakan untuk mendapatkan tipe dalam typeof ekspresi:


 @interface NSArray<__covariant ObjectType> (ForeachSupport) @property (nonatomic, strong, readonly) ObjectType jm_enumeratedType; @end @interface NSDictionary<__covariant KeyType, __covariant ObjectType> (ForeachSupport) @property (nonatomic, strong, readonly) KeyType jm_enumeratedType; @end #define foreach(object_, collection_) \ for (typeof((collection_).jm_enumeratedType) object_ in (collection_)) 

Sekarang kode kami terlihat jauh lebih baik:


 //  for (MyItemClass *item in items) { NSLog(@"%@", item); } //  foreach (item, items) { NSLog(@"%@", item); } 

Cuplikan untuk Xcode
 foreach (<#object#>, <#collection#>) { <#statements#> } 

Generik dan copy / mutableCopy


Tempat lain di mana pengetikan tidak tersedia di Objective-C adalah metode -mutableCopy dan -mutableCopy (serta -copyWithZone: dan -mutableCopyWithZone: metode, tetapi kami tidak memanggilnya secara langsung).


Untuk menghindari kebutuhan gips eksplisit, Anda bisa mendeklarasikan ulang metode dengan tipe kembali. Misalnya, untuk NSArray deklarasi akan menjadi:


 @interface NSArray<__covariant ObjectType> (TypedCopying) - (NSArray<ObjectType> *)copy; - (NSMutableArray<ObjectType> *)mutableCopy; @end 

 let items = [NSMutableArray<NSString *> array]; // ... //  let itemsCopy = (NSArray<NSString *> *)[items copy]; //  let itemsCopy = [items copy]; 

warn_unused_result


Karena kita telah mendeklarasikan ulang -mutableCopy dan -mutableCopy , akan lebih baik untuk menjamin bahwa hasil dari pemanggilan metode ini akan digunakan. Untuk melakukan ini, warn_unused_result atribut warn_unused_result .


 #define JM_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) 

 @interface NSArray<__covariant ObjectType> (TypedCopying) - (NSArray<ObjectType> *)copy JM_WARN_UNUSED_RESULT; - (NSMutableArray<ObjectType> *)mutableCopy JM_WARN_UNUSED_RESULT; @end 

Untuk kode berikut, kompiler akan menghasilkan peringatan:


 let items = @[@"a", @"b", @"c"]; [items mutableCopy]; // Warning: Ignoring return value of function declared with 'warn_unused_result' attribute. 

overloadable


Sedikit yang tahu bahwa Dentang memungkinkan Anda untuk mendefinisikan kembali fungsi dalam bahasa C (dan karenanya di Objective-C). Menggunakan atribut overloadable , Anda dapat membuat fungsi dengan nama yang sama, tetapi dengan berbagai jenis argumen atau dengan nomor yang berbeda.


Fungsi yang tidak dapat ditimpa tidak dapat berbeda hanya dalam jenis nilai pengembalian.


 #define JM_OVERLOADABLE __attribute__((overloadable)) 

 JM_OVERLOADABLE float JMCompare(float lhs, float rhs); JM_OVERLOADABLE float JMCompare(float lhs, float rhs, float accuracy); JM_OVERLOADABLE double JMCompare(double lhs, double rhs); JM_OVERLOADABLE double JMCompare(double lhs, double rhs, double accuracy); 

Ekspresi kotak


Kembali pada tahun 2012, pada sesi WWDC 413, Apple memperkenalkan literal untuk NSNumber , NSArray dan NSDictionary , serta ekspresi kotak. Anda dapat membaca lebih lanjut tentang literal dan ekspresi kotak dalam dokumentasi Dentang .


 //  @YES // [NSNumber numberWithBool:YES] @NO // [NSNumber numberWithBool:NO] @123 // [NSNumber numberWithInt:123] @3.14 // [NSNumber numberWithDouble:3.14] @[obj1, obj2] // [NSArray arrayWithObjects:obj1, obj2, nil] @{key1: obj1, key2: obj2} // [NSDictionary dictionaryWithObjectsAndKeys:obj1, key1, obj2, key2, nil] // Boxed expressions @(boolVariable) // [NSNumber numberWithBool:boolVariable] @(intVariable) // [NSNumber numberWithInt:intVariable)] 

Menggunakan literal dan ekspresi kotak, Anda bisa dengan mudah mendapatkan objek yang mewakili angka atau nilai Boolean. Tetapi untuk mendapatkan objek yang membungkus struktur, Anda perlu menulis beberapa kode:


 //  `NSDirectionalEdgeInsets`  `NSValue` let insets = (NSDirectionalEdgeInsets){ ... }; let value = [[NSValue alloc] initWithBytes:&insets objCType:@encode(typeof(insets))]; // ... //  `NSDirectionalEdgeInsets`  `NSValue` var insets = (NSDirectionalEdgeInsets){}; [value getValue:&insets]; 

Metode dan properti pembantu didefinisikan untuk beberapa kelas (seperti metode +[NSValue valueWithCGPoint:] dan properti CGPointValue ), tetapi ini masih tidak CGPointValue ekspresi kotak!


Dan pada 2015, Alex Denisov membuat tambalan untuk Dentang, memungkinkan Anda untuk menggunakan ekspresi kotak untuk membungkus struktur apa pun di NSValue .


Agar struktur kami mendukung ekspresi kotak, Anda hanya perlu menambahkan atribut objc_boxable untuk struktur.


 #define JM_BOXABLE __attribute__((objc_boxable)) 

 typedef struct JM_BOXABLE JMDimension { JMDimensionUnit unit; CGFloat value; } JMDimension; 

Dan kita dapat menggunakan sintaks @(...) untuk struktur kita:


 let dimension = (JMDimension){ ... }; let boxedValue = @(dimension); //   `NSValue *` 

Anda masih harus -[NSValue getValue:] struktur kembali melalui metode -[NSValue getValue:] atau metode kategori.


CoreGraphics mendefinisikan ekspresi makro, CG_BOXABLE , dan kemas sendiri sudah didukung untuk struktur CGPoint , CGSize , CGVector dan CGRect .


Untuk struktur lain yang umum digunakan, kami dapat menambahkan dukungan untuk ekspresi kotak sendiri:


 typedef struct JM_BOXABLE _NSRange NSRange; typedef struct JM_BOXABLE CGAffineTransform CGAffineTransform; typedef struct JM_BOXABLE UIEdgeInsets UIEdgeInsets; typedef struct JM_BOXABLE NSDirectionalEdgeInsets NSDirectionalEdgeInsets; typedef struct JM_BOXABLE UIOffset UIOffset; typedef struct JM_BOXABLE CATransform3D CATransform3D; 

Kata majemuk majemuk


Konstruksi bahasa lain yang bermanfaat adalah majemuk literal . Literal majemuk muncul di GCC sebagai ekstensi bahasa, dan kemudian ditambahkan ke standar C11.


Jika sebelumnya, setelah memenuhi panggilan ke UIEdgeInsetsMake , kami hanya bisa menebak lekukan apa yang akan kami dapatkan (kami harus menonton deklarasi fungsi UIEdgeInsetsMake ), kemudian dengan literal majemuk, kode tersebut berbicara sendiri:


 //  UIEdgeInsetsMake(1, 2, 3, 4) //  (UIEdgeInsets){ .top = 1, .left = 2, .bottom = 3, .right = 4 } 

Akan lebih nyaman menggunakan konstruksi semacam itu ketika beberapa bidang nol:


 (CGPoint){ .y = 10 } //  (CGPoint){ .x = 0, .y = 10 } (CGRect){ .size = { .width = 10, .height = 20 } } //  (CGRect){ .origin = { .x = 0, .y = 0 }, .size = { .width = 10, .height = 20 } } (UIEdgeInsets){ .top = 10, .bottom = 20 } //  (UIEdgeInsets){ .top = 20, .left = 0, .bottom = 10, .right = 0 } 

Tentu saja, dalam literal majemuk, Anda tidak hanya dapat menggunakan konstanta, tetapi juga ekspresi apa pun:


 textFrame = (CGRect){ .origin = { .y = CGRectGetMaxY(buttonFrame) + textMarginTop }, .size = textSize }; 

Cuplikan untuk Xcode
 (NSRange){ .location = <#location#>, .length = <#length#> } (CGPoint){ .x = <#x#>, .y = <#y#> } (CGSize){ .width = <#width#>, .height = <#height#> } (CGRect){ .origin = { .x = <#x#>, .y = <#y#> }, .size = { .width = <#width#>, .height = <#height#> } } (UIEdgeInsets){ .top = <#top#>, .left = <#left#>, .bottom = <#bottom#>, .right = <#right#> } (NSDirectionalEdgeInsets){ .top = <#top#>, .leading = <#leading#>, .bottom = <#bottom#>, .trailing = <#trailing#> } (UIOffset){ .horizontal = <#horizontal#>, .vertical = <#vertical#> } 

Nullability


Dalam Xcode 6.3.2, anotasi nullability muncul di Objective-C. Pengembang Apple menambahkannya untuk mengimpor API Objective-C ke Swift. Tetapi jika sesuatu ditambahkan ke bahasa, maka Anda harus mencoba memasukkannya ke dalam layanan Anda. Dan kami akan menjelaskan bagaimana kami menggunakan nullability dalam proyek Objective-C dan apa saja batasannya.


Untuk menyegarkan kembali pengetahuan Anda, Anda dapat menonton sesi WWDC .


Hal pertama yang kami lakukan adalah mulai menulis NS_ASSUME_NONNULL_END / NS_ASSUME_NONNULL_END di semua file .m . Agar tidak melakukan ini dengan tangan, kami tambalan templat file langsung di Xcode.


Kami juga mulai menetapkan nullability dengan benar untuk semua properti dan metode pribadi.


Jika kami menambahkan makro NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END ke file .m ada, kami segera menambahkan nullable , null_resettable , dan _Nullable di seluruh file.


Semua peringatan penyusun nullability yang berguna diaktifkan secara default. Tetapi ada satu peringatan ekstrem yang ingin saya sertakan: -Wnullable-to-nonnull-conversion (diatur dalam "Bendera Peringatan Lain" di pengaturan proyek). Compiler menghasilkan peringatan ini ketika variabel atau ekspresi dengan tipe nullable secara implisit dilemparkan ke tipe nonnull.


 + (NSString *)foo:(nullable NSString *)string { return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' } 

Sayangnya, untuk __auto_type (dan karena itu let dan var ), peringatan ini tidak berfungsi. Jenis yang disimpulkan melalui __auto_type membuang anotasi nullability. Dan, dilihat dari komentar pengembang Apple di rdar: // 27062504 , perilaku ini tidak akan berubah. Telah diamati secara eksperimental bahwa menambahkan _Nullable atau _Nonnull ke __auto_type tidak memengaruhi apa pun.


 - (NSString *)test:(nullable NSString *)string { let tmp = string; return tmp; //   } 

Untuk menekan nullable-to-nonnull-conversion kami menulis makro yang "memaksa membuka bungkus". Ide diambil dari RBBNotNil macro. Tetapi karena perilaku __auto_type dimungkinkan untuk menyingkirkan kelas bantu.


 #define JMNonnull(obj_) \ ({ \ NSCAssert(obj_, @"Expected `%@` not to be nil.", @#obj_); \ (typeof({ __auto_type result_ = (obj_); result_; }))(obj_); \ }) 

Contoh menggunakan makro JMNonnull :


 @interface JMRobot : NSObject @property (nonatomic, strong, nullable) JMLeg *leftLeg; @property (nonatomic, strong, nullable) JMLeg *rightLeg; @end @implementation JMRobot - (void)stepLeft { [self step:JMNonnull(self.leftLeg)] } - (void)stepRight { [self step:JMNonnull(self.rightLeg)] } - (void)step:(JMLeg *)leg { // ... } @end 

Perhatikan bahwa pada saat penulisan, peringatan nullable-to-nonnull-conversion tidak berfungsi dengan nullable-to-nonnull-conversion : kompiler belum memahami bahwa variabel nullable , setelah memeriksa ketidaksetaraan, nil dapat diartikan sebagai nonnull .


 - (NSString *)foo:(nullable NSString *)string { if (string != nil) { return string; // Implicit conversion from nullable pointer 'NSString * _Nullable' to non-nullable pointer type 'NSString * _Nonnull' } else { return @""; } } 

Dalam kode Objective-C ++, Anda bisa mengatasi batasan ini dengan menggunakan if let construct, karena Objective-C ++ memungkinkan deklarasi variabel dalam ekspresi if .


 - (NSString *)foo:(nullable NSString *)stringOrNil { if (let string = stringOrNil) { return string; } else { return @""; } } 

Tautan yang bermanfaat


Ada juga sejumlah macro dan kata kunci yang lebih terkenal yang ingin saya sebutkan: kata kunci @ tersedia Kami juga menyarankan Anda melihat perpustakaan libextobjc lainnya .



→ Kode untuk artikel diposting di intisari .


Kesimpulan


Pada bagian pertama artikel, kami mencoba untuk berbicara tentang fitur utama dan peningkatan bahasa sederhana, yang sangat memudahkan penulisan dan dukungan kode Objective-C. Pada bagian selanjutnya, kami akan menunjukkan bagaimana Anda dapat lebih meningkatkan produktivitas Anda menggunakan enum seperti pada Swift (mereka juga kelas kasus; mereka juga tipe data aljabar , ADT) dan kemungkinan menerapkan metode pada tingkat protokol.

Source: https://habr.com/ru/post/id431236/


All Articles