3

In general terms, a program I'm making involves storing a small number of entries (probably less than 30 at any given time) which can be categorized. I want to allow these entries to be seen but not altered from outside the class using them. I made a class called Entry which could be modified and another called ReadOnlyEntry which is a wrapper for an Entry object. The easiest way to organize these Entry objects it seems is to create a List<List<Entry>>, where each List<Entry> is a category. But then exposing that data in a readonly way became messy and complicated. I realized I would have to have one object of each of the following types:

List<List<Entry>> data;
List<List<ReadOnlyEntry>>  //  Where each ReadOnlyEntry is a wrapper for the Entry in the same list and at the same index as its Entry object.
List<IReadOnlyCollection<ReadOnlyEntry>>  //  Where each IReadOnlyCollection is a wrapper for the List<ReadOnlyEntry> at the same index in data.
IReadOnlyCollection<IReadOnlyCollection<ReadOnlyList>> readOnlyList  //  Which is a wrapper for the first item I listed.

The last item in the list would be exposed as public. The first lets me change entries, the second lets me add or delete entries, and the third lets me add or delete categories. I would have to keep these wrappers accurate whenever the data changes. This seems convoluted to me, so I'm wondering if there's a blatantly better way to handle this.

Edit 1: To clarify, I know how to use List.asReadOnly(), and the stuff I proposed doing above will solve my problem. I'm just interested in hearing a better solution. Let me give you some code.

class Database
{
    //  Everything I described above takes place here.

    //  The data will be readable by this property:
    public IReadOnlyCollection<IReadOnlyCollection<ReadOnlyList>> Data
    {
        get
        {
            return readOnlyList;
        }
    }

    //  These methods will be used to modify the data.
    public void AddEntry(stuff);
    public void DeleteEntry(index);
    public void MoveEntry(to another category);
    public void AddCategory(stuff);
    public void DeleteCategory(index);
}
k06a
  • 17,755
  • 10
  • 70
  • 110
  • Possible duplicate of [Make a list readonly in c#](http://stackoverflow.com/questions/29128402/make-a-list-readonly-in-c-sharp) – Markus Mar 01 '16 at 19:20

3 Answers3

4

You can use List<T>.AsReadOnly() to return ReadOnlyCollection<T>.

Also, you're torturing the List<T> class storing the data the way you are. Build your own hierarchy of classes which store your individual lists.

rory.ap
  • 34,009
  • 10
  • 83
  • 174
  • I am using List.asReadOnly(), but returning a ReadOnlyCollection still lets the user modify the objects in the collection. I considered making additional classes to represent a category, but came to the conclusion that it's not worth it because the only thing in a Category would be a list of entries. I might as well just have a List and not make a class for it at all. The only benefit I can see in writing a class is that it makes the syntax a little easier. – portalguy15837 Mar 01 '16 at 19:26
  • There are any number of good design principles you can apply here. [KISS](https://en.wikipedia.org/wiki/KISS_principle), [SRP](https://en.wikipedia.org/wiki/Single_responsibility_principle), etc. If it seems convoluted, then that's a clear indication that you need to break it up into classes. That's one of the fundamental reasons for OO programming. – rory.ap Mar 01 '16 at 19:30
2

.NET collections should support covariance, but they don't support it themselves (instead some interfaces support covariance https://msdn.microsoft.com/ru-ru/library/dd233059.aspx). Covariance means List<Conctrete> behaves like subclass of List<Base> if Concrete is subclass of Base. You can use interfaces covariation or just use casting like this:

using System.Collections.Generic;

namespace MyApp
{
    interface IEntry
    {
    }

    class Entry : IEntry
    {
    }

    class Program
    {
        private List<List<Entry>> _matrix = null;

        public List<List<IEntry>> MatrixWithROElements
        {
            get 
            {
                return _matrix.ConvertAll(row => row.ConvertAll(item => item as IEntry));
            }
        }

        public IReadOnlyList<List<IEntry>> MatrixWithRONumberOfRows
        {
            get 
            {
                return _matrix.ConvertAll(row => row.ConvertAll(item => item as IEntry));
            }
        }

        public List<IReadOnlyList<IEntry>> MatrixWithRONumberOfColumns
        {
            get 
            {
                return _matrix.ConvertAll(row => row.ConvertAll(item => item as IEntry) as IReadOnlyList<IEntry>);
            }
        }

        public IReadOnlyList<IReadOnlyList<IEntry>> MatrixWithRONumberOfRowsAndColumns
        {
            get 
            {
                return _matrix.ConvertAll(row => row.ConvertAll(item => item as IEntry));
            }
        }

        public void Main(string[] args)
        {    
        }
    }
}

Thanks to Matthew Watson for pointing on errors in my previous answer version.

k06a
  • 17,755
  • 10
  • 70
  • 110
  • `List` is not covariant in that way. The line `public List> matrixWithROElements => _matrix;` will not compile. – Matthew Watson Mar 01 '16 at 19:58
  • Specifically, the `List` type is *not* defined as `List`, so it is not covariant over `T`. – Matthew Watson Mar 01 '16 at 20:02
  • You two are absolutely right. This is the most sensible approach. I totally forgot about covariance of interfaces. Thanks for the help! I've marked this as the best answer. – portalguy15837 Mar 02 '16 at 05:56
1

You could make an interface for Entry which contains only getters; you would expose elements via this interface to provide read-only access:

public interface IEntry
{
    int Value { get; }
}

The writable implementation would be simply:

public sealed class Entry : IEntry
{
    public int Value { get; set; }
}

Now you can take advantage of the fact that you can return a List<List<Entry>> as a IReadOnlyCollection<IReadOnlyCollection<IEntry>> without having to do any extra work:

public sealed class Database
{
    private readonly List<List<Entry>> _list = new List<List<Entry>>();

    public Database()
    {
        // Create your list of lists.

        List<Entry> innerList = new List<Entry>
        {
            new Entry {Value = 1},
            new Entry {Value = 2}
        };

        _list.Add(innerList);
    }

    public IReadOnlyCollection<IReadOnlyCollection<IEntry>> Data => _list;
}

Note how simple the implementation of the Data property is.

If you need to add new properties to IEntry you would also have to add them to Entry, but you wouldn't need to change the Database class.

If you're using C#5 or earlier, Data would look like this:

public IReadOnlyCollection<IReadOnlyCollection<IEntry>> Data
{
    get { return _list; }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276