Doesn't -willChangeValueForKey: copy the value before I change it?

  • I've implemented a KVO-compliant NSMutableSet instance variable.  It
    has this little mutator:

    - (void)deleteSomeStuff {
        [self willChangeValueForKey:@"stuffs"] ;
    #ifdef DO_IT_MY_WAY
        [_stuffs removeObject:[_stuffs anyObject]] ;
    #else
        NSMutableSet* newStuffs = [_stuffs mutableCopy] ;
        [_stuffs release] ;
        [newStuffs removeObject:[newStuffs anyObject]] ;
        _stuffs = newStuffs ;
    #endif
        [self didChangeValueForKey:@"stuffs"];
    }

    Either way, the observer gets notified, but if I DO_IT_MY_WAY, the
    "old" value in the notification is wrong; it is the same as the "new"
    value.  My mental picture is that -willChangeValueForKey: is invoked
    to copy the "old" value immediately before a change.  But it looks
    like maybe I'm wrong.

    Can someone please explain, at a little higher level, what's wrong
    with my picture?

    Jerry

    ******** DEMO PROJECT CODE ********

    //#define DO_IT_MY_WAY

    #import <Foundation/Foundation.h>

    #pragma mark Observee Class

    @interface Observee : NSObject {
        NSMutableSet* _stuffs ;
    }

    @end

    @implementation Observee

    + (BOOL)automaticallyNotifiesObserversForKey: (NSString *)theKey {
        BOOL automatic;

        if ([theKey isEqualToString:@"stuffs"])
            automatic = NO;
        else
            automatic = [super
    automaticallyNotifiesObserversForKey:theKey];

        return automatic;
    }

    - (NSMutableSet *)stuffs {
        return _stuffs;
    }

    - (void)setStuffs:(NSMutableSet *)newStuffs {
        [newStuffs retain];
        [_stuffs release];
        [self willChangeValueForKey:@"stuffs"];
        _stuffs = newStuffs;
        [self didChangeValueForKey:@"stuffs"];
    }

    - (void)deleteSomeStuff {
        [self willChangeValueForKey:@"stuffs"] ;
    #ifdef DO_IT_MY_WAY
        [_stuffs removeObject:[_stuffs anyObject]] ;
    #else
        NSMutableSet* newStuffs = [_stuffs mutableCopy] ;
        [_stuffs release] ;
        [newStuffs removeObject:[newStuffs anyObject]] ;
        _stuffs = newStuffs ;
    #endif
        [self didChangeValueForKey:@"stuffs"];
    }

    @end

    #pragma mark Observer Class
    @interface Observer : NSObject {
    }

    @end

    @implementation Observer

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                              context:(void *)context {
        NSLog(@"Observer observed change:\n%@", change) ;
    }

    @end

    #pragma mark Main

    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

        Observer* observer = [[Observer alloc] init] ;
        Observee* observee = [[Observee alloc] init] ;

        [observee addObserver:observer
                      forKeyPath:@"stuffs"
                      options:(NSKeyValueObservingOptionOld |
    NSKeyValueObservingOptionNew)
                      context:NULL] ;

        [observee setStuffs:[NSMutableSet setWithObjects:@"stuff1",
    @"stuff2", @"stuff3", nil]] ;

        [observee deleteSomeStuff] ;

        [pool release] ;
        return 0;
    }

    ********** CONSOLE OUTPUT IF DO_IT_MY_WAY ************

    2007-12-14 17:33:17.606 KVODemo[20028:10b] Observer observed change:
    {
        kind = 1;
        new =    {(
            stuff1,
            stuff3,
            stuff2
        )};
        old = <null>;
    }
    2007-12-14 17:33:17.609 KVODemo[20028:10b] Observer observed change:
    {
        kind = 1;
        new =    {(
            stuff3,
            stuff2
        )};
        old =    {(
            stuff3,
            stuff2
        )};
    }

    ********** CONSOLE OUTPUT IF NOT DO_IT_MY_WAY ************

    2007-12-14 17:44:07.246 KVODemo[20093:10b] Observer observed change:
    {
        kind = 1;
        new =    {(
            stuff1,
            stuff3,
            stuff2
        )};
        old = <null>;
    }
    2007-12-14 17:44:07.249 KVODemo[20093:10b] Observer observed change:
    {
        kind = 1;
        new =    {(
            stuff3,
            stuff2
        )};
        old =    {(
            stuff1,
            stuff3,
            stuff2
        )};
    }
  • I believe -willChangeValueForKey: simply retains the result of -
    valueForKey: when there are observers requesting the old value.  If
    you're signaling a change using -willChangeValueForKey:/-
    didChangeValueForKey: then you should arrange that the object returned
    by -valueForKey: is either immutable or a copy of your internal
    mutable representation.

    Implementing the following KVC compliance methods will ensure that -
    valueForKey:@"stuffs" returns a set other than _stuffs

        - (void) countOfStuffs;
        - (NSEnumerator *) enumeratorOfStuffs;
        - (id) memberOfStuffs:(id)aStuff;

    You might also consider using the following more precise notification
    for removal...

        NSSet *removedObjects = [NSSet setWithObject:[_stuffs anyObject]];
        [self willChangeValueForKey:@"stuffs"
    withSetMutation:NSKeyValueMinusSetMutation usingObjects:removedObjects];
        [_stuffs minusSet:removedObjects];
        [self didChangeValueForKey:@"stuffs"
    withSetMutation:NSKeyValueMinusSetMutation usingObjects:removedObjects];

    Cheers,
    dave

    On 14-Dec-07, at 6:58 PM, Jerry Krinock wrote:

    > I've implemented a KVO-compliant NSMutableSet instance variable.  It
    > has this little mutator:
    >
    > - (void)deleteSomeStuff {
    > [self willChangeValueForKey:@"stuffs"] ;
    > #ifdef DO_IT_MY_WAY
    > [_stuffs removeObject:[_stuffs anyObject]] ;
    > #else
    > NSMutableSet* newStuffs = [_stuffs mutableCopy] ;
    > [_stuffs release] ;
    > [newStuffs removeObject:[newStuffs anyObject]] ;
    > _stuffs = newStuffs ;
    > #endif
    > [self didChangeValueForKey:@"stuffs"];
    > }
    >
    > Either way, the observer gets notified, but if I DO_IT_MY_WAY, the
    > "old" value in the notification is wrong; it is the same as the
    > "new" value.  My mental picture is that -willChangeValueForKey: is
    > invoked to copy the "old" value immediately before a change.  But it
    > looks like maybe I'm wrong.
    >
    > Can someone please explain, at a little higher level, what's wrong
    > with my picture?
    >
    > Jerry
    >
  • On Dec 14, 2007, at 5:58 PM, Jerry Krinock wrote:

    > I've implemented a KVO-compliant NSMutableSet instance variable.  It
    > has this little mutator:

    > Either way, the observer gets notified, but if I DO_IT_MY_WAY, the
    > "old" value in the notification is wrong; it is the same as the
    > "new" value.  My mental picture is that -willChangeValueForKey: is
    > invoked to copy the "old" value immediately before a change.  But it
    > looks like maybe I'm wrong.
    >
    > Can someone please explain, at a little higher level, what's wrong
    > with my picture?

    I think it's your expectation that the old value is being copied
    that's wrong.  Key-Value Observing needs to work with an instance of
    any subclass of NSObject, not just those that conform to <NSCopying>,
    so it can only really retain.

    Furthermore, whether you're implementing a to-many reference or
    containment relationship, you'll want to post the appropriate mutable-
    set KVO notifications rather than just generic KVO notifications.
    This will allow the notification mechanism to be much more efficient.
    You can also make this happen very easily by just implementing a
    fuller suite of the mutable-set KVC cover methods in your class, *and
    using them yourself*.  It's a code smell to have -
    {will,did}ChangeValueForKey: invocations in code that isn't a KVC
    accessor/mutator for the key in question.

    Thus your -deleteSomeStuff method should really look like this:

      - (void)removeOneArbitraryStuff {
          [self removeStuffsObject:[_stuffs anyObject]];
      }

    Your implementation of -removeStuffsObject: doesn't even need to
    manually post KVO notifications.  If you have automatic KVO
    notification posting enabled for the "stuffs" key in your class (it's
    on by default for NSObject subclasses, off by default for
    NSManagedObject subclasses) then the fact that -removeStuffsObject: is
    a mutable-set KVC mutator for the "stuffs" property will be sufficient
    to cause proper notifications to be posted.

      -- Chris
  • On 2007 Dec, 15, at 17:41, Chris Hanson wrote:

    > On Dec 14, 2007, at 5:58 PM, Jerry Krinock wrote:
    >
    >> I've implemented a KVO-compliant NSMutableSet instance variable.
    >> It has this little mutator:
    >
    >> Either way, the observer gets notified, but if I DO_IT_MY_WAY, the
    >> "old" value in the notification is wrong; it is the same as the
    >> "new" value.  My mental picture is that -willChangeValueForKey: is
    >> invoked to copy the "old" value immediately before a change.  But
    >> it looks like maybe I'm wrong.
    >>
    >> Can someone please explain, at a little higher level, what's wrong
    >> with my picture?
    >
    > I think it's your expectation that the old value is being copied
    > that's wrong.  Key-Value Observing needs to work with an instance of
    > any subclass of NSObject, not just those that conform to
    > <NSCopying>, so it can only really retain.

    Ah, very sensible explanation.

    It looks like I'd been reading too much of the deeper documentation
    which focuses on special applications that require things like manual
    KVO.  I believe that the other KVC compliance methods mentioned by
    Dave are more than I need (and also may be only applicable to
    NSManagedObjects).

    With these improvements, I get the more precise change info that Dave
    mentioned, kind=NSKeyValueChangeRemoval.

    Thanks, guys!

    Jerry

    ********* IMPROVED CODE ***************

    /* Improved code demonstrating automatic KVO by using
        mutators with KVC-standard method names.  */

    #import <Foundation/Foundation.h>

    #pragma mark Observee Class

    @interface Observee : NSObject {
        NSMutableSet* _stuffs ;
    }

    @end

    @implementation Observee

    - (NSMutableSet *)stuffs {
        return _stuffs;
    }

    // Mutators with names in standard KVC format that
    // automatically trigger KVO observations

    - (void)setStuffs:(NSMutableSet *)newStuffs {
        [newStuffs retain];
        [_stuffs release];
        _stuffs = newStuffs;
    }
    - (void)addStuffsObject:(id)newStuff {
        [[self stuffs] addObject:newStuff];
    }
    - (void)removeStuffsObject:(id)newStuff {
        [[self stuffs] removeObject:newStuff];
    }

    // Higher-level mutator
    // Note that it invokes a lower-level KVC-standard mutator

    - (void)deleteOneStuff {
        [self removeStuffsObject:[[self stuffs] anyObject]] ;
    }

    @end

    #pragma mark Observer Class

    @interface Observer : NSObject {
    }

    @end

    @implementation Observer

    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary *)change
                            context:(void *)context {
        NSLog(@"Observer observed change:\n%@", change) ;
    }

    @end

    #pragma mark Main

    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

        Observer* observer = [[Observer alloc] init] ;
        Observee* observee = [[Observee alloc] init] ;

        [observee addObserver:observer
                      forKeyPath:@"stuffs"
                      options:(NSKeyValueObservingOptionOld |
    NSKeyValueObservingOptionNew)
                      context:NULL] ;

        [observee setStuffs:[NSMutableSet setWithObjects:@"stuff1",
    @"stuff2", @"stuff3", nil]] ;

        [observee deleteOneStuff] ;

        [pool release] ;
        return 0;
    }

    ******* CONSOLE OUTPUT *********

    2007-12-15 19:22:02.317 KVODemo[24567:10b] Observer observed change:
    {
        kind = 1;
        new =    {(
            stuff1,
            stuff3,
            stuff2
        )};
        old = <null>;
    }
    2007-12-15 19:22:02.338 KVODemo[24567:10b] Observer observed change:
    {
        kind = 3;
        old =    {(
            stuff1
        )};
    }
previous month december 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
31            
Go to today