Как использовать свойства при работе с членами списка <T>, доступными только для чтения - PullRequest
9 голосов
/ 05 августа 2009

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

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

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

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

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

Ответы [ 7 ]

17 голосов
/ 05 августа 2009

Вы можете выставить его 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(); }
  }
}

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

10 голосов
/ 05 августа 2009

Существует ограниченная ценность попыток скрыть информацию в такой степени. Тип свойства должен сообщать пользователям, что им разрешено делать с ним. Если пользователь решит, что он хочет злоупотребить вашим 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; }
    }
}

ОБНОВЛЕНИЕ С тех пор, как я написал этот ответ, вышел C # 4.0, поэтому вышеуказанный интерфейс IReadOnlyList может использовать ковариацию:

public interface IReadOnlyList<out T>

А теперь появился .NET 4.5 и он ... угадайте, что ...

Интерфейс IReadOnlyList

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

5 голосов
/ 05 августа 2009

Ответ 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.

1 голос
/ 05 августа 2009

Эрик Липперт опубликовал в своем блоге серию статей об неизменности в C #.

Первая статья в серии можно найти здесь .

Вы также можете найти полезный ответ Джона Скита на похожий вопрос .

0 голосов
/ 05 августа 2009
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.

0 голосов
/ 05 августа 2009
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.
0 голосов
/ 05 августа 2009
public List<int> li;

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

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

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