为什么此方法不生成列表的深层副本?[复制]

Why doesn't this method produce a deep copy of the List? [duplicate]

提问人:Tomi Tsolov 提问时间:9/18/2023 更新时间:9/19/2023 访问量:108

问:

在此线程中:
如何创建 List<T> 的新深拷贝(克隆)?

提问者的例子是如何创建其列表的深层副本。我没有看到任何人提供关于为什么它不起作用的见解。为什么新列表表现为对旧列表的引用?(或者至少是元素)。

我了解引用在 C++ 中的工作方式,并且我认为它们在 C# 中的行为方式是相似的。但也许我错过了什么。我还是 C# 的新手。

下面是您可以粘贴和运行的简化版本:

namespace OOPtest1
{
    internal class Program
    {
        public class Book
        {
            public string title = "";

            public Book(string title)
            {
                this.title = title;
            }
        }
        
       
        static void Main(string[] args)
        {
            //InitializeComponent();

            List<Book> books_1 = new List<Book>();
            books_1.Add(new Book("One"));
            books_1.Add(new Book("Two"));
            books_1.Add(new Book("Three"));
            books_1.Add(new Book("Four"));

            List<Book> books_2 = new List<Book>(books_1);

            books_2[0].title = "Five"; // this changes the books_1 element as well
            books_2[1].title = "Six"; // this changes the books_1 element as well

        }   
    
    }
}

我以为这会表现得像一个完全不同的集合。

C# 深拷贝

评论

4赞 Daniel A. White 9/18/2023
它是相同引用的不同集合
2赞 Guru Stron 9/18/2023
books_1实际上是对实例的引用的集合,因此构造函数(请参阅源代码)只会将它们复制到新位置。BookList
0赞 Jimi 9/18/2023
引用类型(C# 参考)
0赞 JonasH 9/18/2023
C# 引用在某种程度上等同于 C++ 指针。如果复制指针的 c++ 向量,则会得到完全相同的行为。为了避免此类问题,请尽可能使用不可变对象。C# 记录使不可变性更容易。
0赞 Ghassen 9/18/2023
您必须在 Book 类中创建一个 DeepCopy 方法,在里面创建一个 book 实例并复制属性

答:

1赞 Matthew Watson 9/18/2023 #1

首先,请注意 这是一个引用类型,因此对该类型实例的引用存储在 和 中。Bookbooks_1books_2

当您复制 to 的内容时,您复制了对对象的引用,而不是对象本身的内容books_1books_2Book

因此,在复制之后,每个列表的第一个元素的情况如下:

books_1[0]----\
               \ 
             {Book0}   
               /
books_2[0]----/

现在你应该能够看到,当你改变时,你将改变 - 并且因为那个对象被引用,所以它也会为该引用而改变它 - 因为它引用的是同一个对象。books_1[0].titleBook0.titlebooks_2[0]

如果您认为引用更像是 C++ 指针,您应该会得到它。

评论

0赞 Peter - Reinstate Monica 9/18/2023
您绝对可以将 C# 引用视为 C++ 引用;除非您出于任何原因无法将它们存储在数组中。(显然,C 和 C++ 类型系统被破坏了——变量不应该与文字具有相同的类型,Algol68 做对了:是一个 int,是一个 ref int。5i
0赞 Tomi Tsolov 9/19/2023
非常感谢,这是有道理的。我不知道对象是引用类型。
0赞 Matthew Watson 9/19/2023
@TomiTsolov 需要明确的是:在 C# 中,您可以有引用类型(类)和值类型(结构)。
0赞 Joel Coehoorn 9/18/2023 #2

由于这里似乎对 C++ 有一些熟悉,我想开始谈论 vs 类型。这是一个漫长的时间,但 IIRC 在 C++ 中 a 和 之间的主要区别是访问器默认值 ( vs )。在 C# 中,两者的默认值都是 ,它更接近于 ,但 (值类型) 与 (引用类型) 在语义上存在差异。强烈建议尽可能多地使用类,并保留小且不可变的类型。structclassstructclasspublicprivateinternalprivatestructclassstruct

我从这里开始,因为它将有助于理解接下来的一些内容,并且因为您可以通过将类型定义为 而不是 .但同样:不建议这样做。Bookstructclass

更进一步,并且知道这是一个引用类型,C# 不会假设复制引用类型对象的内存数据就足以深度复制该对象。想想开放流、文件句柄、com/串行端口、数据库连接、对其他内存/对象的引用、需要唯一 guid 成员的项目等。如果需要能够进行深度复制,则必须提供代码才能自己执行此操作,并且没有对等效于 C++ 复制构造函数的语言支持。Book

因此,当基于 创建列表时,虽然它确实复制了 中的内部集合的副本,但只复制引用。中的每个项目最初都与 中的等效项目引用相同的对象books_2books_1books_1books_2books_1

我说“最初”,因为您当然可以在列表中添加和删除整个项目而不会影响.这样,您就拥有了初始列表的副本;列表是它自己的对象,而不仅仅是对与 相同的列表的引用。但是,由于该列表是针对引用类型的,因此您只有引用的副本,并且基础对象是共享的。books_2books_1books_2books_1

因此,在原始代码中,如果要更改 和 的项,可以这样做:books_2[0]books_2[1]

books_2[0] = new Book("Five"); 
books_2[1] = new Book("Six"); 

这将完全保持不变。但是,和 索引仍将在两个列表之间共享相同的对象。books_1[2][3]

如果你真的想完全深度复制整个列表,你可以这样做:

var books_2 = books_1.Select(b => new Book(b.title)).ToList();

评论

0赞 Tomi Tsolov 9/19/2023
谢谢,我现在明白了。此外,了解更新新集合的替代方法非常有用。