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



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

IList<int> a = new List<int>();
a.Add(5);
IList<int> b = a.ToList();

好是坏?这似乎是工作,因为 ToList 返回一个新列表。但是当我谷歌它时,其他人总是使用这样的东西

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

有什么区别?

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

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 |
|_______|__________|

对于在代码中创建的每个对象,您将创建对该对象的引用。对象将分组到一个堆中。有关堆栈内存和堆内存之间的区别,请参阅此说明。

现在,回到你的问题,克隆一个列表。下面是创建整数和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 接口以及实现它的示例来做到这一点。

我希望你现在清楚了。

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

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

如您所见,新集合中的第一个数字发生了变化,并且不会影响另一个数字。但我创造的人的年龄并非如此。

如果内容 a IList<T>直接封装值,标识不可变对象以封装其中的值,或者封装共享可变对象的标识,则调用ToList将创建一个与原始列表分离的新列表,该列表封装相同的数据。

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

相关内容

  • 没有找到相关文章

最新更新