C# 中的 List 引用、浅拷贝与闭包捕获问题详解
1. List 是引用类型
在 C# 中,List<T> 是 引用类型 。如果多个变量指向同一个 List,那么对其中一个变量进行修改,其他变量看到的也会被修改。
            
            
              csharp
              
              
            
          
          List<int> list = new List<int>() { 3, 1, 2 };
List<int> a = list;
List<int> b = list;
b.Sort();
Console.WriteLine(string.Join(",", a)); // 输出 1,2,3
        原因:a 和 b 都只是保存了对同一个 List<int> 对象的引用。修改 b 时,本质上就是修改同一个对象。
1.1 ArrayList 也是引用类型
除了 List<T> 之外,旧版本常用的 ArrayList 也同样是引用类型。
            
            
              csharp
              
              
            
          
          var arr1 = new ArrayList() { 1, 2, 3 };
var arr2 = arr1;
arr2.Add(4);
Console.WriteLine(string.Join(",", arr1.ToArray())); // 输出 1,2,3,4
        这里 arr1 和 arr2 依旧指向同一个 ArrayList 实例。
1.2 List 与 ArrayList 的接口关系
List<T>:实现了 泛型接口IList<T>、ICollection<T>、IEnumerable<T>等。ArrayList:实现了 非泛型接口IList、ICollection、IEnumerable。
两者的共同点:
- 都是 引用类型,赋值时拷贝的是引用,而不是集合本身。
 - 修改任意一个变量操作的集合内容,都会影响到另一个。
 
2. 浅拷贝与深拷贝
浅拷贝
复制的是 List 容器,但元素本身还是指向原来的引用对象。
            
            
              csharp
              
              
            
          
          class Person { public string Name; }
var listA = new List<Person> {
    new Person { Name = "Alice" },
    new Person { Name = "Bob" }
};
var listB = new List<Person>(listA); // 浅拷贝
listB[0].Name = "Changed";
Console.WriteLine(listA[0].Name); // 输出 Changed
        深拷贝
需要为每个元素都创建一份新的对象。
            
            
              csharp
              
              
            
          
          var listC = listA.Select(p => new Person { Name = p.Name }).ToList();
listC[0].Name = "Another";
Console.WriteLine(listA[0].Name); // 仍然是 Changed,不受影响
        总结:
- 浅拷贝:List 本身复制,但元素还是指向同一份对象。
 - 深拷贝:元素对象也复制,互不影响。
 
3. 闭包与循环变量捕获
来看一个常见的闭包问题:
            
            
              csharp
              
              
            
          
          var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var act in actions) act();
        输出结果
5
5
5
5
5
        很多人会以为输出 0~4,为什么会这样?
4. 什么是闭包
闭包(Closure):函数体中引用了外部作用域的变量,函数与这些变量的组合就称为闭包。
在下面这个例子里:
            
            
              csharp
              
              
            
          
          var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var act in actions) act();
        - Lambda 表达式 
()=>Console.WriteLine(i)捕获的是 循环变量 i 的存储位置,而不是它的值。 - 循环结束时 
i == 5,所有 Lambda 执行时都访问到同一个 i,所以打印出5。 - 实际上,编译器会把 
i提升(hoist)到一个隐藏的类中,所有 Lambda 共享这个字段。 
4.1 传递参数 vs 闭包捕获
闭包与向函数传参有本质区别:
- 普通参数传递
 
            
            
              csharp
              
              
            
          
          int a = 10;
void Foo(int x) { Console.WriteLine(x); }
Foo(a);  // 输出 10
a = 20;
Foo(a);  // 输出 20
        - 参数传入时,复制了一份值(副本),函数拿到的是独立拷贝。
 
- 闭包捕获变量
 
            
            
              csharp
              
              
            
          
          int a = 10;
Func<int> f = () => a;
Console.WriteLine(f()); // 输出 10
a = 20;
Console.WriteLine(f()); // 输出 20
        - 闭包捕获的是 变量本身,不是值快照。
 - 所以闭包里看到的值会随外部变量变化而变化。
 
- 引用类型(如字典、List)
 
            
            
              csharp
              
              
            
          
          var dict = new Dictionary<string,int>() { ["a"] = 1 };
Action action = () => Console.WriteLine(dict["a"]);
dict["a"] = 100;
action(); // 输出 100
        - 传引用类型参数或闭包捕获引用类型,都是捕获对象本身的引用。
 - 修改对象内容,闭包也会看到变化。
 
5. 正确做法
如果希望闭包捕获的是"当前值",而不是后续变化的变量,需要在循环内部引入新的局部变量:
            
            
              csharp
              
              
            
          
          var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
    int copy = i; // 新的局部变量
    actions.Add(() => Console.WriteLine(copy));
}
foreach (var act in actions) act();
        输出:
0
1
2
3
4
        这样每个闭包都捕获独立的变量,不会互相干扰。
✨ 总结:
- 闭包捕获的是 变量本身,而不是值。
 - 循环变量、全局变量、引用类型对象都可能造成闭包中值变化的坑。
 - 解决办法:手动复制一份局部变量,或者把需要固定的值当参数传进去。
 
6. 总结
- List 是引用类型:多个变量引用同一个实例时,修改会互相影响。
 - 浅拷贝 vs 深拷贝:浅拷贝只复制引用,深拷贝复制对象本身。
 - 闭包:捕获变量的存储位置而不是值,循环中容易踩坑。
 - 解决闭包问题:引入新的局部变量。