Objective-CブロックとC ++ラムダ

この投稿がC ++ラムダに精通しているが、Objective-Cブロックを学びたい人、またはその逆の人に役立つことを願っています。
ここでは、クロージャの構文、コンテキストをキャプチャするメカニズム、メモリ管理、ラムダとブロックの相互作用について説明しようとしました。
すべての例でApple LLVM Compiler 4.2(Clang)を使用しました。 ARCは、Obj-Cコードには使用されません。ARCがどのように機能するかを理解するために、非ARCコードがどのように機能するかを知っておく必要があるからです。

セクション:


  1. 構文
  2. コンテキストキャプチャ
  3. メモリ管理
  4. Objective-C ++

Objective-Cのブロックはクロージャー[2]の実装です。 ブロックは、コンテキスト(現在のスタック変数とクラスメンバー変数)をキャプチャできる匿名関数です。 実行時のブロックはオブジェクトによって表され、C ++のラムダ式の類似物です。

C ++のラムダもクロージャの実装であり、匿名のローカル関数を表します。

構文



Obj-Cブロック

[3]

テキスト形式で
int multiplier = 7; int (^myBlock)(int) = ^(int num) { return num * multiplier;}; NSLog(@”%d”, myBlock(4)); //  28 

  • ^-この記号は、変数がブロックであることをコンパイラーに伝えます
  • int-ブロックはint型のパラメーターを1つ受け入れ、int型のパラメーターを返します
  • 乗数-コンテキストから取得される変数(これについては、「コンテキストのキャプチャ」セクションを参照してください)


ブロックにはポインターセマンティクスがあります。
Objective-Cのブロックは、すでに標準フレームワーク(Foundation、UIKit)とサードパーティライブラリ( AFNetworkingBlocksKit )の両方でアプリケーションをしっかりと発見しています。
NSArrayクラスクラスの形式で例を示します

NSArrayのブロック使用例
 //   @implementation NSArray (Blocks) //   ,     - (NSArray*)subarrayWithPredicate:(BOOL(^)(id object, NSUInteger idx, BOOL *stop))predicte { NSMutableArray *resultArray = [NSMutableArray array]; BOOL shouldStop = NO; for (id object in self) { if (predicte(object, [self indexOfObjectIdenticalTo:object], &shouldStop)) { [resultArray addObject:object]; } if (shouldStop) { break; } } return [[resultArray copy] autorelease]; } @end // -    NSArray *numbers = @[@(5), @(3), @(8), @(9), @(2)]; NSUInteger divisor = 3; NSArray *divisibleArray = [numbers subarrayWithPredicate:^BOOL(id object, NSUInteger idx, BOOL *stop) { BOOL shouldAdd = NO; //     3 NSAssert([object isKindOfClass:[NSNumber class]], @"object != number"); //  ,   divisor     if ([(NSNumber *)object intValue] % divisor == 0) { shouldAdd = YES; } return shouldAdd; }]; NSLog(@"%@", divisibleArray); //  3, 9 


まず、これらは非同期操作に最適です。AFNetworkingを使用してこれを確認できます。GCDで作業するのは楽しいことです。
たとえば、ブロックタイプを定義できます。

ブロックタイプ宣言
 typedef int (^MyBlockType)(int number, id object); 


C ++ラムダ

同じラムダコード
[11]
テキスト形式で
 int multiplier = 7; auto lambda = [&multiplier](int num) throw() -> int { return multiplier * num; }; lambda(4); //  28 

  • [] -ラムダ宣言の開始、内部-コンテキストキャプチャ
  • &multiplier -キャプチャされた変数( &参照によりキャプチャされた手段)
  • int入力パラメーター
  • mutable値でキャプチャされた変数を変更できるキーワード
  • throw() -ラムダが外部に例外をスローしないことを意味します


ラムダのサブセットを選択する同様の例を示します

述語コレクションからサブセットを取得します
 template<class InputCollection, class UnaryPredicate> void subset(InputCollection& inputCollection, InputCollection& outputCollection, UnaryPredicate predicate) { typename InputCollection::iterator iterator = inputCollection.begin(); for (;iterator != inputCollection.end(); ++iterator) { if (predicate(*iterator)) { outputCollection.push_back(*iterator); } } return; } int main(int argc, const char * argv[]) { int divisor = 3; std::vector<int> inputVector = {5, 3, 8, 9, 2}; std::vector<int> outputVector; subset(inputVector, outputVector, [divisor](int number){return number % divisor == 0;}); //     std::for_each( outputVector.begin(), outputVector.end(), [](const int& number){std::cout << number << std::endl;} ); } 



コンテキストキャプチャ


Obj-Cブロック

変更しない場合、ブロック内のスタック変数の値を自動的に使用できます。 たとえば、上記の例では、 multiplier変数をキャプチャすることをブロック宣言で示していません(ラムダとは異なり、ラムダでは[&]を指定してリンクでコンテキスト全体をキャプチャしたり、 [=]コンテキスト全体をキャプチャしたりできます)値で)。
関数の本体で宣言された名前で値を取得しました。 ブロックの本体で値を変更する場合は、変数に__block修飾子を付ける必要があります

コンテキストから変数値を変更する例
 __block int first = 7; void (^myBlock2)(int) = ^(int second) { first += second;}; myBlock2(4); NSLog(@"%d", first); //  11 


ブロックに渡すポインタであるオブジェクトにメッセージを送信するために、ポインタを__blockとしてマークする必要はありません。 実際、オブジェクトにメッセージを送信するとき、そのポインターは変更しません。

コンテキストからオブジェクトにメッセージを送信する例
 NSMutableArray *array = [NSMutableArray array]; void (^myBlock3)() = ^() { [array addObject:@"someString"];}; myBlock3(); //  someString  array 


ただし、場合によっては、メモリリークを回避するために、 __blockオブジェクトへのポインタをマークする必要があります。 (詳細については、「メモリ管理」セクションを参照してください)

C ++ラムダ

キャプチャされた変数は、特定の場所[5] 、つまり角括弧[]内に示されます

ミューテーターは、コンテキストのキャプチャーに関連しています。
値によって渡される変数のコピーを変更できること。 詳細は次のセクションで。


メモリ管理


Obj-Cブロック

ブロックはオブジェクトであり、スタック上に作成されます(後でヒープに転送できます)
ブロックは、3つの実装[7]の形で存在できます。

  1. ブロック内のコンテキスト(スタック)の変数を使用しない場合、 NSGlobalBlockが作成され、シングルトンとして実装されます。
  2. コンテキスト変数を使用すると、 NSStackBlockが作成されます。これはもはやシングルトンではなく、スタック上にあります。
  3. Block_copy関数を使用する場合、またはオブジェクトプロパティとして、ヒープに配置されたオブジェクト内にブロックを保存する場合: @property (nonatomic, copy) MyBlockType myBlock; 次に、クラスNSMallocBlockオブジェクトNSMallocBlockます。これは、コンテキストで渡されたオブジェクトをキャプチャし、マスター(マスター==メッセージretain送信)します。 これは非常に重要なプロパティです。注意して処理すると、メモリリークが発生する可能性があるためです。 ブロックは保持サイクルを作成できます。 また、 NSMallocBlockpropertyの値を使用する場合、保持されるのはプロパティ自体ではなく、プロパティが属するオブジェクトであることに注意することも重要です。


所有権サイクルの例を次に示します。
API PKHTTPReuquest非同期API PKHTTPReuquest HTTPリクエストを行うクラスを作成したいとしましょう

PKHTTPReuquestの実装
 typedef void (^PKHTTPRequestCompletionSuccessBlock)(NSString *responseString); typedef void (^PKHTTPRequestCompletionFailBlock)(NSError* error); @protocol PKRequest <NSObject> - (void)startRequest; @end @interface PKHTTPRequest : NSObject <PKRequest> // designated initializer - (id)initWithURL:(NSURL *)url successBlock:(PKHTTPRequestCompletionSuccessBlock)success failBlock:(PKHTTPRequestCompletionFailBlock)fail; @end 

 @interface PKHTTPRequest () <NSURLConnectionDelegate> @property (nonatomic, copy) PKHTTPRequestCompletionSuccessBlock succesBlock; @property (nonatomic, copy) PKHTTPRequestCompletionFailBlock failBlock; @property (nonatomic, retain) NSURL *url; @property (nonatomic, retain) NSURLConnection *connection; @property (nonatomic, retain) NSMutableData *data; @end @implementation PKHTTPRequest #pragma mark - initialization / deallocation // designated initializer - (id)initWithURL:(NSURL *)url successBlock:(PKHTTPRequestCompletionSuccessBlock)success failBlock:(PKHTTPRequestCompletionFailBlock)fail { self = [super init]; if (self != nil) { self.succesBlock = success; self.failBlock = fail; self.url = url; NSURLRequest *request = [NSURLRequest requestWithURL:self.url]; self.connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO] autorelease]; } return self; } - (id)init { NSAssert(NO, @"Use desiganted initializer"); return nil; } - (void)dealloc { self.succesBlock = nil; self.failBlock = nil; self.url = nil; self.connection = nil; self.data = nil; [super dealloc]; } #pragma mark - public methods - (void)startRequest { self.data = [NSMutableData data]; [self.connection start]; } #pragma mark - NSURLConnectionDelegate implementation - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.data appendData:data]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { self.failBlock(error); self.data = nil; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { self.succesBlock([NSString stringWithUTF8String:self.data.bytes]); self.data = nil; } @end 



そして、 PKGetUserNameRequest動作するPKGetUserNameRequestサーバーの特定のAPIに対する特定のリクエストを作成しPKHTTPReuquest

PKGetUserNameRequestの実装
 typedef void (^PKGetUserNameRequestCompletionSuccessBlock)(NSString *userName); typedef void (^PKGetUserNameRequestCompletionFailBlock)(NSError* error); @interface PKGetUserNameRequest : NSObject <PKRequest> - (id)initWithUserID:(NSString *)userID successBlock:(PKGetUserNameRequestCompletionSuccessBlock)success failBlock:(PKGetUserNameRequestCompletionFailBlock)fail; @end 


 NSString *kApiHost = @"http://someApiHost.com"; NSString *kUserNameApiKey = @"username"; @interface PKGetUserNameRequest () @property (nonatomic, retain) PKHTTPRequest *httpRequest; - (NSString *)parseResponse:(NSString *)response; @end @implementation PKGetUserNameRequest #pragma mark - initialization / deallocation - (id)initWithUserID:(NSString *)userID successBlock:(PKGetUserNameRequestCompletionSuccessBlock)success failBlock:(PKGetUserNameRequestCompletionFailBlock)fail { self = [super init]; if (self != nil) { NSString *requestString = [kApiHost stringByAppendingFormat:@"?%@=%@", kUserNameApiKey, userID]; self.httpRequest = [[[PKHTTPRequest alloc] initWithURL:[NSURL URLWithString:requestString] successBlock:^(NSString *responseString) { //   -   self NSString *userName = [self parseResponse:responseString]; success(userName); } failBlock:^(NSError *error) { fail(error); } ] autorelease]; } return self; } - (id)init { NSAssert(NO, @"Use desiganted initializer"); return nil; } - (void)dealloc { self.httpRequest = nil; [super dealloc]; } #pragma mark - public methods - (void)startRequest { [self.httpRequest startRequest]; } #pragma mark - private methods - (NSString *)parseResponse:(NSString *)response { /* ...... */ return userName; } @end 



この行のエラーはNSString *userName = [self parseResponse:responseString]; -Mallocブロックのselfで何かを呼び出すと、self retuns、所有権グラフに次のループが形成されます。



これを回避するには、次のように、__ block修飾子を使用してスタック上のselfへの中間ポインターを作成します。

テニュアサイクルを破る例
 //    __block PKGetUserNameRequest *selfRequest = self; self.httpRequest = [[[PKHTTPRequest alloc] initWithURL:[NSURL URLWithString:requestString] successBlock:^(NSString *responseString) { NSString *userName = [selfRequest parseResponse:responseString]; success(userName); } failBlock:^(NSError *error) { fail(error); } ] autorelease]; 


または、初期化メソッドの署名からstartRequestメソッドにブロックを転送できます。
startRequestwithCompaltion:fail:リクエストの期間中のみブロックを保持します。 その後、 __block修飾子なしで実行できます。 これは別の問題を解決します:上記の例では、ブロックが弱い通信を提供するため、ブロックが呼び出されるまでに(要求が完了するまでに)、タイプPKGetUserNameRequestのオブジェクトが既に存在しなくなる(deallocメソッドが呼び出される)危険があります。 そして、 selfRequestselfRequestポインターでハングし、クラッシュを引き起こします。

ブロックを使用した誤ったメモリ管理の別の例、 ビデオ講義からの例[7]

NSStackBlockのエラー
 void addBlockToArray(NSMutableArray* array) { NSString *string = @"example string"; [array addObject:^{ printf("%@\n", string); }]; } void example() { NSMutableArray *array = [NSMutableArray array]; addBlockToArray(array); void (^block)() = [array objectAtIndex:0]; block(); } 


ブロックをヒープにコピーしてスタックを渡した場合、エラーは発生しませんでした。
また、この例ではARCコードにエラーは発生しません。

C ++ラムダ

ランタイムでのラムダの実装は、コンパイラによって異なる場合があります。 彼らは、ラムダのメモリ管理は標準ではあまり記述されていないと言います。 [9]
一般的な実装を検討してください。
C ++のラムダは、スタック上に作成される不明なタイプのオブジェクトです。
コンテキストをキャプチャしないラムダは関数ポインターにキャストできますが、それでもラムダ自体が関数へのポインターであることを意味するわけではありません。 ラムダは、スタック上で際立っているコンストラクタとデストラクタを持つ通常のオブジェクトです。

ヒープ内でラムダを移動する例を次に示します
ヒープ内でラムダを移動する例
 //  №1 auto lamb = []() {return 5;}; auto func_lamb_ptr = new std::function<int()>(lamb); //  №2: auto lamb = []() {return 5;}; auto* p = new decltype(lamb)(lamb); //  №3: template <typename T> T* heap_alloc(T const& value) { return new T(value); } auto* p = heap_alloc([]() {return 5;}); //  №4: std::vector<decltype(lamb)> v; v.push_back(lamb); 


これで、オブジェクトのメンバー変数に関数を渡すことができます。

ラムダ引数を宣言した後は可変であるため、値によってキャプチャされた変数のコピーの値を変更できることを意味します(元の変数の値は変更されません)。 たとえば、次のようにラムダを定義した場合: auto lambda = [multiplier](int num) throw() mutableラムダ内のmultiplier値を変更できますが、関数で宣言されたmultiplierは変更されていません。 さらに、変更されたmultiplier値は、このラムダインスタンスの呼び出しmultiplier保存されます。 このように想像できます。ラムダインスタンス(オブジェクト)で、渡されたパラメーターに対応する変数メンバーが作成されます。 ここで注意する必要があります。ラムダのインスタンスをコピーして呼び出すと、これらのメンバー変数は元のラムダでは変更されず、コピーされたものでのみ変更されるためです。 時々、 std::refラップされたラムダを渡す必要があります。 Obj-Cブロックは、すぐに使用できるような機会を提供しません。


Objective-C ++


Objecitve-C ++はObjective-CとC ++の両方を組み合わせているため、ラムダとブロックを同時に使用できます。 ラムダとブロックはどのように相互に関連していますか?

  1. ブロックにラムダを割り当てることができます。
      void (^block_example)(int); auto lambda_example = [](int number){number++; NSLog(@"%d", number);}; block_example = lambda_example; block_example(10); // log 11 


  2. ブロックをstd :: functionに割り当てることができます

    ここで、Objective-CとC ++には異なるメモリ管理ポリシーがあり、 std::functionブロックを格納するとリンクがダングリングstd::function可能性があることに注意してください。

  3. ラムダブロックを割り当てることはできません。

    Lambdaには、コピー割り当てステートメントが定義されていません。 したがって、ブロックまたは自分自身を割り当てることはできません。
    割り当てエラー
     int main() { auto lambda1 = []() -> void { printf("Lambda 1!\n"); }; lambda1 = lambda1; // error: use of deleted function 'main()::<lambda()>& main()::<lambda()>::operator=(const main()::<lambda()>&)' return 0; } 



ラムダとブロックの間の操作は非常にエキゾチックであると言わなければなりません。たとえば、プロジェクトでそのような割り当てを見たことはありません。

関連リンク


  1. C ++ Lambdasについて
  2. 短絡
  3. Apple Blocksについて
  4. 英語のラムダとブロックの比較
  5. ドックC ++ラムダ
  6. ブロックについて
  7. ブロックに関する素晴らしいビデオ
  8. stackoverflow.comでのC ++ラムダメモリの整理に関する質問
  9. ランタイムでのC ++ラムダの実装に関する質問
  10. ラムダとブロックの相互作用について
  11. ラムダ構文
  12. Objective-CとC ++の相互作用について
  13. Objective-CプロジェクトにC ++を埋め込む方法

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


All Articles