如何在.NET中修改结构体的List<T>?

4

为什么我无法修改列表项?

struct Foo 
{
    public string Name;
}

Foo foo = new Foo();
foo.Name = "fooNameOne";

List<Foo> foos = new List<Foo>();
foos.Add(foo);

// Cannot modify the return value of 
// 'List<Foo>.this[int]' because it is not a variable   
//foos[0].Name = "fooNameTwo";

Foo tempFoo = foos[0];
tempFoo.Name = "fooNameTwo";

Console.WriteLine(foos[0].Name); // fooNameOne

编辑
我想保留Foo的结构。我该怎么做?foos[0] = tempFoo?这样一个赋值操作有点复杂了吧?


7
从根本上讲,这是另一个可变结构体有害的案例。坚决说不。 - Jon Skeet
3个回答

12
因为 Foo 是一个 struct,不是一个对象,因此它是值类型而不是引用类型。当您将值类型添加到 List 中时,会创建一个副本(与引用类型不同,在引用类型中,列表中的条目是指向原始对象的引用)。从 List 中提取实例也是一样的。当您执行 Foo tempFoo = foos[0] 时,实际上是在创建 foos[0] 元素的另一个副本,因此您修改的是副本而不是 List 中的元素。
foos[0].Name = "fooNameTwo";

由于同样的原因,这会给你一个错误。List的索引器仍然是返回值的函数。由于您正在使用值类型,因此该函数将返回一个副本并将其存储在堆栈上供您使用。一旦您尝试修改该副本,编译器就会看到您正在进行某些可能产生意外结果的操作(您的更改不会反映在List中的元素中),从而报错。

正如Jon Skeet所评论的那样...这是可变结构体是邪恶的另一个原因(如果您想要更多细节,请查看此SO问题:为什么可变结构体是邪恶的?)。

如果将Foo作为类而不是结构体,则会获得您想要的行为。


"因为它不是一个变量,所以无法修改 'List<Foo>.this[int]' 的返回值" - serhio
1
@serhio - 同样的原因。 List<T> 上的索引器实际上是一个函数。该函数返回值的副本(因为它不是引用类型)并将其存储在堆栈上。当你试图修改它时,你仍然在修改复制品...而编译器会抛出错误。在这里查看更多细节:http://generally.wordpress.com/2007/06/21/c-list-of-struct/ - Justin Niessner
如果我有一个 Drawing.Point 的列表怎么办?我不能将 Point 重新定义为一个类... - serhio
@serhio - 同样的概念适用。定义一个新的点并替换旧的点。 - Justin Niessner
@serhio - 如果你真的想了解Points,你应该在你的例子中使用Points。有人会更快地给你答案(并从我这里得到相同的基本解释),就像Dan Tao的评论一样。只是让你知道下一次... - Justin Niessner
显示剩余3条评论

2
事情是这样的。
你说了这句话:
“我想留下 Foo 的结构。我该怎么做?foos [0] = tempFoo?只是为了一个赋值有点复杂?!”
没错,就是这样。你需要的是一种赋值,而不是一种修改。值类型(结构体)通常应该被视为
int为例。如果你有一个名为intsList<int>,你会如何更改ints [0]的值?
大概是这样吧?
ints[0] = 5; // assignment

注意,没有办法做到像这样的事情:
ints[0].ChangeTo(5); // modification?

这是因为Int32是一个不可变的结构体。它被设计成作为一个来处理,不能被改变(所以一个int变量只能被赋值给一个新的值)。
你的Foo结构体是一个令人困惑的情况,因为它可以被改变。但由于它是一个值类型,只有副本被传递(与我们在日常生活中处理的所有值类型一样,如intdoubleDateTime等)。因此,除非它被通过引用传递给你(使用方法调用中的ref关键字),否则你不能从远处改变实例。
因此,简单的答案是,是的,要改变List<Foo>中的Foo,你需要将其赋值给一个新的值。但你真的不应该有一个可变的结构体。
免责声明:与几乎所有您可能获得的建议一样,在任何事情上,这都不是100%的硬性规定。非常有技巧的开发人员Rico Mariani为了好的原因编写了可变的Point3d结构体,他在博客上解释了这些原因。但这是一个非常知识渊博的开发人员知道他正在做什么的例子;通常作为编写值类型与引用类型的标准方法,应该使值类型成为不可变的。
针对你的评论:当你处理一个可变结构体,比如 Point 时,基本上你需要像这样做:
Point p = points[0];
p.Offset(0, 5);
points[0] = p;

或者,另外一种选择是:
Point p = points[0];
points[0] = new Point(p.X, p.Y + 5);

这句话的英文原意是:“我不会做...的原因是...”。
points[0] = new Point(points[0].X, points[0].Y + 5);

“…这里你复制了points[0]的值两次。要记住,通过索引访问this属性基本上是一个方法调用。所以这段代码实际上是在执行这个操作:”
points.set_Item(0, new Point(points.get_Item(0).X, points.get_Item(0).Y + 5);

请注意过多地调用get_Item(没有充分的理由而进行额外复制)。

那么,如果我的Foo应该是一个点(Point),我需要这样做:points[0] = new Point(points[0].X, points[0].Y + 5) - serhio
2
@serhio: 我其实不会那样做,因为每次调用 points[0] 你都会得到一个新的副本。我会这样做: Point p = points[0]; p.Offset(0, 5); points[0] = p; - Dan Tao
我认为编译器应该优化 points[0].X, points[0].Y 只调用一次函数。 - serhio
@serhio:它应该吗?那似乎非常可疑。如果我为我的集合类编写索引器以产生副作用呢?(这几乎肯定是我做出的可怕决定,但我可以这样做。)同样,由于这些属性访问器基本上是方法调用,所以它们不应该按照您描述的方式进行“优化”。除非我漏掉了什么? - Dan Tao

0
因为Foo是值类型,
所以应该使用这个:
Foo tempFoo = foos[0];
tempFoo.Name = "fooNameTwo";

做这个:
Foo tempFoo;
tempFoo = "fooNameTwo";
foos[0] = tempFoo;

@serhio:使用class而不是struct。在.NET中,struct几乎永远不是你真正想要的,它与C/C++的结构体不同。 - Dirk Vollmar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接