변수 캡처(Variable Capture)

Shy·2025년 3월 17일

C#

목록 보기
22/27

변수 캡처(Variable Capture)

변수 캡처(Variable Capture)람다 함수(익명 메서드) 또는 로컬 함수에서 바깥 스코프(외부)의 변수를 접근하고 유지하는 기능을 의미한다.

즉, 람다 표현식이나 익명 메서드가 자신이 선언된 스코프의 변수를 기억하고 사용할 수 있도록 한다.


1. 변수 캡처 기본 예제

using System;

class Program
{
    static void Main()
    {
        int capturedVariable = 10; // 외부 변수
        Action action = () =>
        {
            Console.WriteLine(capturedVariable); // 외부 변수 접근
        };

        capturedVariable = 20; // 외부 변수 변경
        action(); // 20 출력 (변경된 값 반영)
    }
}
  • action()이 실행될 때 capturedVariable의 값이 10이 아니라 20이 출력된다.
  • 변수의 값이 복사되는 것이 아니라, 해당 변수의 참조를 유지하기 때문이다.

2. 변수 캡처 방식

C#의 변수 캡처 방식은 참조(Reference Capture) 방식값(Value Capture) 방식 두 가지로 나뉜다.

(1) 참조 캡처 (Reference Capture)

  • 바깥 변수(로컬 변수)의 참조를 저장하므로, 원본 값이 변경되면 캡처된 람다에서도 변경된 값을 반영한다.
  • 기본적으로 C#은 참조 캡처 방식을 사용한다.
using System;

class Program
{
    static void Main()
    {
        int counter = 0;
        Action action = () => Console.WriteLine(counter);
        
        counter = 10; // 변수 변경
        action(); // 10 출력 (변경된 값 반영)
    }
}
  • action()이 실행될 때 counter의 값이 10으로 변경된 상태이므로, 10이 출력된다.
  • C#에서 참조 캡처 방식이 기본 동작이라는 걸 보여주는 예제이다.

(2) 값 캡처 (Value Capture)

  • C# 5.0 이전에는 foreach 루프 변수는 값 캡처 방식이었다.
  • 즉, 루프가 돌 때마다 값이 복사되었기 때문에 예상과 다르게 동작하는 경우가 많았다.

값 캡처 예제 (C# 5.0 이전)

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        for (int i = 0; i < 3; i++)
        {
            actions.Add(() => Console.WriteLine(i)); // i 캡처
        }

        foreach (var action in actions)
        {
            action(); 
        }
    }
}

C# 5.0 이전의 출력

3
3
3
  • i는 참조 캡처 되므로, 반복문이 종료된 후 i == 3이 되어 모든 람다가 3을 출력한다.
  • C# 5.0 이후에는 이 문제가 해결되었다.

3. foreach와 변수 캡처 문제

(1) C# 5.0 이전: foreach의 캡처 문제

  • C# 5.0 이전에는 foreach 루프 변수도 참조 캡처 방식이 아니라, 값 캡처 방식이었다.
  • 즉, 각 반복마다 i의 값이 복사되었기 때문에, 람다 함수에서 예상과 다르게 동작했다.
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        foreach (var i in new int[] { 0, 1, 2 })
        {
            actions.Add(() => Console.WriteLine(i)); // i 캡처
        }

        foreach (var action in actions)
        {
            action();
        }
    }
}

C# 5.0 이전의 출력

0
1
2
  • foreach의 변수 i는 값이 복사되어 각 람다가 별도의 변수를 캡처하기 때문에 예상대로 동작했다.
  • 하지만 for 루프에서는 참조 캡처 방식이 사용되어, 의도한 대로 동작하지 않았다.

(2) C# 5.0 이후: foreach 변수 캡처 방식 변경

  • C# 5.0 이후에는 foreach도 참조 캡처 방식으로 통일되었다.
  • 즉, forforeach가 같은 방식으로 동작하게 되어 일관성이 유지되었다.
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        foreach (var i in new int[] { 0, 1, 2 })
        {
            int captured = i; // 명시적으로 복사하여 값 캡처
            actions.Add(() => Console.WriteLine(captured));
        }

        foreach (var action in actions)
        {
            action();
        }
    }
}

출력 (C# 5.0 이후)

0
1
2
  • 이제 foreachfor와 동일하게 참조 캡처를 사용한다.
  • 루프 변수(i)를 captured에 복사하면, 예상대로 동작하도록 강제할 수 있다.

4. for 루프에서 변수 캡처 문제 해결 방법

위에서 봤듯이, for 루프에서는 변수가 참조 캡처되기 때문에 의도하지 않은 동작이 발생할 수 있다.

이 문제를 해결하려면 반복문 내부에서 변수 복사(값 캡처) 를 해야 한다.

문제 발생 코드

List<Action> actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i)); // i는 참조 캡처됨
}

foreach (var action in actions)
{
    action();
}

출력 (예상과 다르게 동작)

3
3
3

i가 참조 캡처되어, 반복문이 끝난 후 i == 3이 되기 때문이다.


해결 방법: 로컬 변수 사용 (값 캡처)

List<Action> actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    int captured = i; // 명시적으로 복사하여 값 캡처
    actions.Add(() => Console.WriteLine(captured));
}

foreach (var action in actions)
{
    action();
}

출력 (예상대로 동작)

0
1
2

개념설명
변수 캡처람다 함수 또는 익명 메서드가 외부 변수를 기억하고 사용하는 기능
참조 캡처변수의 참조를 저장하여, 값이 변경되면 람다에서도 변경됨 (기본 동작)
값 캡처변수를 복사하여 람다에서 변경되지 않도록 유지
for 루프 문제 해결반복문 내부에서 int captured = i; 로 복사하여 해결
C# 5.0 이후 변경 사항foreach도 for처럼 참조 캡처를 사용하도록 변경
profile
신입사원...

0개의 댓글