C#에서 함수 (메소드, 펑션)은 1급 객체인가?
- 1급 객체란?
- 한글 블로그로 보면 맞다 아니다 갈리는 자료들이 많다.
- 하지만 영문 자료나 전능하신 AI님들께 물어보면 답은 비슷한거같다.
C#에서 함수는 1급 객체가 아니다.
- 1급 객체는? 다른 객체들에 적용되는 연산들을 모두 지원하는 객체를 가르킨다.
- 함수가 1급 객체인 다른 프로그래밍 언어들에서는?
- 함수 그 자체를 변수로 선언하거나, 인자로 넘기거나, 리턴 값으로 반환 할 수 있다.
함수가 1급 객체인 자바 스크립트로 알아보자
-
함수 자체를 변수로 선언하는 경우
-
함수를 메소드의 인자로 넘기는 경우
-
함수를 리턴값으로 반환하는 경우
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라는 사실을 잊어버리고 뭔가 함수를 담아둔다고만 생각하면 벌어질 수 있는 메모리 누수의 예가 될수 있겠다.
- 어찌보면 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는 뒷장으로 분리하는게 좋을 것 같다.