17

in my C#-project, I have a class which contains a List

public class MyClass
{
  public MyClass parent;
  public List<MyClass> children;
  ...
}

I want to prevent the user of the class from Adding (and Deleting) an Element to the children-List, but he shall still be able to parse its elements. I want to handle the Adding and Deleting within MyClass, providing an AddChild(MyClass item) and DeleteChild(MyClass item) to ensure that, when an item is added to the child list, the parent of that item will be set properly.

Any idea how to do this besides implementing my own IList?

Thanks in advance, Frank

Aaginor
  • 4,516
  • 11
  • 51
  • 75
  • possible duplicate of [How to I override List's Add method in C#?](http://stackoverflow.com/questions/580202/how-to-i-override-listts-add-method-in-c) – Hans Passant Jun 16 '10 at 14:12

10 Answers10

34

If you hand the caller the List<T>, you can't control this; you can't even subclass List<T>, since the add/remove are not virtual.

Instead, then, I would expose the data as IEnumerable<MyClass>:

private readonly List<MyClass> children = new List<MyClass>();
public void AddChild(MyClass child) {...}
public void RemoveChild(MyClass child) {...}
public IEnumerable<MyClass> Children {
    get {
        foreach(var child in children) yield return child;
    }
}

(the yield return prevents them just casting it)

The caller can still use foreach on .Children, and thanks to LINQ they can do all the other fun things too (Where, First, etc).

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • You can always mask those methods with the new operator and make them do nothing - but you may as well just make your own custom collection by this point :) – Adam Houldsworth Jun 16 '10 at 12:07
  • @Adam - if you `new` them, the caller can completely bypass your methods by just typing their variable as `List`. You'd also need to re-implement all of `IList` and `IList`. Not worth it. – Marc Gravell Jun 16 '10 at 12:08
  • 3
    You can also use the `.AsEnumerable()` extension method instead of the `foreach`/`yield` loop. http://msdn.microsoft.com/en-us/library/bb335435.aspx – spoulson Jun 16 '10 at 12:11
  • 5
    @spoulson - no; that is implemented as "return source" (look in reflector), so I could still cast it back to a `List` and call `Add`. To demo, try: `var list = new List(); ((IList)list.AsEnumerable()).Add("abc"); Console.WriteLine(list.Count);` – Marc Gravell Jun 16 '10 at 12:14
  • 1
    That way you return an independent collection that gets outdated if children are added to/removed from the original List, no? – Ozan Jun 16 '10 at 12:32
  • 2
    It's better to return the list directly as the `IEnumerable`, as this allows LINQ to work with it efficiently. For instance, the user can use `.Count()` or `.ElementAt()` with O(1) performance. The fact that they could mutate your list by casting it is usually not important, as they could just as easily access your private fields via Reflection if you're running in full trust. The intention of the public contract is clear: I don't expect you to mutate this enumeration's collection and I make no guarantees as to what will happen if you try. – Dan Bryant Jun 16 '10 at 13:44
  • @marc - it _could_ be, but then, the caller could do the same via reflection. Usually, this isn't a security issue, but one of clarity Wrapping the list in an IEnumerable rather than exposing it as an IEnumberable is slightly longer - it depends on the usage. Internally I'd just return the list but for an external interface you might choose to use a more robust interface. – Eamon Nerbonne Jun 16 '10 at 13:48
  • @Dan / @Eamon - indeed there are pluses and minuses of it; returning the list (cast as enumerable) will *generally* suffice too. – Marc Gravell Jun 16 '10 at 13:50
  • 2
    @Ozan - no, IEnumerables implemented via `yield return` are executed lazily via a state machine. The foreach...yield return approach will yield the same results as the underlying list's iterator would in the face of concurrent modification. Modifying the list while it's not actively being enumerated is OK; modifying it while it is (i.e. you're in a foreach loop) is to be avoided. – Eamon Nerbonne Jun 16 '10 at 13:54
  • @Eamon Nerbonne: wow, I was completely ignorant of the power of the yield statement. Will definitely use it more often. (I read http://flimflan.com/blog/ThePowerOfYieldReturn.aspx) – Ozan Jun 16 '10 at 16:00
  • @Ozan - Jon's book has this available as a free chapter (ch 6): http://www.manning.com/skeet/ – Marc Gravell Jun 16 '10 at 20:08
17

Beside of implementing your own IList<T> you could return a ReadOnlyCollection<MyClass>.

public MyClass
{
    public MyClass Parent;

    private List<MyClass> children;
    public ReadOnlyCollection<MyClass> Children
    {
        get { return children.AsReadOnly(); }
    }
}
LukeH
  • 263,068
  • 57
  • 365
  • 409
ba__friend
  • 5,783
  • 2
  • 27
  • 20
  • 1
    +1 for the cleanest solution - though just exposing as an IEnumerable<> is almost as good, faster, and simpler. – Eamon Nerbonne Jun 16 '10 at 13:41
  • 1
    +1 from me. I like this just for the simplicity of code. KISS principle! – Srikanth Venugopalan Jun 16 '10 at 14:14
  • I like this better than enumeration via yield return, as it preserves the efficiency of list access (with only a slight hit for the indirection). I would change the return type to `IEnumerable`, even if `AsReadOnly()` is used, as this gives the class implementer more flexibility to change how the Children are stored/exposed in the future. If they are later placed in a Dictionary internally, I don't have to create a wrapper list just to preserve the public interface. – Dan Bryant Jun 16 '10 at 14:27
  • @DanBryant the flexibility offered by the type `IEnumerable` regarding the internal storage of the `Children` comes at a heavy price: it reduces the freedom with which the consumer can interact with the `Children`. The `IEnumerable` type carries strong deferred execution semantics, meaning that each enumeration might result in a separate database query or communication through the network. This induces the consumer to enumerate the sequence only once, either by materializing it to a list or array, or using memoization techniques. The type `IReadOnlyList` is better. – Theodor Zoulias Aug 30 '23 at 07:05
6

Expose the list as IEnumerable<MyClass>

UpTheCreek
  • 31,444
  • 34
  • 152
  • 221
3

Make the list private, and create public methods to access its content from outside the class.

public class MyClass
{
    public MyClass Parent { get; private set; }

    private List<MyClass> _children = new List<MyClass>();
    public IEnumerable<MyClass> Children
    {
        get
        {
            // Using an iterator so that client code can't access the underlying list
            foreach(var child in _children)
            {
                yield return child;
            }
        }
    }

    public void AddChild(MyClass child)
    {
        child.Parent = this;
        _children.Add(child);
    }

    public void RemoveChild(MyClass child)
    {
        _children.Remove(child);
        child.Parent = null;
    }
}

By the way, avoid declaring public fields, because you can't control what client code does with them.

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
1
using System.Collections.ObjectModel;

public class MyClass
{
    private List<MyClass> children;

    public ReadOnlyCollection<MyClass> Children
    {
        get
        {
            return new ReadOnlyCollection<MyClass>(children);
        }
    }
}
simendsjo
  • 4,739
  • 2
  • 25
  • 53
  • if you add/remove items on this.children, the readOnlyCollection will gain its items, as it stores the original list internally ... –  Jun 16 '10 at 12:08
1

The following code is basically the same which was already provided but uses:

class Order {
  private readonly List<string> _orderItems = new List<string>();
  public IReadOnlyCollection<string> OrderItems => _orderItems;

  // ...
}

IMO much easier to read. Also, here is a Microsoft-provided project where this is used in a production-like scenario.

Yannic Hamann
  • 4,655
  • 32
  • 50
0

You don't need to reimplemented IList, just encapsulate a List in a class, and provide your own Add and Delete functions, that will take care of adding and setting the required properties.

You might have to create your own enumerator if you want to enumerate through the list though.

Miki Watts
  • 1,746
  • 1
  • 17
  • 27
0

You can encapsulate the list in the class by making it private, and offer it as a ReadOnlyCollection:

public class MyClass {

  public MyClass Parent { get; private set; };

  private List<MyClass> _children;

  public ReadOnlyCollection<MyClass> Children {
    get { return _children.AsReadOnly(); }
  }

  public void AddChild(MyClass item) {
    item.Parent = this;
    _children.Add(item);
  }

  public void DeleteChild(MyClass item) {
    item.Parent = null;
    _children.Remove(item);
  }

}

You can make the Parent a property with a private setter, that way it can't be modified from the outside, but the AddChild and DeleteChild methods can change it.

Guffa
  • 687,336
  • 108
  • 737
  • 1,005
0

Use List(T).AsReadOnly().

http://msdn.microsoft.com/en-us/library/e78dcd75.aspx

Returns a read-only IList<(Of <(T>)>) wrapper for the current collection.

Available since .NET 2.0.

code4life
  • 15,655
  • 7
  • 50
  • 82
0

Instead of creating a new ReadOnlyCollection<MyClass> with the help of the AsReadOnly method each time the Children property is invoked, I would suggest to return the same cached instance every time:

public class MyClass
{
    private readonly List<MyClass> _children;
    private readonly ReadOnlyCollection<MyClass> _childrenReadOnly;

    public MyClass()
    {
        _children = new();
        _childrenReadOnly = _children.AsReadOnly();
    }

    public IReadOnlyList<MyClass> Children => _childrenReadOnly;
}

This way the size of the MyClass type will increase by a few bytes, but the gains in terms of reducing the pressure on the garbage collector might be worth it. Beyond that, it is also more reasonable for a consumer to observe that x.Children == x.Children. If you have an album with the photos of your children, you don't put the pictures in a new album each time you want to see the pictures!

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104