서론( 문제 상황 )

유니티에서 버튼을 구현하다보면, 딜리게이트든 액션이든 사용해서 클릭 리스너에 직접 할당하거나, 가독성이나 구현속도를 위해서 바로 무명함수로 꽂아넣는 경우가 흔하다.

이럴 때, 만약에 버튼이 N 개 이어서, 각각 다른 인덱스로 처리하게 하고 싶은데, 다음과 같이 구현하면 반드시 문제가 생긴다.


void SetUpButtons()
{
	int buttonCount = 5;
	for(int i = 0 ; i < 5; i++)
	{
		button[i].OnClickListner.RemoveAllListners();
		button[i].OnClickListner.AddListener( () => { Do(i) });
	}
}

void Do(int index)
{
	Debug.Log( "You Clicked This : " + i ); 
}

분명 위 예제코드를 짠 프로그래머는 , 0번 버튼을 누르면 “ You Clicked This : 0 “ 가 출력되기를 기대하겠지만,

실제로는 “You Clicked This : 4”를 반환한다.

아마츄어 팀 시절에도 한번 당해본적이 있고, 직장인이 되고나서도 두세번 나나 다른 사람이 당하는 걸 본적이 있는 악랄한 이슈다.

본론 ( 문제 분석 )

결론부터 말하자면, 잘못 구현한게 맞다.

관련 MSDN의 공식문서 부터 첨부하고 시작한다. ( 참고 : )

c#의 종가 , MS 사의 공식 답변은 “ 무명함수는 클로져가 아니다.” 이다.

클로져에 대해서 검색하면 관련 내용을 많이 찾을 수 있으니, 그 용어에 대한 자세한 설명은 생략하기로 한다. ( 게다가 아직 잘 모른다. )

루비나 자바스크립트 처럼 함수가 일급객체( first class citizen) 인 경우에는 익명함수는 클로져다.

쉽게 말해, 위 언어에서는 함수 안에 들어가는 함수가 외부 맥락을 그대로 저장하고 있을 수 있다.

하지만, 우리의 C# 에서는 함수 내부에 람다식(익명함수)로 함수를 선언해서, 외부 함수의 필드를 내부함수에 그대로 가져오거나 하면,

가장 최근에 덮어 씐 것으로 덮어 씌워지는 현상이 있는 것이다.

2005년 외국의 블로그 글 하나를 보면 , 당시에도 꽤나 조롱이나 놀라움을 안기는 이슈였던 것 같다.

” 버그가 아니고 기능인데요 “ 식으로 비꼬는 것을 보면 … 결론적으로 성능에서 우위를 점한다고는

하지만, 직관적이지 않은 것임에는 분명하다.

본론2( 해결방법)

원인을 알았으니, 해결방법은 아주 단순하다.


void SetUpButtons()
{
	int buttonCount = 5;
	for(int i = 0 ; i < 5; i++)
	{
		button[i].OnClickListner.RemoveAllListners();
		button[i].OnClickListner.AddListener( () => {

		// 외부함수의 필드를 캐싱해주면 된다.
		int indexTemp = i;
		Do(indexTemp)
		});
	}
}

void Do(int index)
{
	Debug.Log( "You Clicked This : " + i ); 
}

이러면, 원래 의도했던 대로 인덱스 i 번째의 버튼을 누르면 인덱스에 맞는 출력이 나오게 된다.

결론

역시 자나깨나 기본기다. 위 원인 분석에 대해서 컴파일러 단에서 설명하는 내용을 전혀 이해 못했다.

“그냥 그런가보다” 의 상태다. 그래도 우선은 문제를 잘 정리해놓고, 나중에 더 지식이 조밀해지면

더 깊게 이해하고 설명할 수 있겠지.

참고자료

익명함수의 변수 붙잡기 ( Medium ) https://medium.com/@unicorn_dev/how-to-capture-a-variable-in-c-and-not-to-shoot-yourself-in-the-foot-d169aa161aa6

익명함수는 클로져가 아니다. MS 블로그 아카이브 https://docs.microsoft.com/ko-kr/archive/blogs/abhinaba/c-anonymous-methods-are-not-closures