Как использовать свойства при работе с членами списка List <T> только для чтения

Когда я хочу сделать тип значения только для чтения вне моего класса, я делаю это:

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

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

Что я могу сделать, чтобы сделать тип List<T> readonly (чтобы они не могли добавлять/удалять элементы в/из него) вне моего класса? Теперь я просто объявляю его общедоступным:

public class myClassList
{
    public List<int> li;
    public  myClassList()
    {
        li = new List<int>();
        li.Add(1);
        li.Add(2);
        li.Add(3);
    }
}

Ответ 1

Вы можете открыть его AsReadOnly. То есть верните только оболочку IList<T> для чтения. Например...

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

Просто вернуть IEnumerable<T> недостаточно. Например...

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(); }
  }
}

Стив ответ также имеет умный способ избежать приведения.

Ответ 2

Существует ограниченная ценность при попытке скрыть информацию до такой степени. Тип свойства должен сообщать пользователям, что им разрешено делать с ним. Если пользователь решает, что хочет злоупотреблять вашим API, они найдут способ. Блокирование их от кастинга не останавливает их:

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();
    }
}

С помощью этого метода мы можем обойти три ответа, заданные по этому вопросу:

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

также:

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

Повторить... эта "гонка вооружений" может продолжаться до тех пор, пока вам нравится!

Единственный способ остановить это - полностью разбить ссылку на исходный список, что означает, что вы должны сделать полную копию исходного списка. Это то, что делает BCL, когда он возвращает массивы. Недостатком этого является то, что вы накладываете потенциально большие затраты на 99,9% ваших пользователей каждый раз, когда они хотят иметь доступ только к некоторым данным, поскольку вы беспокоитесь о хакерстве в размере 00,1% пользователей.

Или вы можете просто отказаться от поддержки использования вашего API, который обходит систему статического типа.

Если вы хотите, чтобы свойство возвращало список только для чтения со случайным доступом, верните что-то, что реализует:

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

Если (как это гораздо чаще), его нужно перечислить только последовательно, просто верните IEnumerable:

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

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

UPDATE. Поскольку я написал этот ответ, вышел С# 4.0, поэтому вышеприведенный IReadOnlyList интерфейс может использовать ковариацию:

public interface IReadOnlyList<out T>

И теперь .NET 4.5 прибыл, и у него есть... угадайте, что...

Интерфейс IReadOnlyList

Итак, если вы хотите создать самодокументирующий API с свойством, содержащим список только для чтения, ответ находится в рамках.

Ответ 3

Ответ JP относительно возврата IEnumerable<int> правильный (вы можете отбрасывать список в список), но вот техника, которая предотвращает нажатие.

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(); }
  }
}

Преобразование идентичности во время перечисления эффективно создает сгенерированный компилятором итератор - новый тип, который никак не связан с _lst.

Ответ 5

public List<int> li;

Не объявляйте публичные поля, это обычно считается плохой практикой... вместо этого оберните его в свойство.

Вы можете открыть свою коллекцию как ReadOnlyCollection:

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

Ответ 6

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

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

}

Чтобы проверить его

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

Ответ 7

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

если я добавлю пример Earwicker

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

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

Я получаю InvalidOperationException: Sequence contains no matching element.