2018年のObjective-Cの書き方。 パート1

ほとんどのiOSプロジェクトは、部分的または完全にSwiftに切り替わります。 Swiftは優れた言語であり、iOS開発の未来はそこにあります。 しかし、この言語はツールキットと密接にリンクされており、Swiftツールキットには欠陥があります。







Swiftコンパイラには、クラッシュまたは不正なコードの生成を引き起こすバグがまだあります。 Swiftには安定したABIがありません。 そして、非常に重要なことは、Swiftプロジェクトがあまりにも長く続いていることです。







この点で、Objective-Cでの開発を継続するには、既存のプロジェクトの方が収益性が高い場合があります。 そして、Objective-Cは以前のようにはなりませんでした!







この一連の記事では、Objective-Cの便利な機能と改善点を紹介します。Objective-Cを使用すると、コードの記述がより快適になります。 Objective-Cで書く人は誰でも、自分にとって興味深いものを見つけるでしょう。













let



var





Objective-Cは、変数タイプを明示的に指定する必要がなくなりました__auto_type



では、 __auto_type



言語拡張が登場し、Objective-C ++でXcode 8型推論が使用可能になりました(C ++ 0Xの出現でauto



キーワードを使用)。







まず、 let



マクロとvar



マクロを追加しlet









 #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) { // ... };
      
      





Objective-Cクラスへのポインタの後にconst



を記述する場合、これは受け入れがたい贅沢でしたが、 const



暗黙的な指定( let



を介した)は当たり前になっています。 ブロックを変数に保存する場合、違いは特に顕著です。







私たち自身のために、 let



var



を使用してすべての変数を宣言するルールを開発しました。 変数がnil



初期化される場合でも:







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





唯一の例外は、各コードブランチで変数に値が割り当てられていることを確認する必要がある場合です。







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





この方法でのみ、ブランチの1つに値を割り当てることを忘れた場合、コンパイラの警告が表示されます。







最後に、タイプid



変数にlet



およびvar



を使用するには、 auto-var-id



警告を無効にする必要があります(プロジェクト設定の「その他の警告フラグ」に-Wno-auto-var-id



を追加します)。







自動ブロックタイプの戻り値



コンパイラがブロックの戻り値の型を推測できることを知っている人はほとんどいません。







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





とても便利です。 特にReactiveObjCを使用してリアクティブコードを記述する場合。 ただし、戻り値の型を明示的に指定する必要があるいくつかの制限があります。







  1. ブロック内に異なるタイプの値をreturn



    が複数ある場合。







     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. ブロック内にnil



    を返すreturn



    がある場合。







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





  3. ブロックがBOOL



    を返すBOOL









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







Cで(したがってObjective-Cで)比較演算子を使用する式はint



型です。 したがって、戻り型BOOL



を常に明示的に指定するルールにすることをおBOOL



ます。







ジェネリックおよびfor...in





Objective-CのXcode 7では、ジェネリック(より正確には、軽量のジェネリック)が登場しました。 すでに使用されていることを願っています。 ただし、そうでない場合は、 WWDCセッションを見るか、 こちらまたはこちらで読むことできます







私たち自身のために、 id



NSArray<id> *



)であっても、常に汎用パラメーターを指定するルールを開発しました。 これにより、汎用パラメーターがまだ指定されていないレガシーコードを簡単に区別できます。







let



およびvar



マクロを使用すると、 for...in



ループで使用できることが期待さfor...in



ます。







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





しかし、そのようなコードはコンパイルされません。 ほとんどの場合、 __auto_type



for...in



でサポートされfor...in



いません__auto_type



for...in



NSFastEnumeration



プロトコルを実装するコレクションfor...in



のみ機能するためです。 また、Objective-Cのプロトコルでは、ジェネリックのサポートはありません。







この欠点を修正するには、 foreach



マクロを作成してみてください。 最初に思い浮かぶこと:FoundationのすべてのコレクションにはobjectEnumerator



プロパティがあり、マクロは次のようになります。







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





ただし、 NSDictionary



およびNSMapTable



プロトコルメソッドは、値ではなくキーをNSFastEnumeration



します( keyEnumerator



ではなくobjectEnumerator



を使用する必要があります)。







typeof



式で型を取得するためにのみ使用される新しいプロパティを宣言する必要があります。







 @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_))
      
      





今、私たちのコードはずっと良く見えます:







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





Xcodeのスニペット
 foreach (<#object#>, <#collection#>) { <#statements#> }
      
      





ジェネリックとcopy



/ mutableCopy





Objective-Cで入力が存在しないもう1つの場所は、 -mutableCopy



および-mutableCopy



-copyWithZone:



および-mutableCopyWithZone:



メソッドと同様ですが、直接呼び出しません)。







明示的なキャストの必要性を回避するために、戻り値の型でメソッドを再宣言できます。 たとえば、 NSArray



宣言は次のようになります。







 @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





-copyおよび-mutableCopy



を再宣言したため、これらのメソッドを呼び出した結果が使用されることを保証するのは良いことです。 これを行うために、Clangには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
      
      





次のコードの場合、コンパイラーは警告を生成します。







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





overloadable





Clangを使用してC言語(したがってObjective-C)で関数を再定義できることを知っている人はほとんどいません。 overloadable



属性を使用すると、同じ名前の関数を作成できますが、異なるタイプの引数または異なる番号を使用できます。







オーバーライド可能な関数は、戻り値のタイプのみを異にすることはできません。







 #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);
      
      





ボックス化された式



2012年のWWDC 413セッションで、 AppleはNSNumber



NSArray



NSDictionary



リテラルとボックス化された式を導入しました。 リテラルおよびボックス化された式の詳細は、 Clangのドキュメントに記載されています







 //  @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)]
      
      





リテラルとボックス化された式を使用すると、数値またはブール値を表すオブジェクトを簡単に取得できます。 しかし、構造をラップするオブジェクトを取得するには、いくつかのコードを記述する必要があります。







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





ヘルパーメソッドとプロパティは一部のクラス( +[NSValue valueWithCGPoint:]



メソッドやCGPointValue



プロパティなど)に対して定義されていますが、これはまだボックス化された式ほど便利ではありません!







そして2015年、 Alex Denisov Clangのパッチ作成し、ボックス化された式を使用してNSValue



構造をラップできるようにしNSValue









構造がボックス化された式をサポートするには、構造のobjc_boxable



属性を追加するだけです。







 #define JM_BOXABLE __attribute__((objc_boxable))
      
      





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





そして、構造に@(...)



構文を使用できます。







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





メソッド-[NSValue getValue:]



またはカテゴリメソッドを使用し-[NSValue getValue:]



構造を元に戻す必要があります。







CoreGraphicsは独自のマクロCG_BOXABLE



定義し、ボックス式はCGPoint



CGSize



CGVector



およびCGRect



に対して既にサポートされています。







他の一般的に使用される構造については、ボックス式のサポートを独自に追加できます。







 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;
      
      





複合リテラル



別の便利な言語構成体は、 複合リテラルです。 複合リテラルは、GCCで言語拡張機能として登場し、後にC11標準に追加されました。







以前にUIEdgeInsetsMake



呼び出しにUIEdgeInsetsMake



場合、どのインデントが得られるかを推測することしかできませんでした( UIEdgeInsetsMake



関数の宣言を監視する必要がありUIEdgeInsetsMake



)。







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





一部のフィールドがゼロの場合、このような構成を使用するとさらに便利です。







 (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 }
      
      





もちろん、複合リテラルでは、定数だけでなく任意の式も使用できます。







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





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#> }
      
      





ヌル可能性



Xcode 6.3.2では、Objective-Cにnullabilityアノテーションが登場しました。 Apple開発者は、Objective-C APIをSwiftにインポートするためにそれらを追加しました。 しかし、言語に何かが追加された場合、サービスにそれを置くようにしてください。 また、Objective-Cプロジェクトでnullabilityを使用する方法とその制限について説明します。







知識を更新するために、 WWDCセッションを見ることができます。







最初にしたことは、すべての.m



ファイルでNS_ASSUME_NONNULL_BEGIN



/ NS_ASSUME_NONNULL_END



書き込みを開始することでした。 これを手動で行わないために、ファイルテンプレートにXcodeで直接パッチを適用します。







また、すべてのプライベートプロパティとメソッドに対してnullabilityを設定し始めました。







NS_ASSUME_NONNULL_BEGIN



/ NS_ASSUME_NONNULL_END



マクロを既存の.m



ファイルに追加すると、ファイル全体に欠落しているnullable



null_resettable



、および_Nullable



がすぐに追加されます。







すべての有用なnullabilityコンパイラ警告はデフォルトで有効になっています。 しかし、私が含めたい極端な警告が1つあります。 -Wnullable-to-nonnull-conversion



(プロジェクト設定の「その他の警告フラグ」で設定)。 コンパイラーは、null許容型の変数または式が暗黙的に非null型にキャストされたときにこの警告を生成します。







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





残念ながら、 __auto_type



(およびlet



およびvar



)については、この警告は機能しません。 __auto_type



を介して推定される型では、 __auto_type



アノテーションは破棄されます。 そして、 rdar:// 27062504の Apple開発者のコ​​メントから判断すると、この動作は変わりません。 _Nullable



または_Nonnull



__auto_type



しても何にも影響しないことが実験的に確認されています。







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





nullable-to-nonnull-conversion



を抑制するために、「強制nullable-to-nonnull-conversion



解除」するマクロを作成しました。 RBBNotNil



マクロから取ったアイデア。 しかし、 __auto_type



の動作により、補助クラスを__auto_type



ができました。







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





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
      
      





執筆時点では、 nullable-to-nonnull-conversion



警告はnullable-to-nonnull-conversion



は機能しません。コンパイラは、不等式nil



チェックした後のnullable



変数が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 @""; } }
      
      





Objective-C ++コードでは、 if let



構文を使用してこの制限を回避できます。これは、Objective-C ++がif



で変数宣言を許可するためif









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





便利なリンク



私が言及したいより多くのよく知られているマクロとキーワードもあります: @available



キーワード、マクロNS_DESIGNATED_INITIALIZER



NS_UNAVAILABLE



NS_REQUIRES_SUPER



NS_NOESCAPE



NS_ENUM



NS_OPTIONS



(または同じ属性の独自のマクロ)およびライブラリ@keypathマクロ。 また、 libextobjcライブラリの残りを確認することをお勧めします。







他に読むもの

https://pspdfkit.com/blog/2017/even-swiftier-objective-c/

https://medium.com/@maicki/type-in​​ference-with-auto-type-55a38ef56372

https://nshipster.com/__attribute__/

https://www.bignerdranch.com/blog/bools-sharp-corners/







→記事のコードはgistに投稿されています。







おわりに



記事の最初の部分では、Objective-Cコードの作成とサポートを大幅に促進する、言語の主な機能と簡単な改善についてお話しました。 次の部分では、Swiftのように列挙型を使用して生産性をさらに向上させる方法(ケースクラス、 代数データ型 、ADT)およびプロトコルレベルでメソッドを実装する可能性を示します。








All Articles