Enumerating over managed object to-many relationships

  • Hi,

    After compiling my app on Leopard I now run into a number of
    "Collection was mutated while being enumerated" exceptions.
    The problem is that it's not obvious why/how I am editing the
    collection and neither how to solve it the best way.

    Take this example:

    // given a paper, get all its authors, iterate over them and delete
    the ones with just one paper
    NSSet *authors = [paper valueForKey: @"authors"];
    NSEnumerator *e = [authors objectEnumerator];
    Author *author;
    while(author = [e nextObject]){
      if([[author valueForKey: @"papers"]count] == 1){
      [author removePapersObject: paper];
      [context deleteObject: author];
      }
    }

    Do I indeed change the authors set ? Indirectly and ultimately yes,
    if I would ask for  [paper valueForKey: @"authors"] again I would get
    a different set, but does it refresh this everytime this set then?

    And how would I solve this? I'm not sure if I can just copy the set
    first (and if that helps). Or perhaps I should do [NSArray
    arrayWithSet: authors] ?
    I guess the best way would be to first store the authors that need to
    be deleted and only after the enumeration I should delete them all.

    Can someone clearly explains how the situation works, why the above
    is bad, and what the best way of doing this is (perhaps also showing
    the ObjC 2.0 way)?
    Thanks for the insights,
    Alex
  • > I guess the best way would be to first store the authors that need to
    > be deleted and only after the enumeration I should delete them all.

      Exactly. See also the other very recent threads on this subject in
    the list archives.

    --
    I.S.
  • > After compiling my app on Leopard I now run into a number of
    > "Collection was mutated while being enumerated" exceptions.
    > The problem is that it's not obvious why/how I am editing the
    > collection and neither how to solve it the best way.
    >
    > Take this example:
    >
    > // given a paper, get all its authors, iterate over them and delete
    > the ones with just one paper
    > NSSet *authors = [paper valueForKey: @"authors"];
    > NSEnumerator *e = [authors objectEnumerator];
    > Author *author;
    > while(author = [e nextObject]){
    > if([[author valueForKey: @"papers"]count] == 1){
    > [author removePapersObject: paper];
    > [context deleteObject: author];
    > }
    > }
    >
    > Do I indeed change the authors set ? Indirectly and ultimately yes,
    > if I would ask for  [paper valueForKey: @"authors"] again I would get
    > a different set, but does it refresh this everytime this set then?

    You are changing the collection you are iterating over during the loop.

    > Can someone clearly explains how the situation works, why the above
    > is bad, and what the best way of doing this is (perhaps also showing
    > the ObjC 2.0 way)?

    Here's how I would write that on 10.5

    // given a paper, get all its authors, iterate over them and delete
    the ones with just one paper
    NSSet *authors = [[paper authors] copy];
    for(Author* a in authors) {
      if ([[a papers] count] == 1) {
      [a removePapersObject: paper];
      [context deleteObject: a];
      }
    }
            [authors release];

    Although, depending on how you set up the delete rules in your model,
    you could let the framework do more of the work.  For example, authors
    might have a nullify delete rule to the papers, in which case you
    don't need to call -removePapersObject yourself.  Instead you can let
    if happen at the end of the event when the changes are coalesced and
    the deletions cascaded.  In that case, you wouldn't need to make a
    copy, since the side effects on the authors set happens outside the
    loop.

    - Ben
previous month november 2007 next month
MTWTFSS
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
Go to today