---
title: "foreach 는 번호를 신경 쓰지 않는다"
description: "처음부터 끝까지 하나씩 꺼내 줍니다. 인덱스가 필요 없는 반복에서 실수할 자리가 사라집니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-foreach/
---
목록을 전부 도는 코드를 지금까지 이렇게 썼습니다.

```csharp
for (int i = 0; i < nums.Count; i++)
{
    Console.WriteLine(nums[i]);
}
```

하려는 일은 "전부 하나씩 보기" 인데, 적은 것은 그보다 많습니다.
시작값 0, 조건 `< Count`, 증가 `i++`, 그리고 `nums[i]` 까지 네 군데를 적었습니다.

그리고 넷 다 실수할 자리입니다. `<=` 를 쓰면 범위를 넘고,
`i++` 를 빠뜨리면 무한 루프고, 안쪽 반복문에서 `i` 와 `j` 를 바꿔 쓰면 조용히 틀립니다.

> **번호가 필요 없는 반복이면 번호를 안 적으면 됩니다.**

## foreach 는 이렇게 생겼다

```csharp
foreach (int n in nums)
{
    Console.WriteLine(n);
}
```

`in` 앞이 **꺼낸 것을 받을 변수**, 뒤가 **돌 대상**입니다.
번호가 없습니다. 개수도 안 적습니다.

`for` 넷 중 셋이 사라졌습니다. 남은 것은 "무엇을" 과 "어디서" 뿐입니다.

타입을 `var` 로 적으면 컴파일러가 알아서 정합니다.

```csharp
foreach (var n in nums)
```

`var` 는 "타입을 적지 않겠다" 가 아니라 **"오른쪽을 보고 정하라"** 는 뜻입니다.
`nums` 가 `List<int>` 니까 `n` 은 `int` 입니다. 컴파일할 때 정해지고, 나중에 못 바꿉니다.

## 무엇을 돌 수 있나

배열, `List`, `Dictionary`, `HashSet`, `Stack`, `Queue`, 문자열까지 다 됩니다.

```csharp
foreach (char c in "안녕")
{
    Console.WriteLine(c);
}
```

`Dictionary` 는 짝으로 나옵니다.

```csharp
foreach (var pair in prices)
{
    Console.WriteLine(pair.Key + ": " + pair.Value);
}
```

키만 필요하면 `prices.Keys`, 값만 필요하면 `prices.Values` 를 돌면 됩니다.

**공통점은 전부 `IEnumerable` 을 구현했다는 것**입니다.
앞에서 인터페이스는 약속 목록이라고 했습니다.
`foreach` 는 그 약속만 보고 돕니다. 안이 배열이든 해시 테이블이든 상관하지 않습니다.

`Dictionary` 에는 인덱스가 아예 없는데도 `foreach` 로 돌 수 있는 이유가 이것입니다.
번호로 접근하는 것이 아니라 **하나씩 달라고 요청하는 것**이기 때문입니다.

## 도는 동안 고치면 터진다

`foreach` 에는 규칙이 하나 있습니다.

```csharp
foreach (var n in nums)
{
    if (n < 0) nums.Remove(n);      // InvalidOperationException
}
```

**도는 동안 목록을 고치면 안 됩니다.** 넣는 것도 빼는 것도 안 됩니다.

앞에서 `List` 는 중간에서 빼면 뒤가 밀린다고 했습니다.
지금 몇 번째를 보고 있는지 세고 있는데 그 아래가 밀리면 기준이 어긋납니다.
그래서 아예 막고 예외를 냅니다.

조용히 하나를 건너뛰는 것보다 낫습니다. 그건 찾기 어려운 버그가 됩니다.

고쳐야 하면 방법이 셋입니다.

```csharp
for (int i = nums.Count - 1; i >= 0; i--)      // 뒤에서부터 지운다
{
    if (nums[i] < 0) nums.RemoveAt(i);
}
```

뒤에서부터 지우면 밀리는 것이 이미 지나온 자리라 상관없습니다.

```csharp
nums.RemoveAll(n => n < 0);                    // 조건을 넘긴다
```

```csharp
var alive = new List<int>();
foreach (var n in nums) if (n >= 0) alive.Add(n);
nums = alive;                                   // 새로 만든다
```


## 읽기 전용이다

꺼낸 변수에 값을 넣을 수 없습니다.

```csharp
foreach (var n in nums)
{
    n = 0;      // 컴파일 에러
}
```

`n` 은 목록의 자리가 아니라 **꺼내온 사본**입니다. 여기 넣어 봐야 목록은 안 바뀝니다.
그래서 아예 막았습니다.

값을 바꾸려면 `for` 로 번호를 써야 합니다.

```csharp
for (int i = 0; i < nums.Count; i++)
{
    nums[i] = 0;
}
```

참조 타입이면 이야기가 다릅니다.

```csharp
foreach (var dog in dogs)
{
    dog.Name = "바둑";      // 됩니다
}
```

`dog` 에 담긴 것은 주소의 사본입니다. 가리키는 대상은 같으니 내용은 바꿀 수 있습니다.
`dog = new Dog()` 는 안 됩니다. 앞에서 본 그 구분이 그대로 적용됩니다.

## 언제 for 를 쓰나

`foreach` 가 기본입니다. 그런데 `for` 여야 하는 경우가 있습니다.

| 상황 | 무엇을 |
| --- | --- |
| 전부 한 번씩 보기만 하면 됨 | `foreach` |
| 번호가 필요함 (몇 번째인지 출력 등) | `for` |
| 값을 바꿔야 함 | `for` |
| 거꾸로 돌아야 함 | `for` |
| 두 칸씩 건너뛰어야 함 | `for` |
| 도는 동안 지워야 함 | `for` (뒤에서부터) |

**번호를 실제로 쓰는가**가 기준입니다.
`nums[i]` 를 쓰려고만 `i` 를 만들고 있다면 `foreach` 가 맞습니다.

## 정리

- `foreach` 는 처음부터 끝까지 하나씩 꺼내 줍니다. 번호도 개수도 안 적습니다.
- `in` 앞이 받을 변수, 뒤가 돌 대상입니다.
- `var` 는 오른쪽을 보고 타입을 정하라는 뜻입니다. 컴파일할 때 정해집니다.
- `IEnumerable` 을 구현한 것은 전부 돌 수 있습니다. 안이 무엇이든 상관없습니다.
- 도는 동안 넣거나 빼면 예외입니다. 뒤에서부터 지우거나 새로 만듭니다.
- 꺼낸 변수는 사본이라 값을 넣을 수 없습니다. 참조 타입의 내용은 바꿀 수 있습니다.
- 번호를 실제로 쓰면 `for`, 아니면 `foreach` 입니다.