class PseudoButton
{
public delegate void VoidDelegate();
private VoidDelegate onClick;
// 뷰 만들때 여기로 리스너를 넘겨준다.
public void SetOnClick(VoidDelegate onClick)
{
this.onClick = onClick;
}
// UI 게임 오브젝트에서 이 함수로 이벤트를 넘겨주면, 그냥 리스너를 호출하기만 한다.
public void OnClick()
{
this.onClick?.Invoke();
}
}
대략 이렇게.
물론 목적성이 뚜렷한 UI는 이렇게 작성하는게 별 의미 없을 수 있지만, 최대한 다른곳에서 돌려써야 하는 UI는 경험적으로 이렇게 구현하는것이 좀 더 나았던 것 같다.
그러다가 아래 예시와 비슷하게 10개의 동일한 뷰 스크립트가 붙어있는, index값만 조금 다른 버튼을 구현해야 할 일이 있었다. 그래서 별 생각 없이 for문을 돌아가며 i
값을 기준으로 각자 할 일을 하는 리스너를 붙여줬다.
아래의 예시 코드를 보자. 처음에 작성하면서 나는 이 코드가 "I am 5th button" 을 출력할 것이라고 생각했었다.
using System;
using System.Collections;
namespace TestProject
{
class MainClass
{
public static void Main(string[] args)
{
// 가짜 버튼을 10개를 만들어서,
PseudoButton[] buttons = new PseudoButton[10];
// 각 버튼마다 클릭 이벤트 때 할 일들을 정해준다.
for (int i = 0; i < 10; i++)
{
buttons[i] = new PseudoButton();
buttons[i].SetOnClick(() =>
{
Console.WriteLine(string.Format("I am {0}th button", i));
});
}
// "I am 5th button"이 출력되길 기대한다.
buttons[5].OnClick();
}
}
// 여기선 단순히 버튼의 onClick 메소드만 보관하는 가짜 버튼 클래스
class PseudoButton
{
public delegate void VoidDelegate();
private VoidDelegate onClick;
public void SetOnClick(VoidDelegate onClick)
{
this.onClick = onClick;
}
public void OnClick()
{
// 유니티에서 못 쓰는거 여기에서라도 써보자
this.onClick?.Invoke();
}
}
}
하지만 결과는 "I am 10th button" 이라고 나온다. 혹시나 싶어서 다른 버튼을 눌러봐도, 버튼 0~9가 전부 다 똑같이 10번째 버튼이라고 말한다. 아마 for문이 끝나면 i의 값이 마지막으로 10이 되기 때문인것 같은데, 어쨌거나 내가 원했던 동작은 아니다.
여기서 원하는 대로 각 대리자에게 원하는 값의 변수를 전달되게 하기 위해선, 루프 내부에서 로컬 변수를 만들고, 이를 대리자에게 전달해야한다.
for (int i = 0; i < 10; i++)
{
// 루프 내부의 로컬 변수에 값을 복사해 놓고,
int localCopy = i;
buttons[i] = new PseudoButton();
buttons[i].SetOnClick(() =>
{
// 로컬 변수를 전달한다.
Console.WriteLine(string.Format("I am {0}th button", localCopy));
});
}
이렇게 하면 원래 기대했던 결과인 "I am 5th button"가 출력된다.
일단 저렇게 수정하고 커밋을 올려놓긴 했는데, '왜 저럴까? for문 안에선 어차피 i가 로컬 변수일텐데, 그걸 다른곳에 복사한다고 뭐가 달라질까?'라고 생각하다가 이 아티클을 보게 되었다.
아티클에 따르면 람다함수 안에서 사용하고 있는 변수는 C# 컴파일러가 컴파일을 하면서 따로 익명의 private 클래스의 멤버로 옮겨지고, 람다함수의 내용도 그 클래스의 멤버로 옮겨지게 된다. 이 때 처음의 잘못된 로직은 이렇게 바뀌게 될 것이다.
private class InnerClass
{
public int i;
public void lambdaMethod()
{
Console.WriteLine(string.Format("I am {0}th button", i));
}
}
public static void Main(string[] args)
{
PseudoButton[] buttons = new PseudoButton[10];
// 루프 밖에서 생성
InnerClass innerClass = new InnerClass();
for (int i = 0; i < 10; i++)
{
innerClass.i = i;
buttons[i] = new PseudoButton();
buttons[i].SetOnClick(innerClass.lambdaMethod);
}
buttons[5].OnClick();
}
i
가 루프를 뛰어넘어 접근 가능한 변수니까 루프 바깥에서 innerClass
를 생성할테고, 그 멤버인 innerClass.i
를 루프 내부에선 계속 최신화만 시켜주고 있으니 buttons[5].OnClick();
가 불릴때는 루프가 종료되던 시점의 i
값인 10을 출력하게 되는것이다.
반면에, 로컬 변수에 값을 복사했던 로직은 아래와 같이 바뀌게 될 것이다.
private class InnerClass
{
public int localCopy;
public void lambdaMethod()
{
Console.WriteLine(string.Format("I am {0}th button", localCopy));
}
}
public static void Main(string[] args)
{
PseudoButton[] buttons = new PseudoButton[10];
for (int i = 0; i < 10; i++)
{
// 루프 안에서 생성
InnerClass innerClass = new InnerClass();
innerClass.localCopy = i;
buttons[i] = new PseudoButton();
buttons[i].SetOnClick(innerClass.lambdaMethod);
}
buttons[5].OnClick();
}
innerClass
자체를 루프 내부에서 생성하고, 그 멤버를 i값으로 세팅한다. 루프가 다 끝나더라도 각 인스턴스에 저장된 localCopy
값은 그대로 있기 때문에, buttons[5].OnClick()
시점에서 리스너를 호출하더라도 세팅할 때의 인덱스 값을 그대로 출력하게 된다.
다른 스크립트 언어의 클로져랑 동작이 좀 다른것 같다. CLR에서 런타임에 캡쳐된 변수들에 대한 레퍼런스를 가지고 있게 할 줄 알았는데, IEnumerator
로 제네레이터 함수를 만드는 것 처럼 컴파일러가 처리해준다니.
참조
https://www.codeproject.com/Articles/15624/Inside-C-Anonymous-Methods#4
'프로그래밍' 카테고리의 다른 글
렌더링 파이프라인에서 왜 동차 좌표계를 쓸까 (3) | 2017.07.09 |
---|---|
SAT(Separating Axis Theorem) 충돌처리 구현 (0) | 2017.06.21 |
Code jam 2016 quals round (D, Fractiles) (0) | 2016.11.11 |
Code jam 2016 quals round (C, coin jam) (0) | 2016.11.08 |
Code jam 2016 qualification round A, B (0) | 2016.11.02 |