Disparity between value semantics behavior with List< struct > vs. array of struct
-
I may be having dense programmer moment, but I just spent an hour debugging something that came down to a really unexpected (by me, at least) behavior with List<> vs. array. If you create a struct with a method that modifies a member, and invoke that method on an instance of the struct that resides within an array, the array element is modified as you would expect. But if you perform the identical operation on the struct that is contained in a List, the item in the List is NOT modified. The following program illustrates this...the effect is observable in VS 9 and 10. Does this seem correct or reasonable? It certainly doesn't adhere to one of my favorite principles of language design: the principle of least surprise! thanks, jim
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;namespace TestByValue
{
class Program
{
static void Main(string[] args)
{
Foo[] aArray = new Foo[3];
aArray[0].Increment();Console.WriteLine("aArray\[0\].x is {0} and should be {1}", aArray\[0\].x, 1); Debug.Assert(aArray\[0\].x == 1); List<Foo> aList = new List<Foo>(); aList.Add(new Foo()); aList\[0\].Increment(); Console.WriteLine("aList\[0\].x is {0} and should be {1}", aList\[0\].x, 1); Debug.Assert(aList\[0\].x == 1); // This Assert fires } struct Foo { public int x; public void Increment() { x++; } } }
}
-
I may be having dense programmer moment, but I just spent an hour debugging something that came down to a really unexpected (by me, at least) behavior with List<> vs. array. If you create a struct with a method that modifies a member, and invoke that method on an instance of the struct that resides within an array, the array element is modified as you would expect. But if you perform the identical operation on the struct that is contained in a List, the item in the List is NOT modified. The following program illustrates this...the effect is observable in VS 9 and 10. Does this seem correct or reasonable? It certainly doesn't adhere to one of my favorite principles of language design: the principle of least surprise! thanks, jim
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;namespace TestByValue
{
class Program
{
static void Main(string[] args)
{
Foo[] aArray = new Foo[3];
aArray[0].Increment();Console.WriteLine("aArray\[0\].x is {0} and should be {1}", aArray\[0\].x, 1); Debug.Assert(aArray\[0\].x == 1); List<Foo> aList = new List<Foo>(); aList.Add(new Foo()); aList\[0\].Increment(); Console.WriteLine("aList\[0\].x is {0} and should be {1}", aList\[0\].x, 1); Debug.Assert(aList\[0\].x == 1); // This Assert fires } struct Foo { public int x; public void Increment() { x++; } } }
}
Normally in C# most things are passed by reference so when you access the item in your list and call
Increment
on it you modify the actual object in the list. However in C# structs are value types (like anInt
for example) and when they are passed around what you actually get are copies. So if you had a list of Ints and you accessed them in your list you wouldn't get the actual object from the list, you just get a copy that you can do whatever you like with. If you modified the Int you'd have to actually set it back to the list to actually make any changes:List myList = new List();
myList.Add(5);
myList.Add(2);
//myList == {5, 2}int number = myList[1];
number += 3;
//myList == {5, 2}, like you would expect
myList[1] = number;
//myList == {5, 5}With a normal array you are actually accessing the items directly so in your case they get modified as expected, but with a generic list the array subscript operator is the same as calling a function like
int GetAtIndex(int i)
{
return theActualList[i];
}And because the struct isn't passed by reference what you end up with is a copy of what's stored in the list and that's what you're actually modifying. If you wanted it to work the same for both then you could simply change your
struct
for aclass
http://msdn.microsoft.com/en-us/library/ms173109.aspx[^] Well I hope that makes sense, it's rather late and I'm off to bed now ;P And I also hope I haven't made and blindingly obvious mistakes.My current favourite quote is: Punch them in the face, see what happens!
-SK Genius
modified on Thursday, June 3, 2010 11:28 PM
-
Normally in C# most things are passed by reference so when you access the item in your list and call
Increment
on it you modify the actual object in the list. However in C# structs are value types (like anInt
for example) and when they are passed around what you actually get are copies. So if you had a list of Ints and you accessed them in your list you wouldn't get the actual object from the list, you just get a copy that you can do whatever you like with. If you modified the Int you'd have to actually set it back to the list to actually make any changes:List myList = new List();
myList.Add(5);
myList.Add(2);
//myList == {5, 2}int number = myList[1];
number += 3;
//myList == {5, 2}, like you would expect
myList[1] = number;
//myList == {5, 5}With a normal array you are actually accessing the items directly so in your case they get modified as expected, but with a generic list the array subscript operator is the same as calling a function like
int GetAtIndex(int i)
{
return theActualList[i];
}And because the struct isn't passed by reference what you end up with is a copy of what's stored in the list and that's what you're actually modifying. If you wanted it to work the same for both then you could simply change your
struct
for aclass
http://msdn.microsoft.com/en-us/library/ms173109.aspx[^] Well I hope that makes sense, it's rather late and I'm off to bed now ;P And I also hope I haven't made and blindingly obvious mistakes.My current favourite quote is: Punch them in the face, see what happens!
-SK Genius
modified on Thursday, June 3, 2010 11:28 PM
Thanks, SK...that's very clear, though I do think that having anObject[i].Increment() behave differently according to whether anObject is an array of Foo vs. List when Foo is a struct is error prone. I actually think I would prefer that array indexing behaved the same way...that is, the indexer returns a copy. Now that I've had time to sleep on it, I remember that a few years ago I fought through creating an indexer for my own particular struct and came to realize that I had no choice but to return a copy...so I was getting the exact same semantics as occurs with List. Maybe I won't fall into the same trap again in a couple of years. But I wouldn't count on it! :laugh: cheers, jim