How to expose a collection property? [closed]
Asked Answered
L

7

49

Every time I create an object that has a collection property I go back and forth on the best way to do it?

  1. public property with a getter that returns a reference to private variable
  2. explicit get_ObjList and set_ObjList methods that return and create new or cloned objects every time
  3. explicit get_ObjList that returns an IEnumerator and a set_ObjList that takes IEnumerator

Does it make a difference if the collection is an array (i.e., objList.Clone()) versus a List?

If returning the actual collection as a reference is so bad because it creates dependencies, then why return any property as a reference? Anytime you expose an child object as a reference the internals of that child can be changed without the parent "knowing" unless the child has a property changed event. Is there a risk for memory leaks?

And, don't options 2 and 3 break serialization? Is this a catch 22 or do you have to implement custom serialization anytime you have a collection property?

The generic ReadOnlyCollection seems like a nice compromise for general use. It wraps an IList and restricts access to it. Maybe this helps with memory leaks and serialization. However it still has enumeration concerns

Maybe it just depends. If you don't care that the collection is modified, then just expose it as a public accessor over a private variable per #1. If you don't want other programs to modify the collection then #2 and/or #3 is better.

Implicit in the question is why should one method be used over another and what are the ramifications on security, memory, serialization, etc.?

Latty answered 29/8, 2008 at 18:48 Comment(0)
S
57

How you expose a collection depends entirely on how users are intended to interact with it.

1) If users will be adding and removing items from an object's collection, then a simple get-only collection property is best (option #1 from the original question):

private readonly Collection<T> myCollection_ = new ...;
public Collection<T> MyCollection {
  get { return this.myCollection_; }
}

This strategy is used for the Items collections on the WindowsForms and WPF ItemsControl controls, where users add and remove items they want the control to display. These controls publish the actual collection and use callbacks or event listeners to keep track of items.

WPF also exposes some settable collections to allow users to display a collection of items they control, such as the ItemsSource property on ItemsControl (option #3 from the original question). However, this is not a common use case.


2) If users will only be reading data maintained by the object, then you can use a readonly collection, as Quibblesome suggested:

private readonly List<T> myPrivateCollection_ = new ...;
private ReadOnlyCollection<T> myPrivateCollectionView_;
public ReadOnlyCollection<T> MyCollection {
  get {
    if( this.myPrivateCollectionView_ == null ) { /* lazily initialize view */ }
    return this.myPrivateCollectionView_;
  }
}

Note that ReadOnlyCollection<T> provides a live view of the underlying collection, so you only need to create the view once.

If the internal collection does not implement IList<T>, or if you want to restrict access to more advanced users, you can instead wrap access to the collection through an enumerator:

public IEnumerable<T> MyCollection {
  get {
    foreach( T item in this.myPrivateCollection_ )
      yield return item;
  }
}

This approach is simple to implement and also provides access to all the members without exposing the internal collection. However, it does require that the collection remain unmodfied, as the BCL collection classes will throw an exception if you try to enumerate a collection after it has been modified. If the underlying collection is likely to change, you can either create a light wrapper that will enumerate the collection safely, or return a copy of the collection.


3) Finally, if you need to expose arrays rather than higher-level collections, then you should return a copy of the array to prevent users from modifying it (option #2 from the orginal question):

private T[] myArray_;
public T[] GetMyArray( ) {
  T[] copy = new T[this.myArray_.Length];
  this.myArray_.CopyTo( copy, 0 );
  return copy;
  // Note: if you are using LINQ, calling the 'ToArray( )' 
  //  extension method will create a copy for you.
}

You should not expose the underlying array through a property, as you will not be able to tell when users modify it. To allow modifying the array, you can either add a corresponding SetMyArray( T[] array ) method, or use a custom indexer:

public T this[int index] {
  get { return this.myArray_[index]; }
  set {
    // TODO: validate new value; raise change event; etc.
    this.myArray_[index] = value;
  }
}

(of course, by implementing a custom indexer, you will be duplicating the work of the BCL classes :)

Sulla answered 1/9, 2008 at 20:46 Comment(6)
Please note that arrays cause boxing and unboxing of objects which is very resource intensiveBraziel
If the array is strongly typed (i.e. int[], long[], etc), accessing elements will not cause boxing. This would only happen if you were using an object[] to store value types (i.e. object[] a = new { 1, 2, 3 }).Sulla
Ah, fair point, you've improved my suggestion. Nice one! :)Metaphosphate
Implementing the first method you give in solution 2 will cause an "A field initializer cannot reference the non-static field, method, or property" Error, Explained by #923843. By NOT declaring myPrivateCollectionView_ as readonly, it can be set the first time the MyCollection property is used. This is also more efficient, as myPrivateCollectionView_ is only created and maintained if it is actually needed.Untie
@jphofmann: Yes, avoiding obvious compilation errors, even in purely illustrative code, is a good idea :) I've updated the first example code for #2 with your point about lazy initialization of the view.Sulla
How about serialization. I want to serialize the collection, so I have to expose the setter. How achieving a 'best practice' for exposing collections but allowing serialization.Adjudication
M
3

I usually go for this, a public getter that returns System.Collections.ObjectModel.ReadOnlyCollection:

public ReadOnlyCollection<SomeClass> Collection
{
    get
    {
         return new ReadOnlyCollection<SomeClass>(myList);
    }
}

And public methods on the object to modify the collection.

Clear();
Add(SomeClass class);

If the class is supposed to be a repository for other people to mess with then I just expose the private variable as per method #1 as it saves writing your own API, but I tend to shy away from that in production code.

Metaphosphate answered 29/8, 2008 at 19:7 Comment(4)
This creates new ReadOnlycollection on each read of Collection property, that can be very resource intensiveInsolvable
Aye, Emperor XLII improves the premise in the above example he posted.Metaphosphate
@Insolvable Actually no, it's not resource intensive at all since calling this constructor is an O(1) operation, this is just a wrapper. msdn.microsoft.com/en-us/library/ms132476(v=vs.110).aspxEadwina
I should have been more specific. I was thinking about wrapper itself. Each call to this property creates a new wrapper. Although that is O(1) this consumes memory and increases pressure on garbage collector. In some scenarios where this property is called in a loop you might end with pushing a lot of data to generation 2 which does not need to be there. It's a minor issue and a scenarios where that matters are rare. In retrospect I believe I shouldn't have posted the original comment at all :).Insolvable
V
1

ReadOnlyCollection still has the disadvantage that the consumer can't be sure that the original collection won't be changed at an inopportune time. Instead you can use Immutable Collections. If you need to do a change then instead changing the original you are being given a modified copy. The way it is implemented it is competitive with the performance of the mutable collections. Or even better if you don't have to copy the original several times to make a number of different (incompatible) changes afterwards to each copy.

Vaporific answered 18/5, 2015 at 21:55 Comment(0)
I
1

I recommend to use the new IReadOnlyList<T> and IReadOnlyCollection<T> Interfaces to expose a collection (requires .NET 4.5).

Example:

public class AddressBook
{
    private readonly List<Contact> contacts;

    public AddressBook()
    {
        this.contacts = new List<Contact>();
    }

    public IReadOnlyList<Contact> Contacts { get { return contacts; } }

    public void AddContact(Contact contact)
    {
        contacts.Add(contact);
    }

    public void RemoveContact(Contact contact)
    {
        contacts.Remove(contact);
    }
}

If you need to guarantee that the collection can not be manipulated from outside then consider ReadOnlyCollection<T> or the new Immutable collections.

Avoid using the interface IEnumerable<T> to expose a collection. This interface does not define any guarantee that multiple enumerations perform well. If the IEnumerable represents a query then every enumeration execute the query again. Developers that get an instance of IEnumerable do not know if it represents a collection or a query.

More about this topic can be read on this Wiki page.

Illiterate answered 2/10, 2015 at 19:57 Comment(0)
B
0

If you're simply looking to expose a collection on your instance, then using a getter/setter to a private member variable seems like the most sensible solution to me (your first proposed option).

Bigley answered 29/8, 2008 at 18:52 Comment(0)
C
0

I'm a java developer but I think this is the same for c#.

I never expose a private collection property because other parts of the program can change it without parent noticing, so that in the getter method I return an array with the objects of the collection and in the setter method I call a clearAll() over the collection and then an addAll()

Capercaillie answered 29/8, 2008 at 19:50 Comment(0)
R
0

Why do you suggest using ReadOnlyCollection(T) is a compromise? If you still need to get change notifications made on the original wrapped IList you could also use a ReadOnlyObservableCollection(T) to wrap your collection. Would this be less of a compromise in your scenario?

Rubrician answered 5/8, 2009 at 20:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.