C#에서 함수 (메소드, 펑션)은 1급 객체인가?

  • 1급 객체란?
  • 한글 블로그로 보면 맞다 아니다 갈리는 자료들이 많다.
  • 하지만 영문 자료나 전능하신 AI님들께 물어보면 답은 비슷한거같다.

C#에서 함수는 1급 객체가 아니다.

  • 1급 객체는? 다른 객체들에 적용되는 연산들을 모두 지원하는 객체를 가르킨다.
  • 함수가 1급 객체인 다른 프로그래밍 언어들에서는?
    • 함수 그 자체를 변수로 선언하거나, 인자로 넘기거나, 리턴 값으로 반환 할 수 있다.

함수가 1급 객체인 자바 스크립트로 알아보자

  • 함수 자체를 변수로 선언하는 경우
    helloworld1

  • 함수를 메소드의 인자로 넘기는 경우 helloworld2

  • 함수를 리턴값으로 반환하는 경우 helloworld3

C#은..?

public class Program{
    static void Main(string[] args)
    {
        // 말도 안되쥬? 컴파일 에러남
        var someFunc = Program.HelloWorld();
    }

    public static void HelloWorld()
    {
        Console.WriteLine("HELLO WORLD");
    }
}
  • 당연히? 자바에서도 이런거는 안된다.

C#의 Delegate

  • C# Delegate문서
  • C#의 함수는 객체가 생성될때 어딘가 같이 생성되어서 주소값을 가지고 있다.
  • C계열 언어를 깊게 다루진 않아봐서, 함수 포인터를 써본적은 없으나, 함수 포인터와 비슷한 기능이라고 한다.
    • 해당하는 함수를 delegate를 사용해 인스턴스화 하면 메서드를 다른 메서드에 전달 할 수 있다.
    • Javascript 같은곳에서 함수 자체를 전달하듯이, delegate를 사용해서 메소드에 다른 메서드를 전달 할 수 있다.
    // 반환형이 스트링이고, 인자가 인트인 함수를 기억할 수 있는 형식이다.
    // MyDel 이란 이름의 델리게이트 설계도 만든다 -> 객체가 아님
    delegate string MyDel(int a); 
    delegate void MyVoidDel();
  • 델리게이트 선언 시, 반환과 인자값 형태를 선언 해 줄 수 있음
  • 추상 클래스의 함수 정의나 인터페이스의 함수 정의처럼 선언해주고 앞에 delegate 키워드를 붙인다.
  • 실질적으로는 함수 포인터를 담는 자료형이라고 이해하면 좋을듯 하다.

Delegate의 Chaining

  • delegate의 연산자에 대해서
  • delegate 연산자는 함수를 전달할 수 있는것 뿐 아니라 간편하게 함수들을 묶어서 전달할 수 있는 강력한 기능을 제공한다.
    • 위는 신기해서 찾아본 delegate에서 연산자를 처리하는 방법
    • 간단하게 정리하자면 컴파일러에서 + 일때는 Delegate.combine을 실행하고, - 일떄는 Delegate.Remove를 실행한다.
    public delegate void Print();
    
    public class DelegateThings
    {
        public void PrintSomething() { }
        public static void PrintStaticThings() { }
    }

    public static class StaticThings
    {
        public static void PrintPrint() { }
    }
    
    public class DelegateExample
    {
        // 플러스 연산으로 여러개 Delegate를 Chaining 할 수 있음
        public void AddDelegate(Print prints)
        {
            // 클래스가 인스턴스화 되지 않았기 때문에 함수에 접근이 안됨
            // print += DelegateThings.PrintSomeThing;

            // static 함수는 가능
            prints += DelegateThings.PrintStaticThings;
            prints += StaticThings.PrintPrint;
            prints = StaticThings.AddPrint + prints;
            
            DelegateThings things = new DelegateThings();
            // 인스턴스화 했다면 가능
            prints += things.PrintSomething;

            // 지금 대리자에 메서드 어떤것들 있는지 표시?
            foreach(Print print in prints.GetInvocationList())
            {
                Console.WriteLine(print.Method);
            }
        }

        // 마이너스 연산도 가능
        public void MinusDelegate(Print prints)
        {
            prints -= DelegateThings.PrintStaticThings;
        }
    }

delegate의 위험성?

using System;

class Program
{
    // 델리게이트 선언
    delegate void MyDelegate();

    class MyClass
    {
        public void MyMethod()
        {
            Console.WriteLine("MyMethod called");
        }
    }

    static void Main()
    {
        // 델리게이트 선언
        MyDelegate del = null;
        del = new MyDelegate();
        // 'CreateAndChain' 함수에서 객체를 생성하고 델리게이트 체인에 추가
        CreateAndChain(del);
        CreateAndChain(del);
        CreateAndChain(del);

        // 델리게이트 호출
        Console.WriteLine("델리게이트 호출");
        del();
        
        // 명시적으로 해제 해줘야 MyClass 객체 가비지 컬렉팅 실행 
        // del = null;
    }

    static void CreateAndChain(MyDelegate del)
    {
        // 여기서 임시 객체를 new로 할당
        MyClass obj = new MyClass();

        // 객체의 메서드를 델리게이트에 추가
        del += obj.MyMethod;

        // 델리게이트 체인에 메서드를 추가한 객체는 메모리에서 해제되지 않음
    }
}
  • 교안에서 써주신 샘플 그대로 복사한 코드, 이해가 쉽게 CreateAndChain을 몇개 더 넣어주었다.
  • MyClass가 가비지 컬렉팅이 일어나지 않는 이유?
    • 아마도 Main의 MyDelegate가 위처럼 한번 할당되면, null을 선언 해 주기 전까지는 계속 MyMethod에 있는 함수를 참조해야함
    • CreateAndChain 에서 obj가 생성된후 지역변수가 끝까지 가서 없어지는것 처럼 보이지만, 사실 함수 포인터를 계속 제공한다는것
  • 요것도 내가 이해한 바를 그림으로 그려보자.. Delegate는 Object

  • Delegate가 Object라는 사실을 잊어버리고 뭔가 함수를 담아둔다고만 생각하면 벌어질 수 있는 메모리 누수의 예가 될수 있겠다.
  • 어찌보면 C#에서 함수를 그대로 전달하는 것이 아닌, 함수의 포인터를 전달한다는 것의 가장 큰 예가 될 수 있겠다.
  • 코드 끝에 널로 할당 한것 처럼 delegate에 null을 넣어주어 참조를 끊어버려서 가비지 컬렉팅을 해버리거나, 소멸자에서 delegate를 해제해버리자.
      class MyClass
      {
          delegate void MyDelegate();
          // 델리게이트 변수 선언
          private MyDelegate myDelegate;        
          // 소멸자에서 델리게이트에 전달한 메서드 제거
          ~MyClass()
          {
              Console.WriteLine("소멸자 호출됨");
              myDelegate -= someMethod;
          }
      }
    

번외, Delegate가 Object라면 직접 생성해서 쓸수 있는가?

// protected 적절한 예시가 될듯~
public abstract class Delegate : ICloneable, ISerializable
{
    protected Delegate (object target, string method)
    {
        if (target == null)
            throw new ArgumentNullException ("target");

        if (method == null)
            throw new ArgumentNullException ("method");

        this.m_target = target;
        this.data = new DelegateData();
        this.data.method_name = method;
    }

    protected Delegate (Type target, string method)
    {
        if (target == null)
            throw new ArgumentNullException ("target");

        if (method == null)
            throw new ArgumentNullException ("method");

        this.data = new DelegateData();
        this.data.method_name = method;
        this.data.target_type = target;
    }
}
  • 나처럼 위험한 상상을 하는 친구들을 위해 라이브러리 설계자분들은 친절히 Protected로 못 쓰게 막아놓으셨다 후후
  • 쓰다보니까 Delegate에 대한 이야기가 너무 길어져서 Lambda와 Event는 뒷장으로 분리하는게 좋을 것 같다.