如何创建一个 List<T> 的新深拷贝(克隆)?

95

在下面的代码片段中,

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace clone_test_01
{

    public partial class MainForm : Form
    {

        public class Book
        {
            public string title = "";

            public Book(string title)
            {
                this.title = title;
            }
        }


        public MainForm()
        {
            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";
            books_2[1].title = "Six";

            textBox1.Text = books_1[0].title;
            textBox2.Text = books_1[1].title;
        }
    }

}

我使用Book对象类型创建一个List<T>,并向其中添加一些项目,并为它们赋予唯一的标题(从“one”到“five”)。

然后我创建List<Book> books_2 = new List<Book>(books_1)

从这一点上开始,我知道这是列表对象的克隆,但是book_2中的书籍对象仍然是books_1中书籍对象的引用。这通过更改books_2的前两个元素,并在TextBox中检查相同元素的book_1得到证明。

books_1[0].title和books_2[1].title确实已经更改为books_2[0].title和books_2[1].title的新值。

现在的问题是:

如何创建一个新的List<T>的硬拷贝?想法是books_1books_2变得完全独立。

我对Microsoft没有像Ruby那样提供clone()方法的简洁、快速和易于使用的解决方案感到失望。

真正令人敬畏的帮助方式是使用我的代码并将其修改为可工作的解决方案,以便可以编译和运行。我认为这将真正有助于试图理解此问题的新手所提供的解决方案。

编辑:请注意,Book类可能更复杂,并且具有更多属性。我试图保持简单。


类似于CopyTo的东西? - K-ballo
1
这种类型的复制通常被称为深拷贝。如果你觉得“硬拷贝”在你的情况下是不同的东西 - 请撤销我的编辑并添加您对该术语的定义。 - Alexei Levenkov
6
“硬拷贝”指的是将信息打印在实体纸张上。 - Mark Byers
3
请查看之前关于深拷贝的其他相关问题 - http://stackoverflow.com/questions/tagged/c%23+deep-copy?sort=faq&pagesize=50 - Alexei Levenkov
1
个人而言,我喜欢使用一个简短的序列化和反序列化程序来实现深拷贝。虽然序列化和反序列化有点慢,但无论对象有多少属性,这种方法都适用。不幸的是,我正在度假,所以不能随时发布代码。 - slugster
显示剩余4条评论
11个回答

140
你需要创建新的Book对象,然后将它们放入一个新的List中:
List<Book> books_2 = books_1.Select(book => new Book(book.title)).ToList();

更新:稍微简单一点... List<T> 有一个名为ConvertAll的方法,它返回一个新列表:

List<Book> books_2 = books_1.ConvertAll(book => new Book(book.title));

50
如果Book对象更加复杂,有成千上万的其他属性,会怎么样? - TheScholar
23
+1,@TheScholar - 谢谢。你可以创建复制构造函数或实现其他方式来创建对象的深度拷贝。 - Alexei Levenkov
16
那么这将是一个设计不良的类。成千上万的属性……你是认真的吗? - Mark Byers
13
@MarkByers,当然这只是一种修辞手法。我的例子只提供了一个属性,目的是为了保持例子的简单易读。 - TheScholar
8
新问题可以简化为“如何在C#中复制单个对象?” - Mark Byers
显示剩余5条评论

46

创建一个通用的ICloneable<T>接口,你需要在Book类中实现该接口,以便该类知道如何创建自身的副本。

public interface ICloneable<T>
{
    T Clone();
}

public class Book : ICloneable<Book>
{
    public Book Clone()
    {
        return new Book { /* set properties */ };
    }
}

您可以使用Mark提到的linq或ConvertAll方法。
List<Book> books_2 = books_1.Select(book => book.Clone()).ToList();

或者

List<Book> books_2 = books_1.ConvertAll(book => book.Clone());

23
我很失望微软没有像Ruby那样提供一个整洁、快速和易于解决的方案,例如使用clone()方法。 但是这并不会创建深层拷贝,而是仅仅创建浅层拷贝。 对于深层复制,您必须始终小心,确切地了解您想要复制的内容。以下是一些可能出现的问题示例:
1. 对象图中的循环引用。例如,书籍(Book)有一个作者(Author),而作者(Author)有他的书籍列表。 2. 引用到某个外部对象。例如,一个对象可能包含打开写入文件的流(Stream)。 3. 事件。如果对象包含事件,则几乎任何人都可以订阅它。如果订阅者是类似于GUI窗口(Window)的东西,这可能会变得特别棘手。
现在,基本上有两种克隆某些东西的方法: 1. 在每个需要进行克隆的类中实现一个Clone()方法。(也有ICloneable接口,但不应该使用它;使用自定义的ICloneable<T>接口是可以的。)如果您知道所需的仅是创建此类的每个字段的浅层拷贝,则可以使用MemberwiseClone()来实现它。或者,您可以创建一个“复制构造函数”:public Book(Book original)。 2. 使用序列化将对象序列化为MemoryStream,然后将其反序列化回来。这要求您将每个类标记为[Serializable],并且还可以配置应该(以及如何)进行序列化。但是,这更像是一种“快速而肮脏”的解决方案,并且很可能也会性能较差。

为什么不应该使用默认的ICloneable接口,而应该使用自定义的泛型接口? - Jan
我还想知道为什么不应该使用ICloneable? - d0rf47
@d0rf47 文档已经解释了: "由于Clone()方法的调用者不能依赖该方法执行可预测的克隆操作,因此我们建议不要在公共API中实现ICloneable接口。" - svick
@svick 我明白了,但是假设为了一个小程序,我想让我的程序执行深拷贝,这样可以吗?因为似乎不建议这样做,因为实现方法没有强制执行,因此返回深拷贝和浅拷贝的方式可能会有所不同,这是正确的理解吗? - d0rf47

12

C# 9的记录类型(records) with 表达式(with expressions)可以使得操作更加简单,尤其是当你的类型含有许多属性时。

你可以使用类似以下代码:

var books2 = books1.Select(b => b with { }).ToList();

我这样做只是为了做个例子:

record Book
{
    public string Name { get; set; }
}

static void Main()
{
    List<Book> books1 = new List<Book>()
    {
        new Book { Name = "Book1.1" },
        new Book { Name = "Book1.2" },
        new Book { Name = "Book1.3" }
    };

    var books2 = books1.Select(b => b with { }).ToList();

    books2[0].Name = "Changed";
    books2[1].Name = "Changed";

    Console.WriteLine("Books1 contains:");
    foreach (var item in books1)
    {
        Console.WriteLine(item);
    }

    Console.WriteLine("Books2 contains:");
    foreach (var item in books2)
    {
        Console.WriteLine(item);
    }
}

输出结果为:(Books2中对象的更改不会影响Books1中原始对象)

Books1中包含:

Book { Name = Book1.1 }

Book { Name = Book1.2 }

Book { Name = Book1.3 }

Books2中包含:

Book { Name = Changed }

Book { Name = Changed }

Book { Name = Book1.3 }


11

你可以使用这个:

var newList= JsonConvert.DeserializeObject<List<Book>>(list.toJson());

这个有一些限制,但是对于我的使用情况来说直接使用非常好。省去了在各处实现复制方法的麻烦。谢谢! - CitiZen

10

1
当其他方法都失败时,这个方法帮了我大忙。我有一个对象类型的属性,没有其他方法可以复制该属性。感谢这个代码片段。 - Anup Sharma
只有当您拥有易于键入的属性时,此方法才有效。如果您有子类实例,例如,则应该实现ISerializable接口。 - Bence Végert

9

1
这个可以工作,但请注意不能只是将 new List<Book>(books_2.ToArray()); 传递给一个函数。必须像发布的那样完成,然后才能传递“books_2”。 - user3496060
19
这对于引用类型不起作用。你最终得到的是指向原始对象的指针。如果你更新了其中一个属性,那么两个都会被改变! - jrandomuser
这是深度复制数组最干净的解决方案。谢谢,伙计。你节省了我很多时间。 - Akash
2
由于这对引用类型无效,为什么不在值类型列表上调用ToList(),我看不出有什么区别。 - Hossein Ebrahimi

2

由于Clone会返回一个Book对象实例,所以在调用ToList之前,该对象首先需要被转换为一个Book。上面的示例需要写成:

List<Book> books_2 = books_1.Select(book => (Book)book.Clone()).ToList();

2
public static class Cloner
{
    public static T Clone<T>(this T item)
    {
        FieldInfo[] fis = item.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        object tempMyClass = Activator.CreateInstance(item.GetType());
        foreach (FieldInfo fi in fis)
        {
            if (fi.FieldType.Namespace != item.GetType().Namespace)
                fi.SetValue(tempMyClass, fi.GetValue(item));
            else
            {
                object obj = fi.GetValue(item);
                if (obj != null)
                    fi.SetValue(tempMyClass, obj.Clone());
            }
        }
        return (T)tempMyClass;
    }
}

0

2
在我的实例中,这是最简单的方法。例如:var tagsToRead = new List<string>(tagsFailed.ToArray()); - Ocean Airdrop
9
除非数组中的对象仍然是指向原始列表中的项目的引用。 - Tom Lint

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