Как сделать IEnumerable‹T› только для чтения?

Почему списки list1Instance и p в методе Main приведенного ниже кода указывают на одну и ту же коллекцию?

class Person
    {
        public string FirstName = string.Empty;
        public string LastName = string.Empty;

        public Person(string firstName, string lastName) {
            this.FirstName = firstName;
            this.LastName = lastName;
        }
    }

    class List1
    {
        public List<Person> l1 = new List<Person>();

        public List1()
        {
            l1.Add(new Person("f1","l1"));
            l1.Add(new Person("f2", "l2"));
            l1.Add(new Person("f3", "l3"));
            l1.Add(new Person("f4", "l4"));
            l1.Add(new Person("f5", "l5"));
        }
        public IEnumerable<Person> Get()
        {
            foreach (Person p in l1)
            {
                yield return p;
            }

            //return l1.AsReadOnly(); 
        }

    }  

    class Program
    {

        static void Main(string[] args)
        {
            List1 list1Instance = new List1();

            List<Person> p = new List<Person>(list1Instance.Get());           

            UpdatePersons(p);

            bool sameFirstName = (list1Instance.l1[0].FirstName == p[0].FirstName);
        }

        private static void UpdatePersons(List<Person> list)
        {
            list[0].FirstName = "uf1";
        }
    }

Можем ли мы изменить это поведение без изменения возвращаемого типа List1.Get()?

Спасибо


person gk.    schedule 11.12.2008    source источник


Ответы (8)


На самом деле IEnumerable<T> уже доступен только для чтения. Это означает, что вы не можете заменить какие-либо элементы базовой коллекции другими элементами. То есть вы не можете изменить ссылки на Person объектов, содержащихся в коллекции. Однако тип Person доступен не только для чтения, и, поскольку это ссылочный тип (т. е. class), вы можете изменять его члены по ссылке.

Есть два решения:

  • Используйте struct в качестве возвращаемого типа (это делает копию значения каждый раз, когда оно возвращается, поэтому исходное значение не будет изменено, что, кстати, может быть дорогостоящим)
  • Используйте свойства только для чтения для типа Person для выполнения этой задачи.
person mmx    schedule 11.12.2008
comment
Но вы можете преобразовать любой IEnumerable<T> обратно в T[], List<T> или любой другой тип, которым он на самом деле является, и изменить его элементы (сами, а не их свойства). - person Shimmy Weitzhandler; 25.11.2015
comment
@Shimmy Если мы собираемся получить технические сведения о том, что вы можете сделать, вы также можете использовать отражение для доступа к закрытым членам любого класса и делать все, что хотите. Тот факт, что вы можете что-то сделать (например, привести IEnumerable к его фактическому типу), не означает, что вы когда-либо должны это делать. - person Alex; 22.09.2016
comment
Тема о дизайне шрифтов, где рефлексия не в тему. - person Evgeny Gorbovoy; 15.07.2021

Вернуть новый экземпляр Person, который является копией p вместо самого p в Get(). Для этого вам понадобится метод для создания глубокой копии объекта Person. Это не сделает их доступными только для чтения, но они будут отличаться от исходного списка.

public IEnumerable<Person> Get()
{
    foreach (Person p in l1)
    {
        yield return p.Clone();
    }
}
person tvanfosson    schedule 11.12.2008

Они указывают не на одну и ту же коллекцию .Net, а на одни и те же Person объекты. Линия:

List<Person> p = new List<Person>(list1Instance.Get()); 

копирует все элементы Person из list1Instance.Get() в список p. Слово «копии» здесь означает копирование ссылок. Итак, ваш список и IEnumerable просто указывают на одни и те же Person объекты.

IEnumerable<T> по определению всегда доступен только для чтения. Однако объекты внутри могут быть изменяемыми, как в этом случае.

person Szymon Rozga    schedule 11.12.2008

Вы можете сделать глубокий клон каждого элемента в списке и никогда не возвращать ссылки на исходные элементы.

public IEnumerable<Person> Get()
{
  return l1
    .Select(p => new Person(){
      FirstName = p.FirstName,
      LastName = p.LastName
    });
}
person Amy B    schedule 11.12.2008

Если ваш объект person является реальным объектом, вам следует рассмотреть возможность использования неизменяемой версии.

 public class Person
 {
     public FirstName {get; private set;}
     public LastName {get; private set;}
     public Person(firstName, lastName)
     {
         FirstName = firstName;
         LastName = lastName;
     }
  }

Таким образом, невозможно изменить содержимое однажды созданного экземпляра, и поэтому не важно, чтобы существующие экземпляры повторно использовались в нескольких списках.

person AnthonyWJones    schedule 11.12.2008

IEnumerable<T> доступно только для чтения

p — это новая коллекция, которая не зависит от list1instance. Ваша ошибка заключается в том, что вы думали, что эта строка list[0].FirstName = "uf1";
изменит только один из списков, тогда как на самом деле вы изменяете объект Person.
Эти две коллекции различны, они просто имеют одни и те же элементы.
Чтобы доказать, что они разные, попробуйте добавить или удалить элементы из одного из списков, и вы увидите, что другой не затрагивается.

person DonkeyMaster    schedule 11.12.2008

Прежде всего, ваш список в вашем классе общедоступен, поэтому ничто не мешает кому-либо получить прямой доступ к самому списку.

Во-вторых, я бы реализовал IEnumerable и вернул бы это в свой метод GetEnumerator.

return l1.AsReadOnly().GetEnumerator();
person BFree    schedule 11.12.2008

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

Это выдает ошибку, если вы пытаетесь изменить поле (через свойство), поэтому «только для чтения». Если вы действительно хотите иметь возможность изменять значения, не затрагивая оригинал, ответ на клон выше лучше.

class  Person
{
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }


    public Person(string firstName, string lastName) {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

}

class PersonReadOnly : Person
{
    public override string FirstName { get { return base.FirstName; } set { throw new Exception("setting a readonly field"); } }
    public override string LastName { get { return base.LastName; } set { throw new Exception("setting a readonly field"); } }

    public PersonReadOnly(string firstName, string lastName) : base(firstName, lastName)
    {
    }
    public PersonReadOnly(Person p) : base(p.FirstName, p.LastName)
    {

    }

}

class List1
{
    public List<Person> l1 = new List<Person>();

    public List1()
    {
        l1.Add(new Person("f1", "l1"));
        l1.Add(new Person("f2", "l2"));
        l1.Add(new Person("f3", "l3"));
        l1.Add(new Person("f4", "l4"));
        l1.Add(new Person("f5", "l5"));
    }
    public IEnumerable<Person> Get()
    {
        foreach (Person p in l1)
        {
            yield return new PersonReadOnly(p);
        }
        //return l1.AsReadOnly(); 
    }

}  
class Program
{

    static void Main(string[] args)
    {
        List1 list1Instance = new List1();

        List<Person> p = new List<Person>(list1Instance.Get());           

        UpdatePersons(p);

        bool sameFirstName = (list1Instance.l1[0].FirstName == p[0].FirstName);
    }

    private static void UpdatePersons(List<Person> list)
    {
        // readonly message thrown
        list[0].FirstName = "uf1";
    }
person PJJ    schedule 28.11.2018