NSMutableSetを使用しているときのエラー

私はエラーを得る

* Terminating app due to uncaught exception 'NSGenericException', reason: '* Collection <__NSCFSet: 0x6b66390> was mutated while being enumerated.'

私のクラスに新しいデリゲートを追加するとき。少なくとも、それは私が問題だと思うところです。

これは私のコードです:MyAppAPI.m

[...]
static NSMutableSet *_delegates = nil;

@implementation MyAppAPI

+ (void)initialize
{
    if (self == [MyAppAPI class]) {
        _delegates = [[NSMutableSet alloc] init];
    }
}

+ (void)addDelegate:(id)delegate
{
    [_delegates addObject:delegate];
}

+ (void)removeDelegate:(id)delegate
{
    [_delegates removeObject:delegate];
}
[...]

@end

MyAppAPI is a singleton which I can use throughout my application. Wherever I can (or should be able to) do: [MyAppAPI addDelegate:self].
This works great, but only in the first view. This view has a UIScrollView with PageViewController which loads new views within itself. These new views register to MyAppAPI to listen to messages until they are unloaded (which in that case they do a removeDelegate). However, it seems to me that it dies directly after I did a addDelegate on the second view in the UIScrollView.

How could I improve the code so that this doesn't happen?

Update
I'd like to clarify me a bit further. What happens is that view controller "StartPage" has an UIScrollView with a page controller. It loads several other views (1 ahead of the current visible screen). Each view is an instans PageViewController, which registers itself using the addDelegate function shown above to the global singleton called MyAppAPI. However, as I understand this viewcontroller 1 is still reading from the delegate when viewcontroller 2 registers itself, hence the error shows above.

シナリオを明確にすることを願っています。私はいくつかのことを試しましたが、何も助けません。 デリゲートからの読み込み中でもaddDelegateを使ってデリゲートに登録する必要があります。それ、どうやったら出来るの?

Update 2 This is one of the reponder methods:

+ (void)didRecieveFeaturedItems:(NSArray*)items
{   
    for (id delegate in _delegates)
    {
        if ([delegate respondsToSelector:@selector(didRecieveFeaturedItems:)])
            [delegate didRecieveFeaturedItems:items];
    }
}
1
@frowing私は私だと思う
追加された 著者 Paul Peelen,
@NSResponder私は、ASIHttpRequestが何をしたかを見て、いくつかのリスナーを持つデリゲートクラスを作成する方法を探りました。これは終わりの結果でした。
追加された 著者 Paul Peelen,
はい、私は問題に賛成です。しかし、どのように私はそれを解決するのですか?私は@ AndrewZimmerが説明したように同期しようとしましたが、それは助けになりませんでした。
追加された 著者 Paul Peelen,
あなたの問題は、 _delegates オブジェクトを変更していることがほぼ確実です。 [delegate didReceiveFeaturedItems]
追加された 著者 Hot Licks,
通知パターンを使用する代わりに、複数の代理人がいるのはなぜですか?
追加された 著者 NSResponder,
新しいデリゲートを追加しようとすると、セットを反復処理していますか?
追加された 著者 Fran Sevillano,

6 答え

スコットハンターが正しいです。このエラーは、反復処理中にリストを編集しようとするとスローされます。

ここにあなたがしていることの例があります。

+ (void)iteratingToRemove:(NSArray*)items {   
    for (id delegate in _delegates) {
        if(delegate.removeMePlease) {
          [MyAppAPI removeDelegate:delegate];  //error you are editing an NSSet while enumerating
        }
    }
}

これを正しく処理する方法は次のとおりです。

+ (void)iteratingToRemove:(NSArray*)items
{   
    NSMutableArray *delegatesToRemove = [[NSMutableArray alloc] init];
    for (id delegate in _delegates) {
        if(delegate.removeMePlease) {
          [delegatesToRemove addObject:delegate];
        }
    }

    for(id delegate in delegatesToRemove) {
         [MyAppAPI removeDelegate:delegate];  //This works better
    }

    [delegatesToRemove release];
}
11
追加された
これで十分です
追加された 著者 vodkhang,
OK。あなたは私がremoveDelegateをそれに取って代わらなければならないということを意味し、デリゲートを削除するようクラスに指示するのではなく、デリゲートクラス自体の removeMePlease という変数をtrueに設定します。私は正しい?
追加された 著者 Paul Peelen,
この例の重要な部分は、配列を編集するときはいつでも、列挙中にそれをしないことです。編集したい項目を一時的な配列に格納することで、列挙してから編集することができます。最初のコードブロックと2番目のコードブロックの違いがあることに注意してください。
追加された 著者 Andrew Zimmer,

このエラーは、あなたのリストの途中にあるコードがある間に、リストを変更していることを示唆しています(addDelegateが呼び出された後のクラッシュについて説明しています)。列挙するコードがリストを変更するコードの場合は、列挙が完了するまで(たとえば、別のリストでそれらを収集することによって)変更を延期するだけです。列挙しているコードについて何も知らずに、それ以上のことは言えません。

5
追加された
列挙が完了するまでどのように修正を延期するのですか?呼び出し元オブジェクトは、デリゲートに登録または登録解除するときに、代理人が何か起こっているかどうかわからないため、何らかの方法がありますか? (私のコメントが不明確でないことを願っています);)
追加された 著者 Paul Peelen,
?私のコメントに何か提案がありますか?
追加された 著者 Paul Peelen,

簡単な解決策は、変更可能なセットを使用しないでください。彼らはこれを含むさまざまな理由で危険です。

-copyと-mutableCopyを使用すると、NSSet(および他の多くのクラス)の可変と非可変のバージョン間で変換できます。すべてのコピーメソッドが(allocのように)1の保持カウントを持つ新しいオブジェクトを返すので、解放する必要があることに注意してください。

バグの可能性が低いことを除けば、変更できないオブジェクトは処理が速く、メモリを少なくします。

[...]
static NSSet *_delegates = nil;

@implementation MyAppAPI

+ (void)initialize
{
    if (self == [MyAppAPI class]) {
        _delegates = [[NSSet alloc] init];
    }
}

+ (void)addDelegate:(id)delegate
{
    NSMutableSet *delegatesMutable = [_delegates mutableCopy];
    [delegatesMutable addObject:delegate];

    [_delegates autorelease];
    _delegates = [delegatesMutable copy];

    [delegatesMutable release];
}

+ (void)removeDelegate:(id)delegate
{
    NSMutableSet *delegatesMutable = [_delegates mutableCopy];
    [delegatesMutable removeObject:delegate];

    [_delegates autorelease];
    _delegates = [delegatesMutable copy];

    [delegatesMutable release];
}
[...]

@end
2
追加された
_delegates の値はNSSetのオブジェクトであるため、変更することはできませんが、内部で使用する前にポインタを取得(および保持)あなたのスレッド。
追加された 著者 Abhi Beckert,
これが別の良い方法かもしれないとは考えていませんでした!
追加された 著者 Ved,
私はこのアプローチの問題を考えました。スレッドセーフではなく、2つのスレッドが同時に試行して追加すると、一貫性のない結果につながる可能性があります。コピーを反復することは、(ロックを持たない)唯一の解決策です。
追加された 著者 Ved,

スコットハンターが正しいです - セットのアイテムを列挙している間にNSSetを変更することは問題です。アプリケーションがクラッシュする場所からスタックトレースを取得する必要があります。おそらく、_delegatesセットに追加/削除する行があります。これは、変更する必要がある場所です。それは簡単です。セットに追加/削除するのではなく、次の操作を行います。

NSMutableSet *tempSet = [_delegates copy];
for (id delegate in _delegates)
{
    //add or remove from tempSet instead
}
[_delegates release], _delegates = tempSet;

さらに、 NSMutableSetはスレッドセーフではありませんので、メインスレッドから常にメソッドを呼び出す必要があります。追加のスレッドを明示的に追加していない場合は、何も心配はありません。

1
追加された

Objective-Cの「高速列挙」について常に覚えておいてください。
"高速列挙"とforループの2つの大きな違いがあります。

"高速列挙"はforループより速いです。
しかし
列挙しているコレクションを変更することはできません。

NSSetに - (NSArray *)allObjects を依頼し、NSSetを変更している間にその配列を列挙できます。

0
追加された

他のスレッドがそのスレッドを反復している間にスレッドが配列を変更(追加、削除)しようとすると、このエラーが発生します。

NSLockを使用してこれを解決するか、またはメソッドを同期させる1つの方法。その方法では、メソッドの追加、削除、反復を並行して呼び出すことはできません。 しかし、これはパフォーマンス上および/または応答性に影響します。なぜなら、追加/削除は配列を反復していたスレッドを待たなければならないからです。

JavaのCopyOnWriteArrayListに触発されたより良い解決策は、配列のコピーを作成し、そのコピーを反復することです。だからあなたのコードの唯一の変更は:

//better solution
+ (void)didRecieveFeaturedItems:(NSArray*)items
{   
    NSArray *copyOfDelegates = [_delegates copy]
    for (id delegate in copyOfDelegates)
    {
        if ([delegate respondsToSelector:@selector(didRecieveFeaturedItems:)])
            [delegate didRecieveFeaturedItems:items];
    }
}

-装置following following- 13----- 13- following

//not a good solution

+ (void)addDelegate:(id)delegate
{
    @synchronized(self){
        [_delegates addObject:delegate];
    }
}

+ (void)removeDelegate:(id)delegate
{
    @synchronized(self){
        [_delegates removeObject:delegate];
   }
}

+ (void)didRecieveFeaturedItems:(NSArray*)items
{   
    @synchronized(self){
        for (id delegate in _delegates)
        {
            if ([delegate respondsToSelector:@selector(didRecieveFeaturedItems:)])
                [delegate didRecieveFeaturedItems:items];
        }
    }
}
0
追加された