Как использовать свойства при работе с членами списка 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
.
Ответ 4
Эрик Липперт имеет серию статей о неизменности в С# в своем блоге.
В первой статье этой серии можно найти здесь.
Вы также можете найти полезный Jon Skeet ответ на аналогичный вопрос.
Ответ 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
.