Limited-access, KVO-compliant mutable array?

  • I feel certain people have run into this before, but my Googling didn't turn up exactly what I was looking for.

    I'd like to create a "mutable read-only" array property. I want clients of the class to be able to observe changes to the *contents* of the array (that is, if an element is added or removed), in addition to changes of the entire array. But I don't want those clients to be able to add or remove anything to/from the array themselves; I only want the class on which the property is defined to be able to do that.

    Is this possible? My first thought is to make a read-only NSArray* property, but then it seems that addition or removal of elements to/from that property wouldn't get observed; i.e., an NSArray should never change its contents.

    What's the right way to do this? Thanks!
    --
    Rick
  • That's exactly what you do.

    Declare the property as NSArray, that's what your clients see. In most cases you actually use an NSMutableArray as the backing variable so you can easily mutate it, you can either return that mutable array as the value of your property, or you can return a non-mutable copy of it. I normally return the actual array as the property is declared non-mutable so code that tries to mutate it produces lots of errors and warnings. If you're paranoid, copy it before return.

    Then, in the class which owns it, either implement the KVC indexed accessor methods (like insertObject:in<Key>AtIndex:) and call those, or use [ self mutableArrayValueForKey:<Key> ] to get a mutable array which also generates the correct KVC calls. Any observer of the property will get the correct KVO calls.

    On Jul 11, 2012, at 2:17 PM, Rick Mann wrote:

    > I feel certain people have run into this before, but my Googling didn't turn up exactly what I was looking for.
    >
    > I'd like to create a "mutable read-only" array property. I want clients of the class to be able to observe changes to the *contents* of the array (that is, if an element is added or removed), in addition to changes of the entire array. But I don't want those clients to be able to add or remove anything to/from the array themselves; I only want the class on which the property is defined to be able to do that.
    >
    > Is this possible? My first thought is to make a read-only NSArray* property, but then it seems that addition or removal of elements to/from that property wouldn't get observed; i.e., an NSArray should never change its contents.
    >
    > What's the right way to do this? Thanks!
    > --
    > Rick
  • On Jul 10, 2012, at 23:17 , Rick Mann wrote:

    > Is this possible? My first thought is to make a read-only NSArray* property, but then it seems that addition or removal of elements to/from that property wouldn't get observed; i.e., an NSArray should never change its contents.

    No, your first thought was correct.

    It's not really an "array" property, it's an indexed to-many property**. As far as the client is concerned the value returned by the property could be (and sometimes is***) a proxy that doesn't permit the underlying NSMutableArray ivar (assuming there is on) to be mutated. Property type NSArray* is just an abstract behavior of the returned object. (Remember that NSArray isn't a concrete class.)

    All that's required is that the indexed to-many property be mutated (by objects which *are* allowed to mutate it) in a KVO-compliant manner****. In that case, observers of the property will be notified.

    ** It's possible, though perhaps less usual, for a NSArray* property to be a to-one property with an array value. Semantically, it's a different kind of property (an attribute rather than a relationship), but there's no syntactic difference. Warning: The distinction can cause your brain to hurt.

    *** Here's how you get a immutable proxy to an indexed to-one property:

    a. Implement the countOf<Key> and objectIn<Key>AtIndex: methods in the class with the property.

    b. Have clients of the class invoke 'valueForKey: @"<key>"' on an instance of the class with the property.

    c. DO NOT implement a getter for the property "<key>". Doing so will cause infinite recursion when it's accessed. (This is one of KVC's most annoying defects.)

    **** There isn't AFAIK a really easy way to prevent clients that aren't supposed to mutate the property from just invoking 'mutableArrayValueForKey: @"<key>"' by themselves.

    One alternative approach is to use *two* property names: <privateKey> and <publicKey>.

    You can then have <publicKey>'s getter return '[self valueForKey: @"privateKey"]'. That solves the infinitely-recursing-getter problem, but still returns an immutable observable proxy.

    You can then also implement the custom add/remove KVC accessors for <publicKey> to throw an exception. Objects that *are* permitted to mutate the array will therefore need to know privateKey. (Likely, only instances of the implementing class are the only ones allow, so they know privateKey anyway.)

    A second alternative approach is write your own mutable and immutable array proxies (subclasses of NSArray) instead of using the ones provided by KVC. It isn't terribly hard.

    A third alternative approach is simply to trust your class's clients to respect the mutability rules of your class API. :)
  • Thanks to Roland and Quincy for quick and definitive answers. I'll do that.

    It still bugs me, in the sense that publishing an NSArray* property seems to create a (conceptual) contract that while the property may change, that is, a different NSArray may get assigned to it, the contents of a given NSArray won't change. From what you've both said, this is clearly not the case. But it just smells a little funny.

    One last question: What if the underlying data structure isn't an array at all, but rather a dictionary? It's not possible to insert into it at a particular index. Although that wouldn't prevent me from implementing the methods, it seems it would break, because the OS (and clients) would assume remove and insert are paired, and guaranteed to work on the same index (that is, insert followed by remove should result in an unchanged array).

    --
    Rick

    On Jul 10, 2012, at 23:38 , Roland King wrote:

    > That's exactly what you do.
    >
    > Declare the property as NSArray, that's what your clients see. In most cases you actually use an NSMutableArray as the backing variable so you can easily mutate it, you can either return that mutable array as the value of your property, or you can return a non-mutable copy of it. I normally return the actual array as the property is declared non-mutable so code that tries to mutate it produces lots of errors and warnings. If you're paranoid, copy it before return.
    >
    > Then, in the class which owns it, either implement the KVC indexed accessor methods (like insertObject:in<Key>AtIndex:) and call those, or use [ self mutableArrayValueForKey:<Key> ] to get a mutable array which also generates the correct KVC calls. Any observer of the property will get the correct KVO calls.
    >
    >
    > On Jul 11, 2012, at 2:17 PM, Rick Mann wrote:
    >
    >> I feel certain people have run into this before, but my Googling didn't turn up exactly what I was looking for.
    >>
    >> I'd like to create a "mutable read-only" array property. I want clients of the class to be able to observe changes to the *contents* of the array (that is, if an element is added or removed), in addition to changes of the entire array. But I don't want those clients to be able to add or remove anything to/from the array themselves; I only want the class on which the property is defined to be able to do that.
    >>
    >> Is this possible? My first thought is to make a read-only NSArray* property, but then it seems that addition or removal of elements to/from that property wouldn't get observed; i.e., an NSArray should never change its contents.
    >>
    >> What's the right way to do this? Thanks!
    >> --
    >> Rick
    >
  • On Jul 10, 2012, at 23:45 , Quincey Morris wrote:

    > You can then have <publicKey>'s getter return '[self valueForKey: @"privateKey"]'. That solves the infinitely-recursing-getter problem, but still returns an immutable observable proxy.

    Ugh, that's not right. You have to generate the <publicKey> KVO notifications manually in order to make the proxy properly observable. That means calling suitable will/didChange… methods in suitable places.
  • On Jul 10, 2012, at 23:51 , Rick Mann wrote:

    > It still bugs me, in the sense that publishing an NSArray* property seems to create a (conceptual) contract that while the property may change, that is, a different NSArray may get assigned to it, the contents of a given NSArray won't change. From what you've both said, this is clearly not the case. But it just smells a little funny.

    No, your thinking is incorrect, in the sense that you're somewhat confusing the backing storage (i.e. instance variable and hence array pointer value) with the property value. It's perfectly fine, for example, for a class to implement an indexed to-many property like this (ignoring the proxy/mutability issues we were discussing before):

    @implementation MyClass {
      NSMutableArray* _arrayIvar;
    }

    - (NSArray*) myProperty {
      return _arrayIvar;
    }

    - (void) setMyProperty: (NSArray*) newObjects {
      [_arrayIvar removeAllObjects];
      [_arrayIvar addObjectsFromArray: newObjects];
    }

    In that case, the returned array pointer is always the same. (The above code is KVO-compliant, BTW.)

    Because it's a to-many property, there's no API promise regarding the pointer value returned by the getter. It just has to be a NSArray-like object that provides access to the objects "in" the relationship. There's also no inherent API promise saying whether the returned NSArray is a snapshot at a moment in time, or an object that reflects the current contents. If it matters, you have to settle the behavior by what you're returning. (In the above example, the difference would be exemplified by returning '[_arrayIvar copy]' vs. just '_arrayIvar'.)

    > One last question: What if the underlying data structure isn't an array at all, but rather a dictionary? It's not possible to insert into it at a particular index. Although that wouldn't prevent me from implementing the methods, it seems it would break, because the OS (and clients) would assume remove and insert are paired, and guaranteed to work on the same index (that is, insert followed by remove should result in an unchanged array).

    One way is to create a supplementary array data structure that lists the dictionary key values in the order that's correct for the array indexes. You maintain both data structures in your custom insert/remove/replace KVC accessors, as well as in methods that insert/remove dictionary entries for other reasons.
  • On Jul 11, 2012, at 00:24 , Quincey Morris wrote:

    > On Jul 10, 2012, at 23:51 , Rick Mann wrote:
    >
    >> … But it just smells a little funny.
    >
    > No, your thinking is incorrect, in the sense that …

    Perhaps my way of saying this unintentionally sounded rude. I meant, "your thinking is *a little* incorrect, …".

    Apologies for my poor choice of words.
  • On Jul 11, 2012, at 0:34 , Quincey Morris wrote:

    > On Jul 11, 2012, at 00:24 , Quincey Morris wrote:
    >
    >> On Jul 10, 2012, at 23:51 , Rick Mann wrote:
    >>
    >>> … But it just smells a little funny.
    >>
    >> No, your thinking is incorrect, in the sense that …
    >
    > Perhaps my way of saying this unintentionally sounded rude. I meant, "your thinking is *a little* incorrect, …".
    >
    > Apologies for my poor choice of words.

    No worries :-)

    I still disagree, in the sense that, as a client, I would never expect the contents of an NSArray to change (only the array assigned to the property). In practice, that may not matter. In any case, I can live with it.

    --
    Rick
  • This is (one) application of the "wrapper" design pattern - make a class that wraps (contains) an NSMutableArray, and only expose those properties and methods you want clients to have access to. This is possible by declaring public readonly @properties, but redeclaring them in your implementation file using an anonymous category. Ex:

    MyFoo.h
    @interface MyReadonlyFoo : NSObject

    @property (nonatomic, readonly) NSMutableArray *myFooObjects;

    MyFoo.m
    #import "MyFoo.h"

    @interface MyReadonlyFoo ()
    @property (nonatomic, readwrite) NSMutableArray *myFooObjects;
    @end

    @implementation MyReadonlyFoo

    @synthesize myFooObjects;

    @end

    Now your implementation has access to read-write accessors, but everyone else only sees readonly accessors. They should still be KVC/KVO compliant.
    (Note they syntax may be slightly off - this is just what I remember from various 'learn ObjC' books…)

    On Jul 11, 2012, at 1:17 AM, Rick Mann wrote:

    > I feel certain people have run into this before, but my Googling didn't turn up exactly what I was looking for.
    >
    > I'd like to create a "mutable read-only" array property. I want clients of the class to be able to observe changes to the *contents* of the array (that is, if an element is added or removed), in addition to changes of the entire array. But I don't want those clients to be able to add or remove anything to/from the array themselves; I only want the class on which the property is defined to be able to do that.
    >
    > Is this possible? My first thought is to make a read-only NSArray* property, but then it seems that addition or removal of elements to/from that property wouldn't get observed; i.e., an NSArray should never change its contents.
    >
    > What's the right way to do this? Thanks!
    > --
    > Rick
  • On Jul 11, 2012, at 2:36 AM, Rick Mann wrote:

    > I still disagree, in the sense that, as a client, I would never expect the contents of an NSArray to change (only the array assigned to the property).

    That expectation was/is unfounded.  In fact, I suspect you don't even expect it as often as you think.  I think you just got yourself turned around by over-thinking your case.

    For example, do you expect -[NSApplication windows] to change?  -[NSView subviews]?  -[NSMutableDictionary allKeys]?  -[NSWorkspace runningApplications]?

    My point is that it's totally common for a class to have a property of type NSArray* whose contents change over time.  And you regularly use such properties without a second thought.

    Regards,
    Ken
  • On Jul 11, 2012, at 1:45 AM, Quincey Morris wrote:

    > **** There isn't AFAIK a really easy way to prevent clients that aren't supposed to mutate the property from just invoking 'mutableArrayValueForKey: @"<key>"' by themselves.
    >
    > One alternative approach is to use *two* property names: <privateKey> and <publicKey>.
    >
    > You can then have <publicKey>'s getter return '[self valueForKey: @"privateKey"]'. That solves the infinitely-recursing-getter problem, but still returns an immutable observable proxy.

    On Jul 11, 2012, at 1:55 AM, Quincey Morris wrote:

    > Ugh, that's not right. You have to generate the <publicKey> KVO notifications manually in order to make the proxy properly observable. That means calling suitable will/didChange… methods in suitable places.

    You can use +keyPathsForValuesAffecting<PublicKey> to make the public property emit KVO change notifications whenever the private property changes.  However, I doubt that emits appropriate *array-specific* KVO change notifications for the public property – i.e. those whose change kind is NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, or NSKeyValueChangeReplacement.  Rather it will probably just emit whole-property-replaced (NSKeyValueChangeSetting) notifications.

    (By the way, you're using the term "proxy" in a manner which I find confusing.)

    Another approach is to just define a single property.  Instead of implementing the KVC-compliant indexed accessor mutation methods, implement variants with a slightly non-conformant name (perhaps with a prefix of "private").  Then have those manually emit the proper KVO change notifications by invoking -will/didChange:valuesAtIndexes:forKey:.  Use those private methods to mutate the property from within your own code.  I couldn't tell if that's what you meant by:

    > A second alternative approach is write your own mutable and immutable array proxies (subclasses of NSArray) instead of using the ones provided by KVC. It isn't terribly hard.

    Regards,
    Ken
  • On Jul 11, 2012, at 9:28 AM, Ken Thomases wrote:

    > On Jul 11, 2012, at 1:45 AM, Quincey Morris wrote:
    >
    >> **** There isn't AFAIK a really easy way to prevent clients that aren't supposed to mutate the property from just invoking 'mutableArrayValueForKey: @"<key>"' by themselves.

    > Another approach is to just define a single property.  Instead of implementing the KVC-compliant indexed accessor mutation methods, implement variants with a slightly non-conformant name (perhaps with a prefix of "private").  Then have those manually emit the proper KVO change notifications by invoking -will/didChange:valuesAtIndexes:forKey:.  Use those private methods to mutate the property from within your own code.

    Oh, and I forgot to say: always, *always* override +accessInstanceVariablesDirectly to return NO in all of your classes.  KVC's direct instance variable access is an encapsulation-violating abomination.

    Regards,
    Ken
  • On Jul 11, 2012, at 07:28 , Ken Thomases wrote:

    > (By the way, you're using the term "proxy" in a manner which I find confusing.)

    The term "collection proxy object" is used in NSKeyValueCoding.h -- there's an immutable one from 'valueForKey:' and a mutable one from 'mutableValueForKey:'.

    By analogy, when I've written NSArray subclasses that control access to other arrays via the containment pattern, I've called them proxies too.
previous month july 2012 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