이론/일반

클로저 (Closure)

tsyang 2022. 6. 25. 21:16

클로저?


아래 프로그램의 add3, add5는 각각 넘겨받은 숫자에 3,5를 더해서 반환하는 함수이다. 

using System;

public static class Prgoram
{
    public static void Main(string[] args)
    {
        var add3 = Adder(3);
        var add5 = Adder(5);

        Console.WriteLine(add3(10));    //13을 출력
        Console.WriteLine(add5(10));    //15를 출력
    }

    public static Func<int, int> Adder(int add)
    {
        return (int num) => { return num + add; };
    }
}

add3, add5를 생성할 때, Adder메서드에 각각 3과 5를 넘겨주고 add3과 add5는 이를 기억하고 있다가 나중에 넘겨받은 숫자에 각각 3,5를 더한 뒤 그 값을 반환한다. 

 

이처럼 Adder() 함수로 넘어간 paramter  'add' 가 사라지지 않고 기억되고 있는 것이 바로 클로저(Closure)가 해주는 일이다.  다시 말하면, outer scope의 변수('add')를 캡쳐하는 것이다.

 

그렇다면 add3와 add5는 넘겨받은 숫자 3, 5를 어디에 기억하고 있을까?

 

 

 

 

 

원리


컴파일러가 람다, 익명 메서드를 만나면 이를 함수 오브젝트 클래스로 변경한다. 아래 링크 참고.

 

https://tsyang.tistory.com/60?category=919754#lambda-expression 

 

Cpp 함수 (C++11 lambda, std::function)

C++11에 추가된 람다와 function wrapper는 Cpp에서도 함수형 프로그래밍이 어느정도 가능하게 해줬다. (그럼에도 Cpp는 기본적으로 객체지향 프로그래밍 + performance에 최적화 되어있다고 보아야 한다. +

tsyang.tistory.com

 

그렇기에 위 예제코드의 add3과 add5는 컴파일러가 생성한 함수 오브젝트 클래스의 인스턴스라고 볼 수 있다. 

 

그렇다면  add3과 add5가 어디에 3, 5를 기억하고 있는지 대충 감이 온다. 클래스를 하나 만들어서 거기에 넣어주면 된다!

 

 

 

 

예제 코드의 IL 코드를 보자. 여기서 직접 해볼 수 있다.

 

먼저 다음과 같은 새로운 클래스가 정의되었음을 알 수 있다. 

.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass1'
    extends [mscorlib]System.Object
{
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Fields
    .field public int32 'add'	//'add' 라는 이름의 필드가 있다.

    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor () cil managed {}	//내용생략

    .method public hidebysig 
        instance int32 '<Adder>b__0' (int32 num) cil managed 
    {
        //내용 생략
    } // end of method '<>c__DisplayClass1'::'<Adder>b__0'

} // end of class <>c__DisplayClass1

무슨 클래스인지는 모르겠지만 int32 'add' 필드를 가지고 있다.

 

그리고 Adder 메서드에서 위 클래스의 인스턴스를 생성한뒤 'add'필드에 인자로 넘겨받은 add 변수 값을 대입시킨다.

 

그렇다면 예제 코드는 '의미상' 다음과 같은 의미를 지닐 것이다.

using System;

public static class Prgoram
{
    public class CompilerGeneratedAdder
    {
        public int add;

        public int Call(int num)
        {
            return this.add + num;
        }
    }

    public static void Main(string[] args)
    {
        var add3 = Adder(3);
        var add5 = Adder(5);

        Console.WriteLine(add3.Call(10));    //13을 출력
        Console.WriteLine(add5.Call(10));    //15를 출력
    }

    public static CompilerGeneratedAdder Adder(int add)
    {
        var adder = new CompilerGeneratedAdder();
        adder.add = add;

        return adder;
    }
}

 

 

 

 

흔히 하는 실수


많이 하는 실수가 다음과 같이 버튼에 이벤트 연결할때 다음과 같은 코드에서 0 1 2가 출력될 것을 기대하는 것이다.

public static class Prgoram
{
    public static void Main(string[] args)
    {
        SomeButton[] btns = {new SomeButton(), new SomeButton(), new SomeButton()};

        for (int i = 0; i < btns.Length; ++i)
            btns[i].OnClickAction = () => Console.WriteLine(i.ToString());

        foreach (var someButton in btns)
            someButton.OnClickAction?.Invoke();
    }

 

그러나 위의 코드는 3 3 3을 출력한다. for 루프의 변수 i를 캡쳐하기 때문이다. 

 

즉 '의미상' 다음과 같은 코드가 수행된다.

CompilerGeneratedMethod method = new CompilerGeneratedMethod();

for (method.i = 0; method.i < btns.Length; ++method.i)
{
    btns[method.i].OnClickAction = method.Call;	//method.i를 출력
}

이때 컴파일러가 생성한 클래스의 인스턴스는 루프 외부에 생성된다. (어쩌피 다 같으니까)

 

의도대로 0, 1, 2를 출력하는 코드를 작성하기 위해서는 루프를 

for (int i = 0; i < btns.Length; ++i)
{
    int num = i;
    btns[i].OnClickAction = () => Console.WriteLine(num.ToString());
}

위와 같이 고쳐줘야 한다. 이렇게 되면 버튼들이 각각의 scope에 선언된 'num'을 캡쳐하여 0 1 2를 출력하게 된다.

 

위 코드는 '의미상' 다음과 같을 것이다.

for (int i = 0; i < btns.Length; ++i)
{
    CompilerGeneratedMethod method = new CompilerGeneratedMethod();
    method.num = i;
    btns[i].OnClickAction = method.Call;
}

위와 달리 컴파일러가 생성한 클래스 인스턴스가 루프 내부에 생성된다. (num이 다 다르니까!)

 

 


참고 : 

https://web.archive.org/web/20150707082707/http://diditwith.net/PermaLink,guid,235646ae-3476-4893-899d-105e4d48c25b.aspx

https://www.csharpstudy.com/DevNote/Article/26

'이론 > 일반' 카테고리의 다른 글

N번째 난수 값 얻어오기  (0) 2023.06.18
예외(Exception) 써야할까?  (0) 2022.06.19
객체 메모리, Object Alignment  (0) 2021.04.18
멀티플레이 게임과 동기화  (0) 2020.10.11