如何创建一个列表集合的深拷贝?

4

Suppose I have the following class:

public class Author
{
    public int ID {get; private set;}
    public string firstName {get; private set;}
    public string lastName {get; private set; }

    public Author(int id, string firstname, string lastname)
    {
        this.ID = ID;
        this.firstName = firstname;
        this.lastName = lastName;
    }

    public static Author Clone(Author clone)
    {
        Author copyAuth = new Author(clone.ID, clone.firstName, clone.lastName);
        return copyAuth;
    }
}

并且

public class Book
{
    public string bookTitle {get; private set;}
    private List<Author> authors;
    private List<Author> copyofAuthors;
    public string ISBN {get; private set; }

    public Book(string bookTitle, List<Author> authors, string ISBN)
    {
        copyofAuthors = new List<Author>();
        this.bookTitle = bookTitle;
        this.ISBN = ISBN;
        //How do I create a deep copy of my authors List?

        foreach(Author copy in authors)
        {
            Author addAuthors = Author.Clone(copy);
            copyofAuthors.Add(addAuthors);
        }
    }
}

我该如何创建一个包含 List<Authors> 集合的深拷贝?我在 StackOverFlow 上看到了其他页面建议使用序列化以及一些我不熟悉且似乎令人困惑的建议。
我按照 这个链接 创建了我的克隆方法。
问题一:
上述实现是否视为深拷贝?如果是,使用这种方式是否可行?我的意思是,在构造函数中使用 foreach 循环将作者复制到一个新的列表集合中。
问题二:
如果我修改 copyofAuthor 集合中的任何内容,则它将不再引用原始的集合,对吗?因此,原始 集合应该保持不变?
更新1:
    public List<Author> Authors
    {
        get
        {
            return returnAuthors(authors);
        }

    }

    private List<Author> returnAuthors(List<Author> copyList)
    {
        List<Author> getAuthors = new List<Author>();

        foreach(Author copy in copyList){
            getAuthors.Add(Author.Clone(copy));
        }

        return getAuthors;
    }

我正确地实现了我的getter集合,使得当它返回List集合时,它是独立于原始集合的吗?因此,从getter返回的集合所做的任何更改将不会反映在原始集合中,是吗?
更新#2:
使用ReadOnlyCollection
    public class Book
    {

        public string bookTitle {get; private set;}
        private ReadOnlyCollection<Author> authors;
        public string ISBN {get; private set; }


        public Book(string bookTitle, ReadOnlyCollection<Author> authors, string ISBN)
        {

            this.bookTitle = bookTitle;
            this.ISBN = ISBN;
            //Is it okay to do this? 
            this.authors = authors;

        }

        public List<Author> Authors
        {
            get
            {   //Create a shallow copy
                return new ReadOnlyCollection<Author>(authors);
            }

        }

    }

你能描述一下让你相信深拷贝是一个好主意的场景吗?它会消耗大量时间和内存,并且很难正确实现。如果你想对原始列表进行多个独立编辑,最好使用持久化不可变集合。特别是考虑到作者已经是不可变的;为什么你要在第一次克隆一个呢? - Eric Lippert
@K.AlanBates - 非常感谢,我正在寻找对这种方法的验证,您能否检查我的更新并提供建议?我实现了一个getter并想知道是否做得正确。 - user6029770
1
首先阅读文档。 - Eric Lippert
1
如果你在写代码时不知道为什么要写它,那么你今天开始写代码太早了。在你知道为什么要写代码之后再去写它。不要担心深拷贝和浅拷贝的区别;这是一个很少有影响的区别。考虑一下方法的使用者应该能够执行哪些操作。如果你担心这些使用者会改变你的集合,那么你是完全正确的!如果这是你的顾虑,那么不要让他们改变你的集合。给他们一个不可变的集合。 - Eric Lippert
@EricLippert - 让我更新一下我的想法,你可以给予建议吗?这只需要几分钟。 - user6029770
显示剩余17条评论
3个回答

5

在评论区分类已经变得太困难了。

  • 从作者类中删除克隆方法,它是无用的。

在你的书籍类中,你需要解决两个问题。

  • 构造函数需要一个作者列表,但传递进来的调用者可能会更改它。如果我们只复制引用,那么调用者可以意外地更改书中保存的列表。

  • 这本书会返回一个作者列表。如果一个调用者向列表中添加了一些东西,那么他们又改变了这本书。

你可以使用不可变集合解决这两个问题。如果你还没有下载不可变集合库,请使用NuGet下载。

using System.Collections.Immutable;
...
public class Book
{
  public string bookTitle {get; private set;}
  private ImmutableList<Author> authors;
  public IReadOnlyList<Author> Authors { get { return authors; } }
  public string ISBN {get; private set; }

  public Book(string bookTitle, IEnumerable<Author> authors, string ISBN)
  {
    this.authors = ImmutableList<Author>.Empty.AddRange(authors);
    this.bookTitle = bookTitle;
    this.ISBN = ISBN;
  }
}

现在你需要复制作者序列,这样如果调用者更改该顺序,也不必担心,因为你有副本。同时你提供的是一个由不可变集合实现的IReadOnlyList接口,这样就无法更改它。

还有几件事要注意。你要问:“这样做对吗?”

public class Book
{
    private ReadOnlyCollection<Author> authors;
    public Book(ReadOnlyCollection<Author> authors)
    {
        //Is it okay to do this? 
        this.authors = authors;
    }

    public List<Author> Authors
    {
        get
        {   //Create a shallow copy
            return new ReadOnlyCollection<Author>(authors);
        }
    }

(已删除不相关的内容)。

不完全正确,有几个原因。首先,只读集合只是可变集合的一个封装。您仍然处于调用者控制基础集合的情况下,因此可以更改它。

其次,类型不完全匹配;您无法将ReadOnlyCollection转换为List。

我知道这很令人困惑。这里有一个微妙的区别。只读集合就是这样:你只能读取它。这并不意味着其他人不能写入它!这种集合仍然是可变的,只是不被你所改变。不可变集合是真正不可变的;没有人可以更改它。

接下来:通过使作者和书籍都不可变,您做得非常好。但是如果您想要更改它呢?正如您所指出的那样,更改不可变的书意味着制作一本新书。但是您已经有了一本旧书;如何高效地做到这一点?常见的模式是:

public class Book
{
  public string Title {get; private set;}
  private ImmutableList<Author> authors;
  public IReadOnlyList<Author> Authors { get { return authors; } }
  public string ISBN {get; private set; }

  public Book(string title, IEnumerable<Author> authors, string ISBN) : this(
    title, 
    ImmutableList<Author>.Empty.AddRange(authors),
    ISBN) {}

  public Book(string title, ImmutableList<Authors> authors, string ISBN) 
  {
    this.Title = title;
    this.Authors = authors;
    this.ISBN = ISBN;
  }
  public Book WithTitle(string newTitle)
  {
    return new Book(newTitle, authors, ISBN); 
  }
  public Book WithISBN(string newISBN)
  {
    return new Book(Title, authors, newISBN);
  }
  public Book WithAuthor(Author author)
  {
    return new Book(Title, authors.Add(author), ISBN);
  }
  public static readonly Empty = new Book("", ImmutableList<Author>.Empty, "");
}

现在你可以这样做:
Book tlotr = Book.Empty.WithAuthor("JRRT").WithTitle("The Lord Of The Rings");

以此类推。


1
@Svetlana:我从未写过一行生产Java代码。我不知道。我建议你开始在互联网上寻找。 - Eric Lippert
还有一个问题,假设我使用了一个List,比如List<authors> authorList...将其设置为私有的,没有getter,只在构造函数中设置,用户是否仍然可以修改被设置过的List对象? - user6029770
@Svetlana:确保您区分列表的引用所引用的列表。如果将引用存储在私有变量中,则只有可以以某种方式访问该变量的代码才能更改变量。但是,任何拥有对列表的引用的人都可以更改列表的内容。 - Eric Lippert
非常感谢您提供的代码和解释! - user6029770
使 List 集合甚至更大的对象不可变的整个意义是为了线程安全,对吗? - user6029770
显示剩余4条评论

0
如果你有很多属性,写下这个可能会很无聊:
new Author(clone.ID, clone.firstName, clone.lastName...);

看一下这个小例子,了解如何实现 ICloneable 接口:

这个例子来自:http://csharp.2000things.com/2010/11/07/143-an-example-of-implementing-icloneable-for-deep-copies/

public class Person : ICloneable
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public Address PersonAddress { get; set; }

    public object Clone()
    {
        Person newPerson = (Person)this.MemberwiseClone();
        newPerson.PersonAddress = (Address)this.PersonAddress.Clone();

        return newPerson;
    }
}

public class Address : ICloneable
{
    public int HouseNumber { get; set; }
    public string StreetName { get; set; }

    public object Clone()
    {
        return this.MemberwiseClone();
    }
}

Person herClone = (Person)emilyBronte.Clone();

0

问题1: 是的,这会执行深拷贝。我建议使用ICloneable接口而不是这个静态函数,因为它是标准的。

问题2: 是的,由于你正在使用一个新的集合对象,原始集合不会改变。

问题3: 如果你在getter中返回集合,那么有人可以对其进行转换。你可以每次返回集合的新副本,或者将其包装在一个容器中,该容器具有私有只读集合并且不公开集合(但可枚举)。

在你的情况下,不确定你是否希望作者独立。考虑到他们已经只能被私下设置,分享更新可能很有用。你唯一需要保持独立的是集合。所以也许不需要克隆作者。


我发布的链接说ICloneable应该被弃用,不要使用它。 - user6029770
你能演示一下如何实现一个获取器来复制集合吗?像我在构造函数中有的那样,我可以有一个循环遍历的私有方法吗? - user6029770
如果我更新了我的问题,你能看一下并给予建议吗? - user6029770

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