如何在不改变原始列表的情况下更改新列表?

14

我有一个列表,其中填充了一些操作的数据,并将其存储在内存缓存中。现在我想要另一个列表,它包含基于某些条件从列表中获取的一些子数据。

如下面的代码所示,我正在对目标列表进行一些操作。问题是,无论我对目标列表做出什么更改,也会对主列表(mainList)进行更改。我认为这是因为引用是相同的或类似的原因。

我只需要在目标列表上操作不会影响到主列表(mainList)中的数据。

List<Item> target = mainList;
SomeOperationFunction(target);

 void List<Item> SomeOperationFunction(List<Item> target)
{
  target.removeat(3);
  return target;
}

1
你是说两个列表中都存在的对象会被修改吗?也就是说,你需要克隆(Clone())/创建它们的副本而不是在同一实例上操作吗? - Oli
11个回答

26
您需要在方法中克隆(clone)您的列表,因为 List<T> 是一个类,它是引用类型,并且通过引用传递。
例如:
List<Item> SomeOperationFunction(List<Item> target)
{
  List<Item> tmp = target.ToList();
  tmp.RemoveAt(3);
  return tmp;
}

或者

List<Item> SomeOperationFunction(List<Item> target)
{
  List<Item> tmp = new List<Item>(target);
  tmp.RemoveAt(3);
  return tmp;
}
或者
List<Item> SomeOperationFunction(List<Item> target)
{
  List<Item> tmp = new List<Item>();
  tmp.AddRange(target);
  tmp.RemoveAt(3);
  return tmp;
}

这里的 select 没有任何作用,你只需要使用 ToList - Servy
1
第三个例子完全是错的。tmp 变量被用来填充它自己。然后你仍然从 target 而不是 tmp 中删除元素。 - Jesse Webb
@PLB ToList 创建一个新的列表。它以 IEnumerable<T> 作为输入。其内容基本上就是 return new List<T>(source); - Servy
@PLB 我相信所有的LINQ表达式,包括ToList()在内,都会返回数据的新副本,以便操作可以假定原始数据是不可变的。 - Jesse Webb
@JesseWebb 第三个例子应该按预期工作。http://ideone.com/ffHPJV - Leri
显示剩余3条评论

8
您需要复制该列表,以便对副本的更改不会影响原始列表。最简单的方法是使用System.Linq中的ToList扩展方法。
var newList = SomeOperationFunction(target.ToList());

4
@Bernhof:这是正确的;但是请记住,如果列表中的项目是引用类型,列表仍然保持对相同项目的引用。因此对项目本身的更改仍将影响两个列表中的项目。 - Olivier Jacot-Descombes
一个小点需要指出,但我认为SomeOperationFunction应该处理复制。如果函数返回一个List<Item>,那么人们会期望它能够这样做。 - bernhof
@Bernhof 可能是的。鉴于其他5个人建议进行更改,我不认为有必要重申它。关键是复制需要在某个地方进行。 - Servy

5

首先建立一个新的列表并对其进行操作,因为List是引用类型,即当您将其传递给函数时,您不仅传递值,还传递实际对象本身。

如果您只是将target分配给mainList,则两个变量指向同一对象,因此您需要创建一个新的List:

List<Item> target = new List<Item>(mainList);

void List<Item> SomeOperationFunction() 没有意义,因为它要么返回空值 (void),要么返回一个 List<T>。所以你可以从你的方法中删除返回语句,或者返回一个新的 List<Item>。在后一种情况下,我会重新写成:

List<Item> target = SomeOperationFunction(mainList);

List<Item> SomeOperationFunction(List<Item> target)
{
    var newList = new List<Item>(target);
    newList.RemoveAt(3);
    return newList;
}

“List是一种引用类型,因此通过引用传递”这句话是循环的。如果你这么说,你就必须解释什么是引用类型,或者链接到一个参考资料(无意冒犯)。 - Robert Harvey
当然,除非你使用 ref 关键字,否则你仍然是按值传递引用,而不是按引用传递。区分按值传递引用和按引用传递值非常重要。 - Servy

3

我尝试了上面的很多答案。在我测试过的所有答案中,对新列表的更新都会修改原始列表。以下是对我有效的方法。

var newList = JsonConvert.DeserializeObject<List<object>>(JsonConvert.SerializeObject(originalList));
return newlist.RemoveAt(3);

2
即使您创建了一个新列表,新列表中的项目引用仍将指向旧列表中的项目,因此如果需要具有新引用的新列表,我喜欢使用此扩展方法...
public static IEnumerable<T> Clone<T>(this IEnumerable<T> target) where T : ICloneable
{
    If (target.IsNull())
        throw new ArgumentException();

    List<T> retVal = new List<T>();

    foreach (T currentItem in target)
        retVal.Add((T)(currentItem.Clone()));

    return retVal.AsEnumerable();
}

1

由于 List 是一种引用类型,所以传递给函数的是对原始列表的引用。

有关在 C# 中如何传递参数的更多信息,请参见此 MSDN 文章

为了实现您想要的效果,您应该在 SomeOperationFunction 中创建列表副本并返回它。下面是一个简单的示例:

void List<Item> SomeOperationFunction(List<Item> target)
{
  var newList = new List<Item>(target);
  newList.RemoveAt(3);
  return newList; // return copy of list
}

正如Olivier Jacot-Descombes在另一个答案的评论中指出的那样,重要的是要记住,如果项目是引用类型,则列表仍然保留对相同项目的引用。因此,对项目本身的更改仍将影响两个列表中的项目。

1
也许你应该花些时间阅读自己的链接。引用类型并不是通过引用传递的,它们像值类型一样通过值传递,但传递的是一个引用。这是非常重要的区别。 - Servy
抱歉打错字了,我理解了区别并编辑了答案以避免误解。 - bernhof

1

你的目标变量是一个引用类型。这意味着你对它所做的任何操作都会反映在你传递给它的列表中。

为了避免这种情况,你需要在方法中创建一个新的列表,将target的内容复制到其中,然后在新列表上执行删除操作。

关于引用类型和值类型


0

由于在您的原始代码中,您所做的只是传递一个引用(有人会称之为指针),因此您需要复制该列表。

您可以通过在新列表上调用构造函数并将原始列表作为参数来完成此操作:

List<Item> SomeOperationFunction(List<Item> target)
{
    List<Item> result = new List<Item>(target);
    result.removeat(3);
    return result;
}

或者创建一个MemberWiseClone

List<Item> SomeOperationFunction(List<Item> target)
{
    List<Item> result = target.MemberWiseClone();
    result.removeat(3);
    return result;
}

此外,您没有在任何地方存储SomeOperationFunction的返回值,因此您可能需要修改该部分(您将该方法声明为void,不应返回任何内容,但在其中您正在返回一个对象)。 您应该这样调用该方法:
List<Item> target = SomeOperationFunction(mainList);

注意:列表的元素不会被复制(只有它们的引用被复制),因此修改元素的内部状态将影响两个列表。


0

你会看到原始列表被修改,因为默认情况下,任何非基元对象都是按引用传递的(实际上是按值传递,其值是引用,但这是另一回事)。

你需要做的是克隆对象。这个问题将帮助你使用一些代码在 C# 中克隆一个列表:如何在 C# 中克隆一个泛型列表?


1
无论列表是按引用传递还是按值传递都没有关系。在任何情况下,target 都是指向原始列表的引用。只有在你将一个新值分配给 target 时,按值/按引用才有区别,例如 target = null; - Olivier Jacot-Descombes
@OlivierJacot-Descombes - 这就是为什么我简化了说它是按引用传递。括号中的注释只是说明除非您明确使用 ref 关键字,否则 C# 中的所有变量都是按值传递的。恰好对于复杂类型使用的“值”是一个指针。 - Jesse Webb
string 是一种原始类型,但它是一个引用类型(因为它是不可变的)。System.Drawing.Point 不是一种原始类型,而是一个值类型(一个结构体)。 - Olivier Jacot-Descombes
@OlivierJacot-Descombes - string不是原始类型,它是引用类型(只是String的别名)。而Point是一个struct,所有都是值类型。我提到“按引用传递”的原因是因为OP甚至没有使用“引用”这个词,所以我甚至不确定他是否知道值/引用类型是什么。即使“无论如何”,变量也将是对原始列表的引用,我认为帮助OP理解“为什么”而不是告诉他如何修复它会更好。 - Jesse Webb
关于什么是原始类型似乎存在一些混淆,此MSDN文章指出,“任何编译器直接支持的数据类型都称为原始类型”。表达式typeof(string).IsPrimitive返回false - Olivier Jacot-Descombes

0

不要将mainList分配给target,而是使用target.AddRange(mainList);

这样你就会得到一个项目的副本,而不是列表的引用。


为什么要调用 ToArrayAddRange接受一个IEnumerable,而列表已经实现了它。 - Servy
我遇到过一些情况(可能是 .net 2.0 或 3.5),其中 AddRange 只接受一个数组。如果最新的框架接受 IEnumerable,我很乐意修改我的答案。 - Sam Axe
IEnumerable<T> 重载是在 .NET 2.0 中添加的。链接。唯一没有它的版本是 1.0。 - Servy
有点奇怪。也许我在想另外一个拥有 AddRange 函数的东西。感谢您的纠正和参考链接。 - Sam Axe

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