Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

When I want to make a value type read-only outside of my class I do this:

public class myClassInt
{
    private int m_i;
    public int i {
        get { return m_i; }
    }

    public myClassInt(int i)
    {
        m_i = i;
    }
}

What can I do to make a List<T> type readonly (so they can't add/remove elements to/from it) outside of my class? Now I just declare it public:

public class myClassList
{
    public List<int> li;
    public  myClassList()
    {
        li = new List<int>();
        li.Add(1);
        li.Add(2);
        li.Add(3);
    }
}
like image 664
George Avatar asked Aug 04 '09 22:08

George


People also ask

What are the properties of read only?

Read only means that we can access the value of a property but we can't assign a value to it. When a property does not have a set accessor then it is a read only property. For example in the person class we have a Gender property that has only a get accessor and doesn't have a set accessor.

Which of the following option can you use to make property read only?

In c#, you can create the constant fields using const keyword and the read-only fields created by using readonly keyword. In c#, the constant fields are initialized during the declaration, but the read-only fields are initialized either at the declaration or in a constructor.

What is read only property in C sharp?

In C#, a readonly keyword is a modifier which is used in the following ways: 1. Readonly Fields: In C#, you are allowed to declare a field using readonly modifier. It indicates that the assignment to the fields is only the part of the declaration or in a constructor to the same class.


2 Answers

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

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

Just returning 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 has a clever way to avoid the cast.

like image 127
JP Alioto Avatar answered Oct 10 '22 10:10

JP Alioto


There is limited value in attempting to hide information to such an extent. The type of the property should tell users what they're allowed to do with it. If a user decides they want to abuse your API, they will find a way. Blocking them from casting 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 can circumvent the three answers given on 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 kind of "arms race" can go on for as long as you like!

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

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

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

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

If (as is much more common) it only needs to be enumerable 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 this answer, C# 4.0 came out, so the above IReadOnlyList interface can take advantage of covariance:

public interface IReadOnlyList<out T>

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

IReadOnlyList interface

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

like image 11
Daniel Earwicker Avatar answered Oct 10 '22 12:10

Daniel Earwicker