[C#] Lambda Function Capture

2021. 7. 30. 17:19·etc/C#

람다함수를 delegate에 등록해줄 때, 

 

호출부의 지역변수를 사용하고 싶을때가 있다.

 

이를 변수를 Capture한다고 하는데

 

무슨 얘기냐 하면 다음 코드를 보자

 

class Program
{
	static void Main(string[] args)
	{
		Action countZeroToFour = null;
		for(int i=0; i<5; i++)
		{
			countZeroToFour += () => Console.WriteLine(i);
		}
		countZeroToFour.Invoke();
	}
}

 

Action을 Invoke 했을 때, 0~4까지의 숫자를 차례대로 출력하게 하고 싶어서 위와 같이 작성했다.

 

실행 결과는?

 

5
5
5
5
5

 

전혀 다른 결과가 나온다.

 

프로젝트 진행중에 각 오브젝트에 이벤트를 할당해주면서 저런식으로 작성했었는데

 

계속 이상한 값이 들어가서 엄청 헤맸었다. ㅠㅠ

 

그래서 열심히 구글링해본 결과 답을 얻을 수 있었다.

 

 

 

왜 이런 결과가 나올까?

 

우선 컴파일러가 람다함수를 어떻게 바꿔주는지에 대한 이해가 필요하다.

 

https://www.codeproject.com/Articles/15624/Inside-C-2-0-Anonymous-Methods#4

 

Inside C# 2.0 Anonymous Methods

Understand the internal working of anonymous methods in C# 2.0.

www.codeproject.com

 

해당 글을 읽어보면, 컴파일러는

 

private inner class를 만들고 

 

람다함수를 private 함수로 등록한다고 되어있다. 

 

그래서 위의 코드는 컴파일시 다음과 같이 컴파일러에 의해 바뀌게 된다.

 

 

class Program
{
	private class InnerClass
    {
    	public int i;
    	private void InstanceMethod() 
        { 
            Console.WriteLine(i);
        } 
    }
    
	static void Main(string[] args)
	{
		Action countZeroToFour = null;
        
        InnerClass localObject = new InnerClass();
        
		for(int i=0; i<5; i++)
		{
            localObject.i = i;
			countZeroToFour += () => localObject.InstanceMethod;
		}
		countZeroToFour.Invoke();
	}
}

 

 

코드를 잘 살펴보면 InnerClass 인스턴스는 하나만 만들어지는데 

 

for-loop에서 인스턴스의 i에 계속 할당을 해주는걸 볼 수 있다.

 

심지어 결과가 4가 아닌 5로 나오는걸로 봐서

 

for문이 break될때도 할당이 일어남을 알 수 있다.

 

따라서 5가 5번 나오는 결과는 이해가 간다.

 

 

 

여기서 드는 궁금증은 InnerClass의 인스턴스인 localObject 선언 위치의 기준은 무엇이고,

 

localObject.i에 대한 연산을 하는 위치의 기준은 어떻게 되는지이다.

 

 

 

컴파일러의 입장으로 생각해보자.

 

흐름대로 잘 가다가 람다함수를 만나는 순간, 헉 하고 당황하여

 

우선은 람다함수에서 지역변수(람다 기준에서의 외부 변수)에 접근하는지 먼저 살펴볼것이다.

 

우리의 코드는 for-loop scope의 지역변수인 i를 람다함수에서 끌어다 쓰고 있으므로,

 

사용하는 지역변수의 scope에서 localObject.i에 대한 연산을 해준다.

 

인스턴스 생성은 그럼 사용하는 지역변수를 모두 포함하는 scope에서 해줄 수 밖에 없다는게 이해되기 시작한다.

 

따라서 for문 밖에서 인스턴스 생성이 일어나게 된다.

 

 

 

 

그럼 원래 우리가 의도한대로 동작이 되게 하려면 어떻게 해야할까?

 

class Program
{
	static void Main(string[] args)
	{
		Action countZeroToFour = null;
		for(int i=0; i<5; i++)
		{
        	int temp = i; // for-loop scope보다 더 좁은 scope의 지역변수 선언
			countZeroToFour += () => Console.WriteLine(temp); // i가 아닌 temp를 붙여준다
		}
		countZeroToFour.Invoke();
	}
}

 

0
1
2
3
4

 

 

추가된건 temp라는 지역변수 하나뿐이다.

 

그럼 컴파일러는 이를 어떻게 해석할까?

 

temp는 for-loop보다 더 좁은 범위의 scope이어서 

 

다음과 같이 코드가 바뀌게 된다.

 

 

class Program
{
	private class InnerClass
    {
    	public int i;
    	private void InstanceMethod() 
        { 
            Console.WriteLine(i);
        } 
    }
    
	static void Main(string[] args)
	{
		Action countZeroToFour = null;
        
		for(int i=0; i<5; i++)
		{
        	InnerClass localObject = new InnerClass();
            localObject.i = i;
			countZeroToFour += () => localObject.InstanceMethod;
		}
		countZeroToFour.Invoke();
	}
}

 

이제 결과가 제대로 나오는게 이해가 된다.

 

 

 

정리를 하자면,

 

InnerClass의 생성 위치에 따라 지역변수 capture 결과는 천차 만별이다.

 

이 위치는 람다함수에서 사용할 지역변수의 scope를 보면 된다.

 

//로 scope를 표현해보자면 다음과 같다.

 

 

class Program
{
	static void Main(string[] args)
	{
		Action countZeroToFour = null;
		//for(int i=0; i<5; i++)
		//{
		//	countZeroToFour += () => Console.WriteLine(i);
		//}
		countZeroToFour.Invoke();
	}
}

class Program
{
	static void Main(string[] args)
	{
		Action countZeroToFour = null;
		for(int i=0; i<5; i++)
		{
        	//int temp = i;
			//countZeroToFour += () => Console.WriteLine(temp);
		}
		countZeroToFour.Invoke();
	}
}

 

참고로 foreach는 조금 특별한 scope를 가지고 있어서 지역변수 capture side-effect가 발생하지 않고 

 

제대로 동작한다.

 

 

 

 

 

MSDN에서 람다함수의 변수 scope를 다음과 같이 정하고 있다.

 

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions?WT.mc_id=DT-MVP-4038148 

 

Lambda expressions - C# reference

Learn about C# lambda expressions that are used to create anonymous functions.

docs.microsoft.com

 

The following rules apply to variable scope in lambda expressions:

  • A variable that is captured will not be garbage-collected until the delegate that references it becomes eligible for garbage collection.
  • Variables introduced within a lambda expression are not visible in the enclosing method.
  • A lambda expression cannot directly capture an in, ref, or out parameter from the enclosing method.
  • A return statement in a lambda expression doesn't cause the enclosing method to return.
  • A lambda expression cannot contain a goto, break, or continue statement if the target of that jump statement is outside the lambda expression block. It's also an error to have a jump statement outside the lambda expression block if the target is inside the block.
저작자표시 (새창열림)

'etc > C#' 카테고리의 다른 글

[C#] switch 제어문  (0) 2021.07.16
[C#] 문자열 보간(Interpolation)  (0) 2021.07.15
[C#] Property(프로퍼티), C#만의 특별한 기능  (0) 2021.07.13
[C#] Null-Safety를 지원하는 C#  (0) 2021.07.13
[C#] Lambda Expression(람다 식) 이란?  (0) 2021.07.12
'etc/C#' 카테고리의 다른 글
  • [C#] switch 제어문
  • [C#] 문자열 보간(Interpolation)
  • [C#] Property(프로퍼티), C#만의 특별한 기능
  • [C#] Null-Safety를 지원하는 C#
imsongkk
imsongkk
이것저것 적어보는 개발 블로그
  • imsongkk
    이것저것
    imsongkk
  • 전체
    오늘
    어제
    • 분류 전체보기 (81)
      • 일상 (1)
      • Infra (21)
        • AWS (3)
        • Docker (8)
        • Kubernetes (9)
        • Terraform (1)
      • Trouble Shooting (9)
      • Back-End (18)
        • Spring Boot (2)
        • JPA (7)
        • HTTP 기본 (4)
        • DDD (3)
      • 소마 (4)
      • Programming (7)
        • 디자인 패턴 (7)
      • etc (19)
        • Unity (4)
        • Node.js (2)
        • React (1)
        • 리액트를 다루는 기술 (2)
        • C# (6)
        • Language (0)
        • Firebase (2)
        • 알고리즘 (1)
        • CS (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    Git
    firestore
    소프트웨어 마에스트로 #소마 #SWM #소프트웨어 마에스트로 14기
    Firebase
    EC2
    Pull
    도메인
    Private
    포트
    Repository
    VPC #Subnet #NAT #Region #AZ #IGW
    Push
    3000
    clone
    Terraform #테라폼 #IaC #AWS CLI
    8080
    Google Analytics
    Firebase Analytics
    React
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
imsongkk
[C#] Lambda Function Capture
상단으로

티스토리툴바