[C#] Lambda, delegate, 그리고 anonymous functions

문연수·2023년 2월 23일
0

CSharp

목록 보기
1/1
post-thumbnail
class Invoker
{
	private Action action;

	public Invoker() => action = delegate { };

	public void AddAction(Action action)	=> this.action += action;
	public void Invoke()					=> this.action.Invoke();
}

class Program
{
	private static void Test(bool isLambda)
	{
		Invoker invoker = new Invoker();
		Action action = delegate { };

		action += () => Console.WriteLine("Invoke");

		invoker.AddAction(
			isLambda ? () => action.Invoke()
			         : action.Invoke
		);

		action += () => Console.WriteLine("Invoke");

		invoker.Invoke();
	}

	private static void Main(string[] args)
	{
		Console.WriteLine("-------- Test Invoke() for lambda --------");
		Test(isLambda: true);

		Console.WriteLine("-------- Test Invoke() for method --------");
		Test(isLambda: false);
	}
}

 위 코드의 실행 결과를 예측할 수 있겠는가? 얼핏 보면 Invoke 가 네 번 호출될 것 같지만 실제론 그렇지 않다:

 어떻게 이런 일이 벌어질 수 있는 것일까? 이러한 출력 결과가 나타난 이유를 자세히 파헤쳐보려 한다.

1. delegatereference type? value type?

Invoke 가 한 번만 출력된 (isLambdafalse 인) 상황에 대해 먼저 살펴보려 한다. 실행 결과를 이해하기 위해선 앞서 소개한 Action 자체를 Delegate 로 변환할 수 있어야 한다:

using System;

delegate void MyAction();

class Invoker
{
	private MyAction action;

	public Invoker() => action = delegate { };

	public void AddAction(MyAction action)
	=> this.action = (MyAction) Delegate.Combine(this.action, action);

	public void Invoke() => action();
}

class Program
{
	private static void Test()
	{
		Invoker invoker = new Invoker();
		MyAction action = delegate { };

		action = (MyAction) Delegate.Combine(
			action, new MyAction(() => Console.WriteLine("Invoke"))
		);

		invoker.AddAction(new MyAction(action.Invoke));

		action = (MyAction) Delegate.Combine(
			action, new MyAction(() => Console.WriteLine("Invoke"))
		);

		invoker.Invoke();
	}

	private static void Main(string[] args)
	{
		Test();
	}
}
(참조: https://sharplab.io/) (.NET Framework (x64))

isLambdafalse 인 상황에서의 코드는 위와 같을 것이다. 여기에서 AddAction 으로 전달한 action 과 그 다음 행의 action 이 완전히 다른 객체임에 주목하라. 이는 Delegate.Combine() 이 객체를 새롭게 생성하기 때문이다. 결과적으로 AddAction 이후에 추가된 delegateinvoker.Invoke() 에 의해 호출되지 않는다.

 이는 delegatereference type 임에도 불구하고 마치 value type 과 같이 동작하는 특이성을 가지기 때문이다.

2. Capture the moment!

 그렇다면 다시 뒤짚어서, 왜 isLambdatrue 인 상황에선 Invoke 가 터미널에 두 번 출력된 것일까? AddAction 으로 전달된 action 객체는 AddAction 이후의 객체와 분명히 다를 것이다. 이를 Lambda Expression 으로 바꾼다 하더라도 delegate 의 기본 규칙은 바뀌지 않을 것이기 때문이다.

delegate void MyAction();

class Invoker
{
	private MyAction action;

	public Invoker() => action = delegate { };

	public void AddAction(MyAction action)
	=> this.action = (MyAction) Delegate.Combine(this.action, action);

	public void Invoke() => this.action.Invoke();
}

class Program
{
	private static void Test()
	{
		Invoker invoker = new Invoker();
		MyAction action = delegate { };

		action = (MyAction) Delegate.Combine(
			action, new MyAction(() => Console.WriteLine("Invoke"))
		);

		invoker.AddAction(() => action.Invoke());

		action = (MyAction) Delegate.Combine(
			action, new MyAction(() => Console.WriteLine("Invoke"))
		);

		invoker.Invoke();
	}

	private static void Main(string[] args)
	{
		Test();
	}
}

 방금 # 1. delegate 는 reference type? value type? 에서 소개한 코드에서 AddAction 딱 한 줄, 그러니까 26 행 전달 인자만 수정했다. 그런데 실행결과는 놀랍게도 두 번의 Invoke 가 호출이 된다:

 여기에 대해서는 Lambda ExpressionVariable Scope 에 대해서 이해할 필요가 있다. 위 코드를 sharplab.io 에 긁어서 붙여 넣으면 코드를 다음과 같이 변환해준다:

internal class Program
{
    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        public static MyAction <>9__0_0;

        public static MyAction <>9__0_1;

        public static MyAction <>9__0_3;

        internal void <Test>b__0_0()
        {
        }

        internal void <Test>b__0_1()
        {
            Console.WriteLine("Invoke");
        }

        internal void <Test>b__0_3()
        {
            Console.WriteLine("Invoke");
        }
    }

    [CompilerGenerated]
    private sealed class <>c__DisplayClass0_0
    {
        public MyAction action;

        internal void <Test>b__2()
        {
            action();
        }
    }

    private static void Test()
    {
        <>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();
        Invoker invoker = new Invoker();
        <>c__DisplayClass0_.action = <>c.<>9__0_0 ?? (<>c.<>9__0_0 = new MyAction(<>c.<>9.<Test>b__0_0));
        <>c__DisplayClass0_.action = (MyAction)Delegate.Combine(<>c__DisplayClass0_.action, <>c.<>9__0_1 ?? (<>c.<>9__0_1 = new MyAction(<>c.<>9.<Test>b__0_1)));
        invoker.AddAction(new MyAction(<>c__DisplayClass0_.<Test>b__2));
        <>c__DisplayClass0_.action = (MyAction)Delegate.Combine(<>c__DisplayClass0_.action, <>c.<>9__0_3 ?? (<>c.<>9__0_3 = new MyAction(<>c.<>9.<Test>b__0_3)));
        invoker.Invoke();
    }

    private static void Main(string[] args)
    {
        Test();
    }
}

 코드가 좀 뭐같긴 하지만 AddAction 으로 전달되는 action 객체와 여기에 lambda expression 이 추가되는 코드만 잘 살펴보면 된다. 코드가 복잡해서 다 분석할 순 없으나 하나 확실하게 알 수 있는 점은 이전 예제에서 본 것처럼 actionlambda expression 이 추가될 때마다 변경된다는 것이다.

 그러나 이전 예제와 달리 actionc__DisplayClass0_0 라는 기묘한 클래스로 래핑되어 있고 AddAction 으로 전달하는 메서드는 action.Invoke() 가 아닌 c__DisplayClass0_0 클래스 내에 정의된 b__2() 라는 점이다. b__2() 는 다시 action() 을 호출하게 된다. (action()action.Invoke() 와 동치; null conditional operator 를 제외한)

 이는 lambda expressionouter variablecapture 하기 때문이다. lambda expression 은 위에서 본 것처럼 lambda expressoin 외부에 정의된 변수를 가져다 쓸 수 있는데 위 상황에서는 이하의 규칙이 적용된다:

  1. 캡쳐된 변수 (lambda expression 외부에서 접근한 변수) 는 delegate 자체가 가비지 컬렉터의 수집 대상이 되기 전까진 마찬가지로 수집되지 않는다.
  2. 람다식 내에서 도입된 변수는 둘러싼 메서드에 보이지 않는다(aren't visible).
  3. 람다식은 둘러싼 메서드의 in, ref 혹은 out 의 매개변수로 사용된 대상을 직접적으로 캡쳐할 수 없다. (간접적으로는 가능)
  4. 람다식 내의 return 문은 둘러싼 메서드를 반환하는 결과를 야기하지 않는다.
  5. 람다식은 분기 대상이 람다식 밖에 있는 goto, break, continue 문을 포함할 수 없다. 그 역 또한 가질 수 없다.

 위 규칙, 그 중에서도 1 번 규칙에 따라 actionTest() 함수가 반환되기 전까진 불멸(?) 의 객체가 되며 AddAction() 함수가 호출하게 되는 action 또한 Test() 에서 처음 정의한 동일한 action 객체를 가르키게 된다.

출처

[Web] https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions
[Web] https://sharplab.io/
[Web] https://learn.microsoft.com/en-us/dotnet/api/system.delegate?view=net-7.0
[Web] https://learn.microsoft.com/en-us/dotnet/csharp/delegate-class

위 문제를 해결하는데 지대한 도움을 주신 김찬중 선생님께 감사의 말씀을 드립니다.

profile
2000.11.30

0개의 댓글