为什么不能修改元组类型列表元素的值?
c#
在 C# 8.0 中,我可以通过访问字段名称直接修改元组内的值:
(string name, int score) student = ("Tom", 100);
student.name = "Jack";
Console.WriteLine(student);
我可以修改列表元素的属性如下:
var list = new List<Student>(); // assume I have a Student class which has a Name property
list.Add(new Student { Name = "Tom" });
list[0].Name = "Jack";
Console.WriteLine(list[0]);
但是为什么我不能像这样修改元组类型元素的值呢?
var list = new List<(string name, int score)>();
list.Add(("Tom", 100));
list[0].name = "Jack"; // Error!
Console.WriteLine(list[0]);
回答
元组 ( ValueTuple) 是一个结构体。您实际上会收到元组的副本,而不是像您的示例那样返回对该值的引用。Student
对该副本的更改不会反映在列表中,并且会被丢弃。编译器足够聪明,可以识别这一点并阻止您这样做。
如果它确实编译了,它将类似于以下内容:
var list = new List<(string name, int score)>(); list.Add(("Tom", 100));
var copy = list[0];
copy.name = "Jack";
Console.WriteLine(copy.name); // Jack
Console.WriteLine(list[0].name); // still Tom
如果您没有正确使用可变结构,它们可能会很危险。编译器只是在做它的工作。
您可以使用以下方法解决此问题:
var list = new List<(string name, int score)>(); list.Add(("Tom", 100));
var copy = list[0];
copy.name = "Jack";
list[0] = copy; // put it back
Console.WriteLine(copy.name); // Jack
Console.WriteLine(list[0].name); // Jack
在线试用
如果您使用数组(string, int)[]而不是 a List<(string, int)>,由于数组元素访问的工作方式,这不是问题:
var arr = new (string name, int score) [] { ( "Tom", 10 ) };
arr[0].name = "Jack";
Console.WriteLine(arr[0].name); // Jack
在线试用
此行为不是List您的元组类型所独有的。您将在元素为值类型的任何集合中遇到此问题(当然,除非它们提供ref元素访问器)。
请注意,当具有通过方法调用发生变异readonly的可变值类型的字段时,也存在类似的问题。这可能会更加阴险,因为没有发出错误或警告:
struct MutableStruct {
public int Val;
public void Mutate(int newVal) {
Val = newVal;
}
}
class Test {
private readonly MutableStruct msReadonly;
private MutableStruct msNormal;
public Test() {
msNormal = msReadonly = new MutableStruct(){ Val=5 };
}
public void MutateReadonly() {
Console.WriteLine(msReadonly.Val); // 5
msReadonly.Mutate(66); // defensive copy!
Console.WriteLine(msReadonly.Val); // still 5!!!
}
public void MutateNormal() {
Console.WriteLine(msNormal.Val); // 5
msNormal.Mutate(66);
Console.WriteLine(msNormal.Val); // 66
}
}
new Test().MutateReadonly();
new Test().MutateNormal();
在线试用
ValueTuple是对框架和语言的一个很好的补充。但是有一个原因,您经常会听到[Mutable] structs are evil.在大多数情况下您不应该达到这些限制。如果您发现自己经常陷入这种模式,我建议转向 a record,它是一种引用类型(因此不会遇到这些问题)并且可以简化为类似元组的语法。
回答
可变值类型是邪恶的,很难理解为什么这会打印“Tom”而不是“Jack”:
(string name, int score) student = ("Tom", 100);
(string name, int score) student2 = student;
student.name = "Jack";
Console.WriteLine(student2);
原因是你总是创建一个副本。因为这并不明显,您应该避免使用可变值类型。为了避免人们陷入那个陷阱,编译器只允许直接通过属性修改对象(如上)。但是,如果您尝试通过方法调用来执行此操作,则会收到编译器错误“无法修改...的返回值,因为它不是变量”。
所以这是不允许的:
list[0].name = "Jack";
它将创建 的新副本ValueTuple,分配一个值,但不会在任何地方使用或存储它。
这会编译,因为您将它分配给一个新变量并通过属性修改它:
(string name, int score) x = list[0];
x.name = "Jack"; // Compiles
所以它编译但再次给你一个令人惊讶的结果:
Console.WriteLine(list[0]); // Tom
在此处阅读更多相关信息:不要定义可变值类型