Как мне использовать свойства при работе с участниками 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> доступным только для чтения (чтобы они не могли добавлять / удалять элементы в / из него) за пределами моего класса? Теперь я просто объявляю это публичным:

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

person George    schedule 04.08.2009    source источник


Ответы (7)


Вы можете открыть его 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(); }
  }
}

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

person JP Alioto    schedule 04.08.2009
comment
Я добавил пример ... Не стесняйтесь откатывать назад, если возражаете. - person Adam Robinson; 05.08.2009
comment
См. Мой ответ, как это обойти. - person Daniel Earwicker; 05.08.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 со свойством, содержащим список только для чтения, ответ находится в структуре.

person Daniel Earwicker    schedule 05.08.2009
comment
Хотя я согласен с вашей общей точкой зрения, см. Мой ответ. С обфусцированными источниками этого может быть достаточно. - person Alexey Romanov; 05.08.2009
comment
Смотрите обновленную версию - вот в чем действительно была моя основная мысль: это гонка вооружений. Мы можем продолжать работу до тех пор, пока вы не будете вынуждены сделать полную копию исходного списка, чтобы избежать каких-либо ссылок на оригинал. - person Daniel Earwicker; 05.08.2009
comment
NB. Я занимаюсь реверс-инжинирингом с помощью отладчика. Не нужно видеть какой-либо источник. - person Daniel Earwicker; 05.08.2009
comment
Я несколько не согласен с вашей логикой. Вы можете вызывать закрытые члены с отражением и #define private public works в C ++, но вы не станете спорить, что вам больше никогда не следует делать что-либо частным. Смысл интерфейса не в том, чтобы сбить с толку злонамеренного разработчика, а в том, чтобы указать разработчику, как правильно использовать интерфейс. AsReadOnly отлично с этим справляется. - person JP Alioto; 05.08.2009
comment
Почему вы используете First(v => (v as IList<T>) != null)? Не могли бы вы использовать First(v => v is IList<T>)? В любом случае, я понимаю вашу точку зрения, но должен согласиться с JP. Кроме того, я бы сказал, что иногда неплохо защищать себя от простого заброса. Но когда люди начинают использовать рефлексию в вашем коде, все ставки в любом случае отменяются, поэтому не нужно беспокоиться о том, что: p - person Svish; 05.08.2009
comment
@JP - я бы не стал спорить, что ничто не должно быть приватным. Чтобы сделать что-то частным, нужно использовать систему статических типов, чтобы объявить, как правильно использовать класс во время компиляции, и это именно то, что я предлагаю, является правильным ответом. Возврат IList ‹T› (который во время компиляции предполагает изменчивость) - неправильный ответ. Чтобы вернуть неизменяемый список, верните статический тип, который имеет только общедоступные операции чтения. - person Daniel Earwicker; 05.08.2009
comment
@Svish - Я бы сказал, что когда пользователи игнорируют статические типы и начинают спекулятивно кастовать, все ставки отключены. Они делают то же самое, что и отражение, но с более удобным синтаксисом (хотя его можно эмулировать, как показано выше). Я думаю, что другие упускают из виду, что IList - это неправильный статический тип, который нужно возвращать, если список предполагается неизменяемым. IEnumerable - правильный выбор, если требуется прямая итерация. Если действительно необходим произвольный доступ только для чтения, используйте что-то вроде IReadOnlyList<T> (см. Выше). Получите правильный статический тип, и вы сделали все, что действительно могли. - person Daniel Earwicker; 05.08.2009
comment
Еще несколько хороших мыслей в комментариях к этому ответу: stackoverflow.com/questions/1228176/ - person Daniel Earwicker; 05.08.2009
comment
Проблема в том, что многие программисты глупы и сочтут приведение безопасным (независимо от того, действительно ли это или нет, не имеет значения, они все еще делают это). Однако, по моему опыту, даже самые глупые программисты дважды подумают, прежде чем использовать операцию BindingFlags.NonPublic отражения. Предполагается, что они в первую очередь нашли эту неясную часть структуры. - person Martin Brown; 06.08.2009
comment
Беда в том, что многие программисты глупы - да, но чья это беда? - person Daniel Earwicker; 06.08.2009
comment
но чья это проблема? К сожалению, здесь я обычно заканчиваю тем, что обращаюсь в службу поддержки. Если вам не нужно поддерживать свой код, вам очень повезло. - person Martin Brown; 06.08.2009
comment
Я предполагаю, позвонил ли вам пользователь и спросил: Почему этот API не может работать с моим тостером? Вы бы, наверное, сказали что-то вроде RTFM, он не предназначен для работы с тостером. И все же, если они скажут, что я попытался преобразовать IEnumerable в IList, чтобы я мог добавить к нему что-то, и, похоже, это сработало, но позже программа вылетела, что случилось с утверждением, что это не предназначено для работы таким образом, поэтому мы сделали это IEnumerable? Вы приняли решение поддерживать пользователей, злоупотребляющих вашим API, за вашу неограниченную плату. И RE: ваша точка зрения о тупых программистах - они всегда могут прийти в SO и попросить о помощи! :) - person Daniel Earwicker; 06.08.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.

person Steve Guidi    schedule 04.08.2009
comment
может также использовать foreach(var i in _lst) yield return i; и избежать выбора? Хотя, наверное, не имеет особого значения ... - person Svish; 05.08.2009
comment
См. Мой ответ, как это обойти. - person Daniel Earwicker; 05.08.2009
comment
@Svish: при выборе идентификатора создается тот же код, который вы указали. Другими словами, select - это вызов метода, который вызывает ваш код. - person Steve Guidi; 05.08.2009

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

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

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

person Winston Smith    schedule 04.08.2009

public List<int> li;

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

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

private List<int> li;
public ReadOnlyCollection<int> List
{
    get { return li.AsReadOnly(); }
}
person Thomas Levesque    schedule 04.08.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.
person Raghav    schedule 05.08.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.

person Alexey Romanov    schedule 05.08.2009
comment
Теперь исправлено - я искал только приватные поля правильного статического типа. Если я ослаблю его до частных и общедоступных полей правильного динамического типа, он обойдется. - person Daniel Earwicker; 05.08.2009