c# delegate, 익명함수

JHO·2024년 7월 25일
0

c#스터디

목록 보기
5/9

1. delegate


1-1. delegate

  • 메서드를 담을 수 있는 자료형.
  • 참조 타입
  • 콜백함수, 지정자, 이벤트(앞에 event 붙여서)로서 사용할 수 있다.

1-3. 사용 방법

public delegate float MyDel(float left, float right);
public static float Plus(float left, float right) { return left + right; }
static void Main()
{
    MyDel del;
    del = Plus;
    Console.WriteLine(del(2.5f, 3.5f));;
}
  • 담으려는 메서드의 매개변수 타입과 리턴타입이 delgate와 일치해야한다.

1-4. Func, Action, Predicate

  • 일반화 delegate
Func<float, double, int> fun1 = Function;
Action<float, double> fun2 = Function2;
Predicate<int> p = IsCheck;
public static int Function(float a, double b) { return 0; }
public static void Function2(float a, double b) {  }
public bool IsCheck(int value)
{
    return true;
}
  • 위와 같이 따로 delgate 변수를 선언하지 않아도 바로, Func와 Action을 이용해서 메서드를 담기 가능.
  • Func과 Action 둘다 맨 뒤의 매개 변수는 return타입이다.
  • Func은 return타입으로 void형이 불가능, Action은 return 타입으로 void형이 가능.
  • Predicate는 단일 매개 변수를 가지고,return 타입으로 void형을 가짐.

1-5. delegate 체인

 public static void Hello()
 {
     Console.WriteLine("Hi");
 }
 public static void Bye( )
 {
     Console.WriteLine("Bye");
 }
 static void Main()
 {
     MyDel del;
     del = Hello;
     del += Bye;
     del();         
 }
  • 델리게이트는 하나의 메서드뿐만 아니라 여러 개의 메서드를 저장가능.

1-6. delegate 호출 방법

del();
del?.Invoke();
  • 기존 함수처럼 사용할 수도 있지만, delegate에 저장된 메서드가 없다면 runtime error가 발생가능.
  • ?.Invoke() 식으로 사용하면, null이 아닌 경우에만 실행.

1-7. 콜백함수

  • 델리게이트를 이용하여 특정조건에서 반응하는 함수를 구현.
  • 함수의 호출이 아닌 역으로 호출받을 때 반응을 참조하여 역호출
    -> 즉, 조건이 충족하는 지 여러번 확인하고, 충족될 때 함수가 스스로 호출하는 것이 아닌, 조건이 충족될 때 함수가 역으로 호출을 받는 것이다.
        void Main()
        {
            File file = new File();
            Button saveButton = new Button();
            saveButton.callback = file.Save;
            Button loadButton = new Button();
            loadButton.callback = file.Load;
            saveButton.Click();     // output : 저장하기 합니다.
            loadButton.Click();     // output : 불러오기 합니다.
        }
        public class Button
        {
            public Action callback;
            public void Click()
            {
                if (callback != null)
                {
                    callback();
                }
            }
        }
        public class File
        {
            public void Save()
            {
                Console.WriteLine("저장하기 합니다.");
            }
            public void Load()
            {
                Console.WriteLine("불러오기 합니다.");
            }
        }
    }
  • Button의 Click()메서드에서 등록된 콜백함수 Save()와 Load()가 호출 된다.

1-8.지정자

  • 델리게이트를 사용하여 미완성 상태의 함수를 구성.
  • 매개변수로 전달한 지정자를 기준으로 함수를 완성하여 동작시킴.
  • 기준을 정해주는 것으로 다양한 결과가 나올 수 있는 함수를 구성가능.
void Main()
{
    int[] array = { 3, -2, 1, -4, 9, -8, 7, -6, 5 };
    int index1 = CountIf(array, IsPositive);            // 배열 중 값이 양수인 데이터 갯수
    int index2 = CountIf(array, IsNagative);            // 배열 중 값이 음수인 데이터 갯수
    int index3 = CountIf(array, value => value > 5);    // 배열 중 값이 5보다 큰 데이터 갯수
}
public static int CountIf(int[] array, Predicate<int> predicate)
{
    int count = 0;
    for (int i = 0; i < array.Length; i++)
    {
        if (predicate(array[i]))
        {
            return count++;
        }
    }
    return count;
}
public static bool IsPositive(int value)
{
    return value > 0;
}
public static bool IsNagative(int value)
{
    return value < 0;
}

1-9. event키워드와 delegate

  • 이벤트를 사용할 경우 클래스의 개방폐쇄원칙을 준수 가능.
  • 일련의 사건이 발생한 타이밍에만 연산을 진행할 수 있음
  • 단순이 delegate로만 이벤트를 구현하면, 대입 연산(=)을 통해 기존 저장된 메서드들이 사라질 수 있는 위험성이 존재.
    또한, 외부에서 이벤트를 발생시켜 의도치 않은 반응을 이끌어 낼수 있음.
    -> event키워드를 사용하면, 두 단점을 막을 수가 있다.
    -> 즉, 외부클래스에서 사용 불가능하고 += 또는 -=만 사용가능.
    public class Player
    {
       public int hp = 100;
       public event Action<int> OnChangeHP;
       public void Hit(int damage)
       {
           hp -= damage;
           Console.WriteLine($"플레이어가 데미지를 받아 체력이 {hp} 이 되었습니다.");
           if (OnChangeHP != null)
               OnChangeHP(hp);
       }
    }
    public class UI
    {
       // 이벤트 발생 시점에 호출당할 함수
       // 이벤트 발생 시점에 반드시 호출당하기 때문에 주기적인 실행이 필요없음
       public void SetHP(int hp)
       {
           Console.WriteLine($"UI의 체력표시를 {hp} 으로 변경합니다.");
       }
    }
    internal class Program
    {
       public static void Main()
       {
           Player player = new Player();
           UI ui = new UI();
           player.OnChangeHP += ui.SetHP;
           //외부클래스에서 직접 호출 불가능 -> 컴파일 오류발생
           player.OnChangeHP?.Invoke(20);
           player.Hit(20);
       }
    }
- player.Hit과 같이 event delegate를 생성한 클래스 내부에서만 호출이 가능하다.

2. 익명 함수


2-1. 익명 함수란

  • 이름이 없는 함수.
  • 주로 재사용 될 여지가 없는 기능 등, 일회성 사용을 위해 메서드가 선언되는 경우 사용하는 기능.
  • 코드 내용이 간결하고 직관적이지만, 남용되는 경우 코드 리뷰와 이해가 오랜 시간이 걸리고 디버깅이 어려울 수 있다.

2-2. 사용 방법

internal class Program
{
    public delegate void VoidDel();
    public delegate int IntDel(params int[] values);
    public static VoidDel voidDel;
    public static IntDel intDel;
    public static void Main()
    {
        //이름이 없기 때문에 delegate를 통하지 않으면 함수를 프로그램내에 실행할수 없다
        voidDel += delegate ()
        {
            Console.WriteLine("익명 함수!");
        };
        intDel += delegate (int[] values)
        {
            int result = 0;
            foreach (int v in values)
            {
                result += v;
            }
            return result;
        };
        //람다식선언
        voidDel += () => Console.WriteLine("Lamda 선언!");
        intDel += (values) =>
        {
            int result = 0;
            foreach (int i in values)
            {
                result += i;
            }
            return result;
        };
        voidDel?.Invoke();
        Console.WriteLine(intDel?.Invoke(5, 4, 2, 4, 1));
    }
}
public static class Util
{
    public static void PrintLog()
    {
        Console.WriteLine("Print Log");
    }
    public static int Sum(params int[] values)
    {
        int result = 0;
        foreach(int v in values)
        {
            result += v;
        }
        return result;
    }
    public static void CreateClosure()
    {
        int count = 0;
        Program.voidDel += () =>
        {
            count++;
            Console.WriteLine(count);
        };
    }
}
  • delegate 방식선언과 람다식 방식 선언이 존재.

2-3. Closure

internal class Program
{
    public delegate void VoidDel();
    public delegate int IntDel(params int[] values);
    public static VoidDel voidDel;
    public static IntDel intDel;
    public static void Main()
    {
        Util.CreateClosure();
        voidDel?.Invoke();
        voidDel?.Invoke();
        voidDel?.Invoke();
        voidDel?.Invoke();
    }
}
public static class Util
{
    public static void PrintLog()
    {
        Console.WriteLine("Print Log");
    }
    public static int Sum(params int[] values)
    {
        int result = 0;
        foreach(int v in values)
        {
            result += v;
        }
        return result;
    }
    public static void CreateClosure()
    {
        int count = 0;
        //클로저
        Program.voidDel += () =>
        {
        //캡쳐
            count++;
            Console.WriteLine(count);
        };
    }
}
  • 위 코드 처럼, CreateClosure()을 호출하고 종료되면
    그 안에 count 변수는 메모리에서 해제가 될것이다.
  • 여기서 문제는 할당될 count변수를 익명함수가 참조하고 있다.
  • 에러가 발생할거라 예상할 수 있지만, 에러가 발생하지 않는다.
    -> 외부의 변수나 객체를 익명함수에서 참조할 경우,
    해당 변수의 생명주기는 익명함수가 더 이상 호출되지 않을 때까지로 연장.
    -> 참조하는 변수와 익명 함수를 임시 클래스로 만들어 heap메모리에 할당하기 때문!
    -> 해당 익명함수 자체를 클로저라고 부르고, 클로저가 변수를 캡쳐 했다고 표현
  • 실수에 의해 지속적으로 closure가 누적되면 메모리 누수가 발생할 수 있으니 조심할 필요가 있음!
profile
개발노트

0개의 댓글