Изменение значения одного элемента в коллекции влияет на все повторяющиеся элементы.

Кажется, у меня странная проблема, из-за которой каждый раз, когда я пытаюсь изменить значение элемента в коллекции, это влияет на все остальные, которые содержат те же начальные значения.

Пример ниже:

public class Product : ICloneable
{
  public int Id { get; set; }
  public string Name { get; set; }
  public int Quantity { get; set; }

  public Product()
  {
    Id = 0;
    Quantity = 0;
  }

  public Clone()
  {
    return (Product)this.MemberwiseClone();
  }
}

...

private static IEnumerable<Product> GetProducts(Product product, int quantity)
{
  for (int i = 0; i < quantity; i++)
  {
    yield return product.Clone();
  }
}

...

IEnumerable<Product> myProducts = Enumerable.Empty<Product>();
Product product1 = new Product() { Id = 0, Name = "Buzz Cola" };
Product product2 = new Product() { Id = 1, Name = "Choco Bites" };

myProducts = myProducts.Concat(GetProducts(product1, 2));
myProducts = myProducts.Concat(GetProducts(product2, 1));

//Now set the quantity of the first product to be 1.
myProducts.ElementAt(0).Quantity = 1;

foreach(Product product in myProducts)
{
  Console.WriteLine(string.Format("Id: {0}  Quantity: {1}", product.Id, product.Quantity));
}

//Output:
//Id: 0  Quantity: 1
//Id: 0  Quantity: 1 //NO!
//Id: 1  Quantity: 0

Любые идеи?

Большое спасибо!

Обновление Я обновил вопрос, чтобы включить Clone(), как было предложено. Однако выход остается прежним.


person Dan Atkinson    schedule 24.08.2009    source источник
comment
Как вы меняете предмет?   -  person Adam Wright    schedule 24.08.2009
comment
myProducts.ElementAt(0).Количество = 1;   -  person Dan Atkinson    schedule 24.08.2009
comment
myProducts не имеет свойства Is или Quantity. Вы имеете в виду product.Id?   -  person dtb    schedule 24.08.2009
comment
Я также попытался сделать цикл, увеличивая каждое количество на единицу, и в итоге я получил Buzz Cola с количеством 2!   -  person Dan Atkinson    schedule 24.08.2009
comment
Я запустил вашу исправленную версию, и она выводит ожидаемые количества (1, 0, 0)   -  person dtb    schedule 24.08.2009
comment
@dtb — то же самое, VS2008, .NET Framework 3.5. Дэн, есть какой-то важный код, который ты нам не показываешь?   -  person Gavin    schedule 24.08.2009
comment
@Gavin: Да, я ошибся в том, как создаются настоящие продукты. Я переписал вопрос, чтобы отразить это.   -  person Dan Atkinson    schedule 24.08.2009
comment
Вы добавляете один и тот же продукт дважды, поэтому существует только один экземпляр, и изменения становятся видимыми для обоих элементов в списке.   -  person Daniel Brückner    schedule 24.08.2009
comment
@Daniel: Есть ли лучший способ сделать это, чтобы предотвратить копирование ссылки на оба объекта? Я попытался снова создать объекты и присвоить каждому идентификатор и имя, но это не имеет значения.   -  person Dan Atkinson    schedule 24.08.2009
comment
Мой последний комментарий немного странный, но, наверное, все поняли, что я хотел выразить - есть один экземпляр, и он добавлен дважды, а не один экземпляр, потому что он добавлен дважды. @ Дэн лучше? Здесь у вас уже был рабочий код — просто создайте и добавьте три продукта. Чего вы пытаетесь достичь? Этакий копи-конструктор?   -  person Daniel Brückner    schedule 24.08.2009


Ответы (6)


Вам нужно что-то вроде метода клонирования или конструктора копирования.

public class Product
{
  public int Id { get; set; }
  public string Name { get; set; }
  public int Quantity { get; set; }

  public Product()
  {
      this.Id = 0;
      this.Name = null;
      this.Quantity = 0;
  }

  public Product(Product product)
  {
      this.Id = product.id;
      this.Name = product.Name;
      this.Quantity = product.Quantity;
  }
}

IList<Product> myProducts = new List<Product>();

Product product1 = new Product() { Id = 0, Name = "Buzz Cola" };
Product product2 = new Product() { Id = 1, Name = "Choco Bites" };
Product product3 = new Product(product1); // Use copy-constructor.

myProducts.Add(product1);
myProducts.Add(product2);
myProducts.Add(product3);

myProducts[0].Quantity = 1;

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

Просто отметим, что этот код по-прежнему имеет очень неприятный вкус — вы создаете разные экземпляры продукта с одинаковыми идентификаторами. Я могу только догадываться, но хотите ли вы создать что-то вроде корзины для покупок, в которой элементы корзины имеют количество и продукт? Если да, вам действительно следует подумать о разделении класса продуктов на два класса. И снова подумайте о доступности вашей недвижимости.

public class Product
{
  public Int32 Id { get; private set; }
  public String Name { get; private set; }
}

public class ShoppingCartItem
{
  public Product Product { get; private set; }
  public Int32 Quantity { get; set; }
}

public class ShoppingCart
{
  public IList<ShoppingCartItem> Items { get; private set; }
}

Это решает ваши текущие проблемы, потому что больше нет необходимости в клонировании продуктов.

person Daniel Brückner    schedule 24.08.2009
comment
Даниэль, я обновил GetProducts() с помощью yield return new Product(product); и создали конструктор копирования, но результат все тот же. Я обновляю один, и оба изменяют свои количества. - person Dan Atkinson; 24.08.2009
comment
Проблема в следующем. Когда установлено новое количество, запрос оценивается, и новый продукт создается путем клонирования или вызова конструктора копирования, и новый продукт модифицируется. Когда коллекция печатается, запрос переоценивается, и продукт снова клонируется или копируется, поэтому обновление теряется, и все количества равны нулю. Это другая ошибка, чем исходная. - person Daniel Brückner; 24.08.2009

Product — это ссылочный тип, и ваш метод GetProducts просто дает несколько ссылок на один и тот же объект Product.

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

person LukeH    schedule 24.08.2009
comment
Есть ли способ изменить эту ссылку, чтобы она относилась только к одному? - person Dan Atkinson; 24.08.2009
comment
Вы можете либо клонировать объект Product в методе GetProducts, либо объявить Product как структуру, а не класс. - person Thomas Levesque; 24.08.2009
comment
Я изменил GetProducts() так, чтобы он выполнял: yield return product.Clone(); и добавили Clone() в Product с return (Product)this.MemberwiseClone(); Увы, результат все тот же. - person Dan Atkinson; 24.08.2009
comment
Я обновил вопрос с предложенными изменениями Clone(). - person Dan Atkinson; 24.08.2009

Я предполагаю, что он дважды ссылается на ваш первый экземпляр с ID = 0 вместо двух отдельных экземпляров, как вы ожидаете.

Попробуйте изменить идентификатор третьего экземпляра с 0 -> 2 и посмотрите, «исправляет» ли это его.

person Beth    schedule 24.08.2009
comment
Это также устанавливает первое значение 2. - person Dan Atkinson; 24.08.2009
comment
Тогда ваш пример в вашем вопросе не отражает ваш фактический код. Ваш пример работает по назначению и дает ожидаемые количества. - person dtb; 24.08.2009
comment
Я переделал код. Это не отражало фактический код, вы правы. Проблема заключалась в том, что я пропустил GetProducts(), дважды передавая ссылку на один и тот же объект. - person Dan Atkinson; 24.08.2009

и myProducts.ElementAt(0), и myProducts.ElementAt(1) будут ссылаться на один и тот же объект.

Не уверен, что это лучший способ исправить это: может быть, прежде чем добавлять в список, проверьте, есть ли у вас уже ссылка на объект? если вы это сделаете, глубоко клонируйте объект и вставьте его...

person BigBlondeViking    schedule 24.08.2009

После исправления нескольких опечаток в цикле WriteLine (скопируйте/вставьте рабочий код) ошибка не воспроизводится, мой вывод:

Id: 0  Quantity: 1
Id: 1  Quantity: 0
Id: 0  Quantity: 0

После изменений:

Вы создаете только 2 экземпляра, и поэтому вы страдаете от очень простого факта, что у вас есть 3 ссылки, но только 2 экземпляра. И вывод такой, какой должен быть. Будет немного понятнее, если вы также напечатаете свойство Name.

Но то, что вы, по-видимому, хотите, где-то в этой очень сложной истории Enumerator/Concat, — это клонировать ваши продукты.

Чарли Солтс может восстановить свой ответ, он был прав.

person Henk Holterman    schedule 24.08.2009
comment
Я переписал вопрос, так как не отразил истинный код. - person Dan Atkinson; 24.08.2009
comment
@Henk: я обновил вопрос с помощью Clone(). Однако результат тот же. - person Dan Atkinson; 24.08.2009
comment
Дэн, лучше сначала подумайте о том, что вы на самом деле хотите, Clone() и IClonable полны ловушек, особенно для тех, кто все еще борется с типами значений/ссылок. В: Должен ли клон иметь тот же идентификатор, что и оригинал? - person Henk Holterman; 24.08.2009

Вместо этого используйте индексатор.

myProducts[0].Quantity = 1

person Chrisb    schedule 24.08.2009
comment
IEnumerable не предоставляет индексатора. - person Daniel Brückner; 24.08.2009
comment
Я мог бы поклясться, что он использовал список. Извиняюсь. Код вроде меняется. - person Chrisb; 24.08.2009
comment
Дэн раньше использовал список, но с самого начала переменная имела тип IEnumerable‹T›, поэтому вы не могли получить доступ к индексатору без преобразования вниз. - person Daniel Brückner; 24.08.2009