[ACCEPTED]-How should I use properties when dealing with read-only List<T> members-readonly

Accepted answer
Score: 21

You can expose it AsReadOnly. That is, return a read-only 3 IList<T> wrapper. For example ...

public ReadOnlyCollection<int> List
{
    get { return _lst.AsReadOnly(); }
}

Just returning 2 an IEnumerable<T> is not sufficient. For example ...

void Main()
{
    var el = new ExposeList();
    var lst = el.ListEnumerator;
    var oops = (IList<int>)lst;
    oops.Add( 4 );  // mutates list

    var rol = el.ReadOnly;
    var oops2 = (IList<int>)rol;

    oops2.Add( 5 );  // raises exception
}

class ExposeList
{
  private List<int> _lst = new List<int>() { 1, 2, 3 };

  public IEnumerable<int> ListEnumerator
  {
     get { return _lst; }
  }

  public ReadOnlyCollection<int> ReadOnly
  {
     get { return _lst.AsReadOnly(); }
  }
}

Steve's answer also 1 has a clever way to avoid the cast.

Score: 11

There is limited value in attempting to 32 hide information to such an extent. The 31 type of the property should tell users what 30 they're allowed to do with it. If a user 29 decides they want to abuse your API, they 28 will find a way. Blocking them from casting 27 doesn't stop them:

public static class Circumventions
{
    public static IList<T> AsWritable<T>(this IEnumerable<T> source)
    {
        return source.GetType()
            .GetFields(BindingFlags.Public |
                       BindingFlags.NonPublic | 
                       BindingFlags.Instance)
            .Select(f => f.GetValue(source))
            .OfType<IList<T>>()
            .First();
    }
}

With that one method, we 26 can circumvent the three answers given on 25 this question so far:

List<int> a = new List<int> {1, 2, 3, 4, 5};

IList<int> b = a.AsReadOnly(); // block modification...

IList<int> c = b.AsWritable(); // ... but unblock it again

c.Add(6);
Debug.Assert(a.Count == 6); // we've modified the original

IEnumerable<int> d = a.Select(x => x); // okay, try this...

IList<int> e = d.AsWritable(); // no, can still get round it

e.Add(7);
Debug.Assert(a.Count == 7); // modified original again

Also:

public static class AlexeyR
{
    public static IEnumerable<T> AsReallyReadOnly<T>(this IEnumerable<T> source)
    {
        foreach (T t in source) yield return t;
    }
}

IEnumerable<int> f = a.AsReallyReadOnly(); // really?

IList<int> g = f.AsWritable(); // apparently not!
g.Add(8);
Debug.Assert(a.Count == 8); // modified original again

To reiterate... this 24 kind of "arms race" can go on 23 for as long as you like!

The only way to 22 stop this is to completely break the link 21 with the source list, which means you have 20 to make a complete copy of the original 19 list. This is what the BCL does when it 18 returns arrays. The downside of this is 17 that you are imposing a potentially large 16 cost on 99.9% of your users every time they 15 want readonly access to some data, because 14 you are worried about the hackery of 00.1% of 13 users.

Or you could just refuse to support 12 uses of your API that circumvent the static 11 type system.

If you want a property to return 10 a read-only list with random access, return 9 something that implements:

public interface IReadOnlyList<T> : IEnumerable<T>
{
    int Count { get; }
    T this[int index] { get; }
}

If (as is much 8 more common) it only needs to be enumerable 7 sequentially, just return IEnumerable:

public class MyClassList
{
    private List<int> li = new List<int> { 1, 2, 3 };

    public IEnumerable<int> MyList
    {
        get { return li; }
    }
}

UPDATE Since I wrote 6 this answer, C# 4.0 came out, so the above 5 IReadOnlyList interface can take advantage of covariance:

public interface IReadOnlyList<out T>

And 4 now .NET 4.5 has arrived and it has... guess 3 what...

IReadOnlyList interface

So if you want to create a self-documenting 2 API with a property that holds a read-only 1 list, the answer is in the framework.

Score: 6

JP's answer regarding returning IEnumerable<int> is correct (you can 6 down-cast to a list), but here is a technique 5 that prevents the down-cast.

class ExposeList
{
  private List<int> _lst = new List<int>() { 1, 2, 3 };

  public IEnumerable<int> ListEnumerator
  {
     get { return _lst.Select(x => x); }  // Identity transformation.
  }

  public ReadOnlyCollection<int> ReadOnly
  {
     get { return _lst.AsReadOnly(); }
  }
}

The identity 4 transformation during enumeration effectively 3 creates a compiler-generated iterator - a 2 new type which is not related to _lst in any 1 way.

Score: 1

Eric Lippert has a series of articles on 3 Immutability In C# on his blog.

The first 2 article in the series can be found here.

You 1 might also find useful Jon Skeet's answer to a similar question.

Score: 1
public List<int> li;

Don't declare public fields, it's generally 3 considered bad practice... wrap it in a 2 property instead.

You can expose your collection 1 as a ReadOnlyCollection :

private List<int> li;
public ReadOnlyCollection<int> List
{
    get { return li.AsReadOnly(); }
}
Score: 0
public class MyClassList
{
    private List<int> _lst = new List<int>() { 1, 2, 3 };

    public IEnumerable<int> ListEnumerator
    {
        get { return _lst.AsReadOnly(); }
    }

}

To check it

    MyClassList  myClassList = new MyClassList();
    var lst= (IList<int>)myClassList.ListEnumerator  ;
    lst.Add(4); //At this point ypu will get exception Collection is read-only.

0

Score: 0
public static IEnumerable<T> AsReallyReadOnly<T>(this IEnumerable<T> source)
{
    foreach (T t in source) yield return t;
}

if I add to Earwicker's example

...
IEnumerable<int> f = a.AsReallyReadOnly();
IList<int> g = f.AsWritable(); // finally can't get around it

g.Add(8);
Debug.Assert(a.Count == 78);

I get InvalidOperationException: Sequence contains no matching element.

0

More Related questions