tags:

views:

112

answers:

5

I seem to be having an odd issue whereby every time I try to change a value of an item in a collection, it affects all others that contain the same initial values.

An example is below:

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

Any ideas?

Many thanks!

Update I have updated the question to include the Clone() as suggested. The output is still the same however.

+1  A: 

I'm guessing it's referencing your first instance with ID=0 twice instead of two separate instances like you expect.

Try changing the ID of the third instance from 0 -> 2 and see if that 'fixes' it.

Beth
Doing this sets the first to 2 as well.
Dan Atkinson
Then your example in your question doesn't reflect your actual code. Your example works as intended and yields the expected quantities.
dtb
I have redone the code. It didn't reflect the actual code, you are correct. The problem was that I had missed the GetProducts() passing the same object reference twice.
Dan Atkinson
A: 

Use the indexer instead.

myProducts[0].Quantity = 1

Chrisb
What's the difference?
Gavin Schultz-Ohkubo
IEnumerable does not provide an indexer.
Daniel Brückner
I could have sworn he was using a list. Sorry. The code seems to be changing.
Chrisb
I have changed the code, but it was never a List. :)
Dan Atkinson
My bad. Sorry about the incorrect comment :)
Chrisb
Dan used a list before, but the variable was of type IEnumerable<T> right from the begining, hence you could not access the indexer without a downcast.
Daniel Brückner
+3  A: 

Product is a reference type and your GetProducts method just yields multiple references to the same Product object.

That's why updating one instance updates any others - they're all references to the same object.

LukeH
Is there any way to change this reference so that it only relates to one?
Dan Atkinson
You can either clone the Product object in the GetProducts method, or declare Product as a struct rather than a class
Thomas Levesque
I have altered GetProducts() so that it does: yield return product.Clone(); and have added the Clone() to Product with return (Product)this.MemberwiseClone(); Alas, the result is still the same.
Dan Atkinson
I have updated the question with the Clone() changes suggested.
Dan Atkinson
+1  A: 

both myProducts.ElementAt(0) and myProducts.ElementAt(1) will reference the same object.

Not sure the best way to fix this: Maybe before you add to a list, check to see if you have a reference to the object already? if you do, deep clone the object and insert it...

BigBlondeViking
+1  A: 

You need something like a clone method or a copy constructor.

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;

And now everything should be fine. You can use this together with your cloniung method to produce a large number of clones at once.

Just to note, this code has still a very bad taste - you are creating different product instances with equal ids. I can just guess, but do you want to build something like a shopping cart with cart items having a quantity and a product? If yes, you should really think about splitting the product class into two classes. And think about the accessibility of your properties aganin.

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; }
}

This solves your current problems because there is no longer a need for cloning products.

Daniel Brückner
Daniel, I have updated the GetProducts() with yield return new Product(product); and have created a copy constructor, but the result is still the same. I update one, and both get their quantities changed.
Dan Atkinson
The problem is the following. When the new quantity is set, the query is evaluated and a new product is created by cloning or calling the copy constructor and the new product is modified. When the collection is printed, the query is reevaluated and the product is cloned or copy-constructed again, hence the update is lost and all quantities are zero. This is a different bug then the original one.
Daniel Brückner