在 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