C# Closure : for문의 변수를 람다식에서 참조하면 슬퍼지는 이유
1
발생한 문제
오늘 개인 프로젝트를 작업하다가 슬픈 일을 겪었다.
delegate를 사용해 Change_Tab 함수를 버튼에 추가하려고 했는데, 의도대로 되지 않았다.
아래쪽과 같은 코드를 사용해 프로그램을 구현했더니 다음과 같은 결과가 발생했다.
의도한 대로라면 아래쪽과 같이 탭을 누를 때마다 해당 탭으로 이동해야 했지만,
위와 같이 아무리 눌러도 첫번째 탭을 보여 주고 있었던 것이다.
심지어 for문에는 아무런 문제가 없었다.
i를 Print로 출력해 봐도 0, 1, 2, 3, 4를 반복할 뿐이었고, 고통받았다.
원인은 Closure에 대한 잘못된 이해였다.
그러므로 미래에 내가 고통받지 않기 위해 클로저에 대해 정리하고, 왜 위의 코드가 슬픈 결과를 낼 수밖에 없었는지 알아 보겠다.
2
클로저
클로저(Closure)란, 외부 변수나 필드와 같은 '환경'을 저장하고 있는 함수이다.
람다식을 사용해 전달할 때, 외부의 변수나 필드를 사용하는 경우 클로저로 처리된다.
예를 들어 보자.
TabControlButton[0].Click += delegate { Change_Tab(0); };
Click에 Change_Tab(0)을 추가하기 위해 람다식을 작성했다.
그러나 위의 코드는 외부 변수를 참조하고 있지 않다. 그러므로 클로저로 처리되지는 않는다.
int i = 2;
TabControlButton[0].Click += delegate { Change_Tab(i); };
i = 4;
그렇다면 위와 같은 코드는 어떨까? 람다식이 i라는 외부 변수를 참조하고 있다.
이와 같이 외부 변수를 참조하는 경우 클로저로 처리된다.
여기서 잠깐 생각을 해 보자.
위의 코드에서, Change_Tab(i)은 i의 값을 얼마로 전달할까?
클로저에 대한 이해가 없으면 나처럼, "아 당연히 2를 전달하죠!" 라고 말할 수 있다.
그러나 결과는? 4를 전달한다.
외부 변수나 필드와 같은 주변 환경을 저장한다는 건, 값이 아니라 변수 그 자체를 참조하여 저장한다는 의미이다.
i의 값을 복사해서 가져오는 게 아니라 변수 i 그 자체를 참조해버린다는 말이다.
우리가 TabControlButton을 누르는 건 컴파일 이후인, 프로그램 실행 도중이다.
버튼을 눌렀을 때, 클로저가 된 Change_Tab(i)는 외부 변수 i를 참조하려 들 것이고...
i는 컴파일이 완료되었을 때 4의 값을 가지고 있다. 그래서 클로저가 된 Change_Tab(i)는 Change_Tab(4)가 된다.
그러면 문제가 있었던 코드를 다시 보도록 하자.
for (int i = 0; i < TabControlButton.Length; i++)
{
TabControlButton[i].Click += delegate { Change_Tab(i); };
}
Change_Tab(i)은 for문에 들어 있는 i를 참조하고 있다. 외부 변수를 참조하고 있으므로 클로저로 처리될 것이다.
컴파일이 완료되었을 때 i는 그럼 몇이 된다?
TabControlButton.Length + 1이 된다.
3
해결
그래서... 슬픔을 겪은 후 다음과 같은 코드로 바꾸었더니 해결되었다.
for (int i = 0; i < TabControlButton.Length; i++)
{
int j = i;
TabControlButton[i].Click += delegate { Change_Tab(j); };
}
이와 같은 코드에서 Change_Tab(j)는 for문 내에 있는 j를 참조하게 될 것이다.
이걸 보고, "j도 어차피 TabControlButton.Length + 1이 되지 않나?" 라고 말할 수 있을 것 같은데, 그렇게 되지 않는다.
왜냐하면 for문을 겪는 동안 j는 매번 새로 선언되어 값을 할당받기 때문이다.
for (int i = 0; i < TabControlButton.Length; i++)
{
TabControlButton[i].Click += delegate { Change_Tab(i); };
}
여기에서 Change_Tab(i)는 모두 for문에 있는 하나의 i를 참조하고 있다.
for (int i = 0; i < TabControlButton.Length; i++)
{
int j = i;
TabControlButton[i].Click += delegate { Change_Tab(j); };
}
그러나 여기에서는 Change_Tab(i)이 매번 새로 만들어진 j를 참조하고 있다.
매번 새로 만들어진 j는 그 순간의 i 값으로 고정되므로, 의도한 대로 정상 작동하게 된다.
'프로그래밍 > C#' 카테고리의 다른 글
Enum.ToString 최적화하기 (0) | 2022.02.25 |
---|---|
[.Net C# 설명서] 읽고 정리하기 (0) | 2021.09.29 |
[C# 코딩의 기술]에서 얻은 지식들 (0) | 2021.09.24 |
C# 연산자 오버로딩 : ==, Equals에서 null값 판단하기 (0) | 2021.03.24 |
LINQ Method 정리 (1) | 2020.05.06 |
댓글
이 글 공유하기
다른 글
-
[.Net C# 설명서] 읽고 정리하기
[.Net C# 설명서] 읽고 정리하기
2021.09.29 -
[C# 코딩의 기술]에서 얻은 지식들
[C# 코딩의 기술]에서 얻은 지식들
2021.09.24 -
C# 연산자 오버로딩 : ==, Equals에서 null값 판단하기
C# 연산자 오버로딩 : ==, Equals에서 null값 판단하기
2021.03.24 -
LINQ Method 정리
LINQ Method 정리
2020.05.06