在 C# 中生成 List 的新实例
Generating a new instance of a List in C#
我在使用 C# 时遇到问题,如果我初始化某个列表,假设 List<T> exampleList
使用另一个预先存在的列表,假设 toModify 是这样的: List<T> exampleList = new List<T>(toModify)
。当我稍后修改 toModify 列表时,新创建的列表也会自行修改。如果它通过引用传递值,exampleList 的值不应该保持不变,因为它是从另一个生成的吗?
TLDR:当我更改第二个列表时,我使用另一个列表(第二个列表)初始化的列表的值会发生变化。我来自 Java 背景,不明白为什么会这样。我总是必须使用克隆吗?
是的。
您正在创建一个包含与旧列表相同项目的新列表。如果清除第一个列表,第二个列表中的项目将保留。
但是,如果您为第一个列表中的其中一项更改 属性,那么它就是第二个列表中的同一对象。
因此,两个列表都引用了内存中的相同项目。当您编写 list1[0].SomeProperty = 1
时,您正在使用与 list2
中相同的对象引用来更改它,因此更改会反映在第二个列表中。
有关如何克隆列表并为项生成新引用的信息,请查看此 SO Answer。
在下一行中:
List<T> exampleList = new List<T>(toModify)
你创建了一个 T 调用 List<T>
的构造函数的列表,它接受一个 IEnumerable<T>
类型的参数。有关后者的更多信息,请查看 here.
C# 中方法的参数默认 按值而不是按引用传递。它们可以通过引用传递,但您必须使用 ref
关键字在相应方法的签名中明确说明这一点,并在您调用此方法时再次使用相同的关键字。所以 toModify
按值传递给 List<T>
.
的构造函数
这有什么重要性?
在 C# 中类型可以分为两类(尽管所有类型都继承自 System.Object):
- 值类型
- 引用类型
当我们将值类型作为参数传递时,我们传递的是它的值的副本。我们对原始值或原始值的副本所做的每一项修改都不会相互反映。另一方面,当我们将引用类型作为参数传递时,我们传递的是该引用的副本。所以现在我们有两个指向内存中相同位置的引用(指针)。话虽如此,很明显,如果我们更改两个引用指向的对象的任何 属性,这将对它们都可见。
在你的情况下,这就是正在发生的事情。 toModify
是引用类型的列表(在引擎盖下你有一个数组,它的项目是对其他对象的引用)。因此,对初始列表 toModify
的项目的任何更改都会反映到您基于此列表构建的列表中。
您可以用来验证以上内容的一个简单示例如下:
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString() => $"X: {X}, Y: {Y}";
}
class Program
{
static void Main(string[] args)
{
var listA = new List<int> {1, 2, 3};
var listB = new List<int>(listA);
// Before the modification
Console.WriteLine(listA[0]); // prints 1
Console.WriteLine(listB[0]); // prints 1
listA[0] = 2;
// After the mofication
Console.WriteLine(listA[0]); // prints 2
Console.WriteLine(listB[0]); // prints 1
Console.ReadKey();
var pointsA = new List<Point>
{
new Point {X = 3, Y = 4},
new Point {X = 4, Y = 5},
new Point {X = 6, Y = 8},
};
var pointsB = new List<Point>(pointsA);
// Before the modification
Console.WriteLine(pointsA[0]); // prints X: 3, Y: 4
Console.WriteLine(pointsB[0]); // prints X: 3, Y: 4
pointsA[0].X = 4;
pointsA[0].Y = 3;
// After the modification
Console.WriteLine(pointsA[0]); // prints X: 4, Y: 3
Console.WriteLine(pointsB[0]); // prints X: 4, Y: 3
Console.ReadKey();
}
}
让我们用这个例子:
List<A> firstList = new List<A>()
{
new A() { Id = 3 },
new A() { Id = 5 }
};
List<A> secondList = new List<A>(firstList);
secondList[1].Id = 999;
Console.WriteLine(firstList[1].Id);
输出:999
这样做的主要原因是,即使我们创建了一个新的 List<T>
指向在堆上分配的新内存,它仍然可以使用指向相同对象的引用。
要创建指向具有相同值的新 (!) 对象的列表,我们需要以某种方式克隆这些元素,一种方法是使用 LINQ .Select()
方法来创建新对象,然后是 ToList()
复制列表本身的方法:
List<A> firstList = new List<A>()
{
new A() { Id = 3 },
new A() { Id = 5 }
};
List<A> secondList = firstList.Select(el => new A() { Id = el.Id }).ToList();
secondList[1].Id = 999;
Console.WriteLine(firstList[1].Id);
输出:5
我在使用 C# 时遇到问题,如果我初始化某个列表,假设 List<T> exampleList
使用另一个预先存在的列表,假设 toModify 是这样的: List<T> exampleList = new List<T>(toModify)
。当我稍后修改 toModify 列表时,新创建的列表也会自行修改。如果它通过引用传递值,exampleList 的值不应该保持不变,因为它是从另一个生成的吗?
TLDR:当我更改第二个列表时,我使用另一个列表(第二个列表)初始化的列表的值会发生变化。我来自 Java 背景,不明白为什么会这样。我总是必须使用克隆吗?
是的。
您正在创建一个包含与旧列表相同项目的新列表。如果清除第一个列表,第二个列表中的项目将保留。
但是,如果您为第一个列表中的其中一项更改 属性,那么它就是第二个列表中的同一对象。
因此,两个列表都引用了内存中的相同项目。当您编写 list1[0].SomeProperty = 1
时,您正在使用与 list2
中相同的对象引用来更改它,因此更改会反映在第二个列表中。
有关如何克隆列表并为项生成新引用的信息,请查看此 SO Answer。
在下一行中:
List<T> exampleList = new List<T>(toModify)
你创建了一个 T 调用 List<T>
的构造函数的列表,它接受一个 IEnumerable<T>
类型的参数。有关后者的更多信息,请查看 here.
C# 中方法的参数默认 按值而不是按引用传递。它们可以通过引用传递,但您必须使用 ref
关键字在相应方法的签名中明确说明这一点,并在您调用此方法时再次使用相同的关键字。所以 toModify
按值传递给 List<T>
.
这有什么重要性?
在 C# 中类型可以分为两类(尽管所有类型都继承自 System.Object):
- 值类型
- 引用类型
当我们将值类型作为参数传递时,我们传递的是它的值的副本。我们对原始值或原始值的副本所做的每一项修改都不会相互反映。另一方面,当我们将引用类型作为参数传递时,我们传递的是该引用的副本。所以现在我们有两个指向内存中相同位置的引用(指针)。话虽如此,很明显,如果我们更改两个引用指向的对象的任何 属性,这将对它们都可见。
在你的情况下,这就是正在发生的事情。 toModify
是引用类型的列表(在引擎盖下你有一个数组,它的项目是对其他对象的引用)。因此,对初始列表 toModify
的项目的任何更改都会反映到您基于此列表构建的列表中。
您可以用来验证以上内容的一个简单示例如下:
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public override string ToString() => $"X: {X}, Y: {Y}";
}
class Program
{
static void Main(string[] args)
{
var listA = new List<int> {1, 2, 3};
var listB = new List<int>(listA);
// Before the modification
Console.WriteLine(listA[0]); // prints 1
Console.WriteLine(listB[0]); // prints 1
listA[0] = 2;
// After the mofication
Console.WriteLine(listA[0]); // prints 2
Console.WriteLine(listB[0]); // prints 1
Console.ReadKey();
var pointsA = new List<Point>
{
new Point {X = 3, Y = 4},
new Point {X = 4, Y = 5},
new Point {X = 6, Y = 8},
};
var pointsB = new List<Point>(pointsA);
// Before the modification
Console.WriteLine(pointsA[0]); // prints X: 3, Y: 4
Console.WriteLine(pointsB[0]); // prints X: 3, Y: 4
pointsA[0].X = 4;
pointsA[0].Y = 3;
// After the modification
Console.WriteLine(pointsA[0]); // prints X: 4, Y: 3
Console.WriteLine(pointsB[0]); // prints X: 4, Y: 3
Console.ReadKey();
}
}
让我们用这个例子:
List<A> firstList = new List<A>()
{
new A() { Id = 3 },
new A() { Id = 5 }
};
List<A> secondList = new List<A>(firstList);
secondList[1].Id = 999;
Console.WriteLine(firstList[1].Id);
输出:999
这样做的主要原因是,即使我们创建了一个新的 List<T>
指向在堆上分配的新内存,它仍然可以使用指向相同对象的引用。
要创建指向具有相同值的新 (!) 对象的列表,我们需要以某种方式克隆这些元素,一种方法是使用 LINQ .Select()
方法来创建新对象,然后是 ToList()
复制列表本身的方法:
List<A> firstList = new List<A>()
{
new A() { Id = 3 },
new A() { Id = 5 }
};
List<A> secondList = firstList.Select(el => new A() { Id = el.Id }).ToList();
secondList[1].Id = 999;
Console.WriteLine(firstList[1].Id);
输出:5