C#에서 "Collection was modified; enumeration operation may not execute"(컬렉션이 수정되어 열거 작업이 실행되지 않을 수 있습니다) 오류는 List와 같은 컬렉션을 foreach 루프로 순회하는 도중에 해당 컬렉션에 요소를 추가하거나 삭제하는 등의 변경이 일어날 때 발생합니다.
오류가 발생하는 예제
using System;
using System.Collections.Generic;
namespace DemoApplication {
public class Program {
static void Main(string[] args) {
try {
var studentsList = new List<Student> {
new Student {
Id = 1,
Name = "John"
},
new Student {
Id = 0,
Name = "Jack"
},
new Student {
Id = 2,
Name = "Jack"
}
};
foreach (var student in studentsList) {
if (student.Id <= 0) {
studentsList.Remove(student);
}
else {
Console.WriteLine($"Id: {student.Id}, Name: {student.Name}");
}
}
}
catch(Exception ex) {
Console.WriteLine($"Exception: {ex.Message}");
Console.ReadLine();
}
}
}
public class Student {
public int Id { get; set; }
public string Name { get; set; }
}
}실행 결과
위 코드를 실행하면 다음과 같은 결과가 출력됩니다.
Id: 1, Name: John Exception: Collection was modified; enumeration operation may not execute.
오류가 발생하는 이유
위 예제에서는 foreach 루프를 사용해 studentsList를 순회하고 있습니다. 학생의 Id가 0이면 해당 항목을 studentsList에서 제거하는데, 바로 이 시점에 컬렉션의 크기가 변경(재조정)되면서 런타임 중에 예외가 발생합니다.
foreach 문은 내부적으로 열거자(Enumerator)를 사용해 컬렉션을 순회하는데, 이 열거자는 컬렉션의 버전 정보를 추적합니다. 순회 도중 컬렉션이 수정되면 버전이 달라져 열거자가 더 이상 유효하지 않다고 판단하고 예외를 던지는 것입니다.
해결 방법
이 문제를 해결하는 가장 간단한 방법은 반복을 시작하기 전에 ToList() 메서드를 호출하여 원본 컬렉션의 복사본을 만든 뒤, 그 복사본을 대상으로 순회하는 것입니다.
foreach (var student in studentsList.ToList())
ToList()는 LINQ 확장 메서드로, 원본 리스트와는 별개인 새로운 리스트를 반환합니다. 따라서 복사본을 순회하는 동안 원본 컬렉션에서 요소를 안전하게 제거할 수 있고, 열거자 충돌 없이 코드가 정상적으로 동작합니다.
수정된 전체 예제
using System;
using System.Collections.Generic;
using System.Linq;
namespace DemoApplication {
public class Program {
static void Main(string[] args) {
var studentsList = new List<Student> {
new Student {
Id = 1,
Name = "John"
},
new Student {
Id = 0,
Name = "Jack"
},
new Student {
Id = 2,
Name = "Jack"
}
};
foreach (var student in studentsList.ToList()) {
if (student.Id <= 0) {
studentsList.Remove(student);
}
else {
Console.WriteLine($"Id: {student.Id}, Name: {student.Name}");
}
}
Console.ReadLine();
}
}
public class Student {
public int Id { get; set; }
public string Name { get; set; }
}
}실행 결과
위 코드를 실행하면 다음과 같은 결과가 출력됩니다.
Id: 1, Name: John Id: 2, Name: Jack
참고: 다른 해결 방법들
ToList() 외에도 상황에 따라 아래 방법들을 활용할 수 있습니다.
- 역방향 for 루프 사용: 인덱스 기반 for 루프를 뒤에서부터 앞으로 순회하면 요소를 제거해도 인덱스가 밀리지 않아 안전합니다.
- RemoveAll() 사용: 조건에 맞는 요소를 한 번에 제거하는 RemoveAll(student => student.Id <= 0)처럼 람다식을 활용하면 별도의 루프 없이 깔끔하게 처리할 수 있습니다.
- 새 컬렉션 생성: Where() 같은 LINQ 메서드로 필터링된 새 리스트를 만들어 원본을 교체하는 방식도 좋은 선택입니다.