It’s quite reasonable to want to expose a collection as a property where the type of the property is a base type of the backing field. For example, the backing field for a collection property could be declared as List<string> but the property itself could be declared as IEnumerable<string>. This is useful when you need to modify the collection from within the type but don’t want clients to have the same freedom. You could implement this as follows:
public class MyClass { private readonly List<string> m_items = new List<string>(); public IEnumerable<string> Items { get { return m_items; } } }
Simple right? Unfortunately not. The problem here is that we’re still returning an instance of List<string> even though we only expose it as IEnumerable<string>. Callers can still cast the instance we returned to a more specialised type, which means they can modify the collection and generally wreak havoc. One way to address this is to return a proper read-only instance, such as ReadOnlyCollection<string>.
public class MyClass { private readonly List<string> m_items = new List<string>(); private readonly ReadOnlyCollection<string> m_itemsReadOnly; public MyClass() : base() { m_itemsReadOnly = m_items.AsReadOnly(); } public IEnumerable<string> Items { get { return m_itemsReadOnly; } } }
Now if a caller casts the returned instance to a more specialised type, such as ICollection<string>, any attempt to modify the collection will throw a NotSupportedException because the instance we returned is read-only. But why do we need a separate backing field for the read-only collection? Well the Framework Design Guidelines states that properties should not return a different result each time they’re invoked. For reference types this means we should return the same instance. This makes sense when we consider that properties are generally expected to behave like fields but with some additional lightweight logic thrown in to the mix. In other words, we’d expect both of the following lines to evaluate to true.
m_value == m_value; // Field equal to itself? item.Value == item.Value; // Property equal to itself?
This means we can’t use the AsReadOnly method in the property because it will return a different instance each time. We have to use a separate backing field for the read-only collection to achieve the expected behaviour for a property. We also don’t have to worry about updating this backing field when the collection changes because the AsReadOnly method returns a wrapper around the original collection. Another approach I’ve seen is to use an iterator as shown below.
public class MyClass { private readonly List<string> m_items = new List<string>(); public IEnumerable<string> Items { get { foreach (string item in m_items) { yield return item; } } } }
This works because the iterator will generate its own implementation of IEnumerable<string> which is completely independent of the implementation provided by List<string>. This means a caller cannot cast the returned instance to a more specialised type. Unfortunately it will also return a different instance every time we access the property, which as we’ve already seen is not how properties should behave. So why not make it a method then? Well if the member represents a logical attribute of the type then it really should be a property. The Items member seems to fit the bill, so a property is the right choice.
0 comments:
Post a Comment