我应该使用ToList()Deep Clone IList吗?

时间:2014-03-29 09:04:15

标签: c# linq clone

假设我有以下代码深度克隆a到b

IList<int> a = new List<int>();
a.Add(5);
IList<int> b = a.ToList();
好不好? 它看起来像ToList返回一个新列表。但是当我谷歌时,其他人总是使用像

这样的东西
listToClone.Select(item => (T)item.Clone()).ToList();

什么是差异?

3 个答案:

答案 0 :(得分:3)

如果您了解数据的存储方式,则可以解释。有两种存储类型的数据,值类型和引用类型。下面是原始类型和对象声明的示例

int i = 0;
MyInt myInt = new MyInt(0);

MyInt类是

public class MyInt {
    private int myint;

    public MyInt(int i) {
        myint = int;
    }

    public void SetMyInt(int i) {
        myint = i;
    }
    public int GetMyInt() {
        return myint;
    }
}

如何将其存储在内存中?以下是一个例子。 请注意,以下所有内存示例均已简化!

 _______ __________
|   i   |     0    |
|       |          |
| myInt | 0xadfdf0 |
|_______|__________|

对于您在代码中创建的每个对象,您将创建对该对象的引用。该对象将被分组在一个堆中。有关堆栈和堆内存之间的差异,请参阅this explanation

现在,回到你的问题,克隆一个列表。下面是创建整数列表和MyInt对象

的示例
List<int> ints = new List<int>();
List<MyInt> myInts = new List<MyInt>();
// assign 1 to 5 in both collections
for(int i = 1; i <= 5; i++) {
    ints.Add(i);
    myInts.Add(new MyInt(i));
}

然后我们看看记忆,

 _______ __________
| ints  | 0x856980 |
|       |          |
| myInts| 0xa02490 |
|_______|__________|

由于列表来自集合,因此每个字段都包含一个引用地址,该引用地址指向下一个

 ___________ _________
| 0x856980  |   1     |
| 0x856981  |   2     |
| 0x856982  |   3     |
| 0x856983  |   4     |
| 0x856984  |   5     |
|           |         |
|           |         |
|           |         |
| 0xa02490  | 0x12340 |
| 0xa02491  | 0x15631 |
| 0xa02492  | 0x59531 |
| 0xa02493  | 0x59421 |
| 0xa02494  | 0x59921 |
|___________|_________|

现在您可以看到列表myInts再次包含引用,而ints包含值。当我们想要使用ToList()克隆列表时,

List<int> cloned_ints = ints.ToList();
List<MyInt> cloned_myInts = myInts.ToList();

我们得到如下所示的结果。

       original                    cloned
 ___________ _________      ___________ _________
| 0x856980  |   1     |    | 0x652310  |   1     |
| 0x856981  |   2     |    | 0x652311  |   2     |
| 0x856982  |   3     |    | 0x652312  |   3     |
| 0x856983  |   4     |    | 0x652313  |   4     |
| 0x856984  |   5     |    | 0x652314  |   5     |
|           |         |    |           |         |
|           |         |    |           |         |
|           |         |    |           |         |
| 0xa02490  | 0x12340 |    | 0xa48920  | 0x12340 |
| 0xa02491  | 0x15631 |    | 0xa48921  | 0x12340 |
| 0xa02492  | 0x59531 |    | 0xa48922  | 0x59531 |
| 0xa02493  | 0x59421 |    | 0xa48923  | 0x59421 |
| 0xa02494  | 0x59921 |    | 0xa48924  | 0x59921 |
|           |         |    |           |         |
|           |         |    |           |         |
| 0x12340   |   0     |    |           |         |
|___________|_________|    |___________|_________|

0x12340是第一个MyInt对象的引用,保存变量0.此处显示的内容已经过简化,可以很好地解释它。

您可以看到列表显示为已克隆。但是当我们想要更改克隆列表的变量时,第一个变量将设置为7.

cloned_ints[0] = 7;
cloned_myInts[0].SetMyInt(7);

然后我们得到下一个结果

       original                    cloned
 ___________ _________      ___________ _________
| 0x856980  |   1     |    | 0x652310  |   7     |
| 0x856981  |   2     |    | 0x652311  |   2     |
| 0x856982  |   3     |    | 0x652312  |   3     |
| 0x856983  |   4     |    | 0x652313  |   4     |
| 0x856984  |   5     |    | 0x652314  |   5     |
|           |         |    |           |         |
|           |         |    |           |         |
|           |         |    |           |         |
| 0xa02490  | 0x12340 |    | 0xa48920  | 0x12340 |
| 0xa02491  | 0x15631 |    | 0xa48921  | 0x12340 |
| 0xa02492  | 0x59531 |    | 0xa48922  | 0x59531 |
| 0xa02493  | 0x59421 |    | 0xa48923  | 0x59421 |
| 0xa02494  | 0x59921 |    | 0xa48924  | 0x59921 |
|           |         |    |           |         |
|           |         |    |           |         |
| 0x12340   |   7     |    |           |         |
|___________|_________|    |___________|_________|

你看到了变化吗? 0x652310中的第一个值已更改为7.但在MyInt对象中,参考地址未更改。但是,该值将分配给0x12340地址。

当我们想要显示结果时,我们有下一个

ints[0]   -------------------> 1
cloned_ints[0]  -------------> 7

myInts[0].GetMyInt()  -------> 7
cloned_myInts[0].GetMyInt() -> 7

正如您所看到的,原始ints保留了其值,而原始myInts具有不同的值,它已更改。这是因为两个指针都指向相同的对象。如果编辑该对象,则两个指针都将调用该对象。

这就是为什么有两种类型的克隆,深度和浅的克隆。这里的例子是深度克隆

listToClone.Select(item => (T)item.Clone()).ToList();

这将选择原始列表中的每个项目,并克隆列表中的每个找到的对象。 Clone()来自Object类,它将创建一个具有相同变量的新对象。

但是,请注意,如果您的课程中有对象或任何参考类型,则不安全,您必须自己实施克隆机制。或者您将面临与上面描述的相同的问题,原始和克隆的对象只是持有一个引用。您可以通过实施ICloneable界面和实现它的this example来实现这一目标。

我希望你现在明白了。

答案 1 :(得分:0)

这取决于。如果您有value types的集合,则会复制它们。如果您有一个引用类型列表,那么它只会复制对实际对象的引用,而不是实际值。这是一个小例子

void Main()
{
    var a = new List<int> { 1, 2 };
    var b = a.ToList();
    b[0] = 2;
    a.Dump();
    b.Dump();

    var c = new List<Person> { new Person { Age = 5 } };
    var d = c.ToList();
    d[0].Age = 10;
    c.Dump();
    d.Dump();
}

class Person
{
    public int Age { get; set; }
}

上一个代码导致

  

a - 1,2

     

b - 2,2

     

c - 年龄= 10

     

d - 年龄= 10

正如您所看到的,新集合中的第一个数字已更改,并且不会影响另一个。但是我所创造的人的年龄并非如此。

答案 2 :(得分:0)

如果内容IList<T>直接封装值,则为了封装其中的值而识别不可变对象,或者封装共享可变对象的身份,然后调用{{1将创建一个与原始分离的新列表,该列表封装相同的数据。

但是,如果ToList的内容封装了可变对象中的值或状态,则这种方法不起作用。对可变对象的引用只能是值,或者有问题的对象是非共享。如果共享对可变对象的引用,则所有这些引用的下落将成为由此封装的状态的一部分。为避免这种情况,复制此类对象的列表需要生成一个新列表,其中包含对其他(可能是新创建的)对象的引用,这些对象封装与原始对象相同的值或状态。如果有问题的对象包含可用于此目的的IList<T>方法,但如果相关对象本身就是集合,则Clone方法的正确有效行为将依赖于它们知道它们是否包含不得暴露给复制列表的接收者的对象 - 这是.NET Framework无法告诉他们的。