foreach 는 번호를 신경 쓰지 않는다
Esc

    foreach 는 번호를 신경 쓰지 않는다

    –C#기초

    목록을 전부 도는 코드를 지금까지 이렇게 썼습니다.

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

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

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

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

    foreach 는 이렇게 생겼다

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

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

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

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

    foreach (var n in nums)

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

    무엇을 돌 수 있나

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

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

    Dictionary 는 짝으로 나옵니다.

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

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

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

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

    도는 동안 고치면 터진다

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

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

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

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

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

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

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

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

    nums.RemoveAll(n => n < 0);                    // 조건을 넘긴다
    var alive = new List<int>();
    foreach (var n in nums) if (n >= 0) alive.Add(n);
    nums = alive;                                   // 새로 만든다

    읽기 전용이다

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

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

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

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

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

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

    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 입니다.