델리게이트와 람다, 버블정렬

이정현
∙∙조회 5

20261028 델리게이트와 람다, 버블정렬

개요

델리게이트도 사용자 정의 데이터 형이다. 메서드 자체를 값처럼 다루게 해 주는 타입이다. 메서드를 변수에 담고, 매개변수로 넘기고, 여러개를 묶어 한꺼번에 호출할 수 있음.

Mermaid
렌더링 중...

구조는 같고 한 군데만 다른 코드

다음은 배열의 모든 요소를 제곱하는 메서드와, 두배로 만드는 메서드임.

csharp
// 한군데만 다른 코드 예시
static void TransformToSquare(int[] arr)
{
    for (int i = 0; i < arr.Length; i++)
    {
        arr[i] = arr[i] * arr[i];
    }
}

static void TransformToDouble(int[] arr)
{
    for (int i = 0; i < arr.Length; i++)
    {
        arr[i] = arr[i] * 2;
    }
}

두 메서드에서 다른 부분은 arr[i] * arr[i] 와 arr[i] * 2 한 줄뿐임. 나머지 반복 구조는 글자 그대로 같음. 세제곱·절댓값·부호 반전이 필요해지면 그때마다 같은 반복문을 다시 쓰게 됨.

값이 달라질 때 매개변수로 뽑아내는 것처럼, 동작이 달라질 때 그 동작을 매개변수로 뽑아내는 것이 델리게이트임.

csharp
// 위 예시를 델리게이트로 사용하는 예시
// 변환 방법을 인수로 넘김
Transform(a, Square);
Console.WriteLine(string.Join(", ", a));

Transform(b, Double);
Console.WriteLine(string.Join(", ", b));

static int Square(int x) => x * x;
static int Double(int x) => x * 2;

// 반복 구조는 하나뿐이고, 무엇으로 바꿀지는 밖에서 정함
static void Transform(int[] arr, Transformer t)
{
    for (int i = 0; i < arr.Length; i++)
    {
        arr[i] = t(arr[i]);
    }
}

// int 하나를 받아 int 하나를 돌려주는 메서드를 담는 타입
delegate int Transformer(int x);

Transform은 무엇으로 바꾸는지 모름. 그것은 호출하는 쪽이 정함.

델리게이트

메서드를 담는 타입

델리게이트는 메서드의 모양을 정의하는 타입임. 여기서 모양이란 반환 타입과 매개변수 목록을 말하며, 이를 시그니처(signature)라고 부름.

csharp
// 델리게이트 선언 예시
// 반환값도 매개변수도 없는 메서드를 담는 타입
delegate void SayDelegate();

// int 하나를 받아 int 를 돌려주는 메서드를 담는 타입
delegate int Transformer(int x);

// double 둘을 받아 double 을 돌려주는 메서드를 담는 타입
delegate double Calculator(double x, double y);

class나 struct처럼 델리게이트도 타입을 선언하는 것이므로, 선언 자체는 실행되는 코드가 아님. 델리게이트도 참조형 변수이다.

  • 참고: 최상위문을 쓰는 파일에서는 타입 선언이 실행문보다 뒤에 와야 함. 델리게이트 선언도 파일 맨 아래에 둠. 클래스 안에 선언할 때는 위치 제한이 없음.

델리게이트 인스턴스 만들기

선언한 타입에 실제 메서드를 담으면 델리게이트 인스턴스가 됨. 표기는 두 가지임.

csharp
// 두가지 표기 각각 예시
// 방법 1: 메서드 이름을 그대로 대입
SayDelegate say = Hi;
say();

// 방법 2: new 로 명시
SayDelegate say2 = new SayDelegate(Hi);
say2();

static void Hi() => Console.WriteLine("안녕하세요.");

delegate void SayDelegate();

메서드 이름 뒤에 괄호를 붙이지 않는다는 점이 중요함. Hi 는 메서드 자체를 가리키고, Hi() 는 메서드를 호출해 그 결과를 가리킴.

호출하는 두 가지 표기

델리게이트에 담긴 메서드를 실행하는 표기도 두 가지임.

csharp
// 두가지 표기 각각 예시
SayDelegate sayPointer = Hello;

// 표기 1: 변수에 괄호를 붙임
sayPointer();

// 표기 2: Invoke 를 명시
sayPointer.Invoke();

static void Hello() => Console.WriteLine("Hello Delegate");

delegate void SayDelegate();

둘은 완전히 같은 일을 함. Invoke 는 “이것은 델리게이트 호출”임을 코드에서 드러낼 때 씀. 어지간하면 Invoke()를 사용하는것이 좋다. sayPointer?.Invoke(); 처럼 해당 델리게이트가 null일 경우에 대한 예외 처리도 가능하고 읽기에도 명확하기 때문(?를 붙이면 null일경우 실행안함)

시그니처가 맞아야 담김

델리게이트에는 아무 메서드나 담기지 않음. 반환 타입과 매개변수가 선언과 일치해야 함. 반대로 시그니처만 맞으면 무엇이든 담김. 직접 만든 메서드가 아니어도 됨.

정적 메서드와 인스턴스 메서드

델리게이트는 둘 다 담을 수 있음. 정적 메서드는 타입 이름으로, 인스턴스 메서드는 객체 이름으로 접근함. 인스턴스 메서드를 담으면 그 객체까지 함께 붙잡힘. multiply 를 호출하면 calc 의 multiplier 를 씀. 델리게이트 하나에 “어떤 메서드”와 “어떤 객체”가 같이 들어 있는 셈임.

csharp
// 서로 다른 인스턴스 메서드 담은 예시
Calculator three = new Calculator(3);
Calculator ten = new Calculator(10);

Transformer byThree = three.Multiply;
Transformer byTen = ten.Multiply;

Console.WriteLine(byThree(7));
Console.WriteLine(byTen(7));

delegate int Transformer(int x);

class Calculator
{
    private int multiplier;

    public Calculator(int multiplier)
    {
        this.multiplier = multiplier;
    }

    public int Multiply(int x) => x * multiplier;
}

같은 메서드를 담았는데 결과가 다름. 붙잡힌 객체가 다르기 때문임.

동작을 밖에서 주입함

델리게이트를 매개변수로 받는 메서드는 뼈대만 갖고 있고 알맹이는 호출자에게 맡김.

csharp
// 밖에서 만든 동작 호출 예시
RunnerCall(Go);
Console.WriteLine();
RunnerCall(Back);

static void Go() => Console.WriteLine("직진");
static void Back() => Console.WriteLine("후진");

static void RunnerCall(Runner runner)
{
    Console.WriteLine("=== 실행 시작 ===");
    runner();
    Console.WriteLine("=== 실행 종료 ===");
}

delegate void Runner();

시작·종료 출력은 RunnerCall 이 책임지고, 가운데에서 무엇을 할지는 넘긴 쪽이 정함.

배열 전체에 적용하기

csharp
// 배열 전체에 적용하는 예시
int[] numbers = { 1, 2, 3, 4, 5 };

Transform(numbers, Square);
PrintArray(numbers);

numbers = new int[] { 1, 2, 3, 4, 5 };
Transform(numbers, Cube);
PrintArray(numbers);

numbers = new int[] { 1, 2, 3, 4, 5 };
Transform(numbers, Negate);
PrintArray(numbers);

static int Square(int x) => x * x;
static int Cube(int x) => x * x * x;
static int Negate(int x) => -x;

static void Transform(int[] values, Transformer t)
{
    for (int i = 0; i < values.Length; i++)
    {
        values[i] = t(values[i]);
    }
}

static void PrintArray(int[] arr) => Console.WriteLine(string.Join(", ", arr));

delegate int Transformer(int x);

변환 규칙이 세 가지로 늘어도 반복문은 하나뿐임.

조건을 넘기기

bool 을 돌려주는 델리게이트를 넘기면 “어떤 것을 고를지”를 밖에서 정하게 됨.

csharp
// 조건을 넘겨서 필요한 값만 받는 예시
int[] scores = { 88, 45, 92, 67, 71, 39, 100 };

Console.WriteLine("80점 이상: " + CountIf(scores, IsHigh));
Console.WriteLine("낙제 점수: " + CountIf(scores, IsFail));
Console.WriteLine("짝수 점수: " + CountIf(scores, IsEven));

static bool IsHigh(int score) => score >= 80;
static bool IsFail(int score) => score < 60;
static bool IsEven(int score) => score % 2 == 0;

static int CountIf(int[] values, Condition test)
{
    int count = 0;
    foreach (int v in values)
    {
        if (test(v))
        {
            count++;
        }
    }
    return count;
}

delegate bool Condition(int value);

델리게이트와 인터페이스

동작을 밖에서 주입하는 방법은 인터페이스로도 가능함. 둘의 차이는 다음과 같음.

항목델리게이트인터페이스
정의하는 것메서드 하나의 시그니처메서드 여러 개의 묶음
준비 비용기존 메서드를 그대로 대입클래스를 만들어 구현
여러 개 묶기+=로 가능불가능
상태를 함께 갖기인스턴스 메서드를 담으면 가능필드로 자유롭게 가능

메서드가 하나뿐이면 델리게이트가 짧음. 관련된 메서드가 여럿 묶여 다녀야 하면 인터페이스가 맞음.

  • 참고: 델리게이트로 가능한건 델리게이트로. 델리게이트로 안되는건 인터페이스로. 그래도 안될때만 따로. 보통 델리게이트로 하는것과 인터페이스로 하는것 둘다 구현해놓고 쓰는사람이 선택하도록 한다.

멀티캐스트 델리게이트

여러 메서드를 하나에 묶기

델리게이트 하나에 메서드 여러 개를 담을 수 있음. 이것을 멀티캐스트 델리게이트 라고 함. += 로 추가하고 -= 로 뺌.

csharp
// 멀티캐스트 델리게이트 예시
CarDriver go = GoForward;
go += GoLeft;
go += GoRight;

// 한 번 호출하면 담긴 것이 모두 실행됨
go();

Console.WriteLine();

go -= GoLeft;
go();

static void GoForward() => Console.WriteLine("직진");
static void GoLeft() => Console.WriteLine("좌회전");
static void GoRight() => Console.WriteLine("우회전");

delegate void CarDriver();

Mermaid
렌더링 중...

등록 순서대로 실행됨

null 인 델리게이트에 += 를 쓰면 그것이 첫 번째 항목이 됨. 시작값을 null 로 두고 붙여 나가는 표기가 자주 쓰임.

아무것도 담기지 않았을때

담긴 것이 하나도 없으면 델리게이트는 null 이 됨. 그대로 호출하면 실행 중에 멈춤. 담긴 것이 있는지 확인하고 호출해야 함.

csharp
// null호출 및 호출 전 null상태 확인 예시
CarDriver go = GoForward;
go -= GoForward;      // 마지막 하나를 빼면 null 이 됨

Console.WriteLine(go == null);
// go();                    // 여기서 예외
if (go != null)             // 이렇게 null이 아닐때만 실행
{
    go();
}

static void GoForward() => Console.WriteLine("직진");

delegate void CarDriver();

반환값이 있으면 마지막 것만 남음

csharp
// 여러개의 반환값이 있는 델리게이트 실행시 반환값이 마지막 것만 남는 예시
Transformer t = Square;
t += Cube;

// 둘이 모두 실행되지만 받는 값은 마지막 것뿐임
Console.WriteLine(t(3));

static int Square(int x)
{
    Console.WriteLine($"  Square({x}) 실행");
    return x * x;
}

static int Cube(int x)
{
    Console.WriteLine($"  Cube({x}) 실행");
    return x * x * x;
}

delegate int Transformer(int x);

scss
실행결과
  Square(3) 실행
  Cube(3) 실행
27

Square 가 돌려준 9 는 버려짐. 그래서 멀티캐스트로 쓸 델리게이트는 반환값이 없는 것(void)으로 선언하는 것이 보통임.

중간에서 예외가 나면 나머지

아래 예제의 throw 는 “여기서 문제가 생겼다”고 알리며 실행을 중단시키는 문장임. 그것을 붙잡아 처리하는 방법은 여기서 다루지 않음.

csharp
// 예외 발생시에 대한 예시
Reporter r = Step1;
r += Step2;
r += Step3;

r();

static void Step1() => Console.WriteLine("1단계 완료");
static void Step2() => throw new Exception("2단계 실패");
static void Step3() => Console.WriteLine("3단계 완료");

delegate void Reporter();

마지막 줄 아래에는 오류가 난 위치까지의 호출 경로가 함께 출력됨. Step3 은 실행되지 않았음. 여러 구독자를 묶을 때 유의할 점임.

진행률 보고

csharp
// 진행률 보고에 델리게이트를 사용하는 예시
ProgressReporter reporter = WriteToConsole;
reporter += WriteToLog;

for (int i = 0; i <= 100; i += 50)
{
    reporter(i);
    Console.WriteLine();
}

static void WriteToConsole(int percent) => Console.WriteLine($"진행률: {percent}%");
static void WriteToLog(int percent) => Console.WriteLine($"[기록] {percent}%");

delegate void ProgressReporter(int percent);

진행률을 만드는 쪽은 그것을 누가 받아 무엇을 하는지 몰라도 됨. 받는 쪽이 늘어도 만드는 쪽 코드는 그대로임.

람다식

이름 없이 그 자리에 쓰기

지금까지는 델리게이트에 담을 메서드를 미리 이름 붙여 선언했음. 그런데 한 번만 쓰고 마는 짧은 동작에까지 이름을 붙이는 것은 번거로움. 람다식(lambda expression) 은 이름 없는 메서드를 쓰는 자리에 바로 적는 표기임.

csharp
// 람다식 사용 예시
// 이름 붙인 메서드를 담는 방식
Transformer square1 = Square;
Console.WriteLine(square1(4));

// 람다식으로 그 자리에 적는 방식
Transformer square2 = x => x * x;
Console.WriteLine(square2(4));

static int Square(int x) => x * x;

delegate int Transformer(int x);

람다 연산자

=> 를 람다 연산자 라고 하고 “goes to” 로 읽음. 왼쪽에 매개변수를, 오른쪽에 실행할 내용을 적음. (매개변수) => 식-또는-문장블록 같은 => 를 static int Square(int x) => x * x; 처럼 메서드 선언 뒤에 붙여 쓰기도 함. 그쪽은 이름이 있는 메서드의 본문을 한 줄로 줄이는 표기(식 본문 멤버)이고, 람다식은 이름 자체를 만들지 않는 표기임. 기호는 같고, => 왼쪽에 오는 것이 메서드 선언인지 매개변수인지로 갈림.

매개변수가 없으면 빈 괄호를 적음.

csharp
// 매개변수가 없는 경우 예시
PrintMessage hello = () => Console.WriteLine("안녕하세요.");
hello();

delegate void PrintMessage();

매개변수가 하나일 때

괄호를 생략할 수 있음.

csharp
// 매개변수가 하나일 때 괄호 생략하는 예시
Transformer square = x => x * x;
Transformer negate = (x) => -x;      // 괄호를 써도 됨

Console.WriteLine(square(3));
Console.WriteLine(negate(3));

delegate int Transformer(int i);

매개변수 x 의 타입을 적지 않았는데도 동작함. 델리게이트 타입이 int 하나를 받는다고 정해 두었으므로 컴파일러가 그것을 알아냄. 이를 타입 추론 이라고 함.

매개변수가 둘 이상일 때

괄호를 통해서 명시해주어야 함

csharp
// 매개변수 둘 이상일 때의 예시
IsLonger isLong = (msg, len) => msg.Length > len;

// 타입을 직접 적을 수도 있음. 어느 타입인지 코드에서 바로 보이게 하고 싶을 때 씀.
// IsLonger isLong = (string msg, int len) => msg.Length > len;
// 타입을 적을 때는 전부 적거나 전부 생략해야 함. 섞어 쓸 수 없음. 섞어쓰면 에러 발생
// IsLonger isLong = (string msg, len) => msg.Length > len; // 이 경우 에러 발생

Console.WriteLine(isLong("안녕하세요.", 5));
Console.WriteLine(isLong("반갑습니다.", 10));

delegate bool IsLonger(string msg, int len);

식 람다와 문장 람다

=> 오른쪽에 식 하나만 오는 형태를 식 람다라고 함. 그 식의 결과가 그대로 반환값이 되므로 return 을 적지 않음. 식 람다에서 return 을 적으면 오류임. 예시: Transformer square = x => x * x;

여러 문장을 실행해야 하면 중괄호로 묶음. 이것을 문장 람다라고 함. 문장 람다에서 값을 돌려주려면 return 을 명시해야 함.

csharp
// 문장 람다 사용 예시
Greeting greet = () =>
{
    Console.WriteLine("안녕하세요.");
    Console.WriteLine("반갑습니다.");
};
delegate void Greeting();

// 반환값이 있는 문장 람다 사용 예시
Calculator add = (a, b) =>
{
    int result = a + b;
    Console.WriteLine($"{a} + {b} 를 계산함");
    return result;
};
delegate int Calculator(int a, int b);

형태표기return
식 람다x => x * x적지않음(식의 값이 반환됨)
문장 람다x => { ... }값을 돌려주려면 적어야 함

람다식을 그 자리에서 넘기기

람다식의 이점은 변수에 담지 않고 인수 자리에 바로 적을 수 있다는 데 있음.

csharp
// 람다식을 이용하여 조건에 맞는 것만 출력하는 예시
int[] scores = { 88, 45, 92, 67, 71, 39, 100 };

Console.WriteLine("80점 이상: " + CountIf(scores, s => s >= 80));
Console.WriteLine("낙제 점수: " + CountIf(scores, s => s < 60));
Console.WriteLine("짝수 점수: " + CountIf(scores, s => s % 2 == 0));
Console.WriteLine("60~90점: " + CountIf(scores, s => s >= 60 && s <= 90));

static int CountIf(int[] values, Condition test)
{
    int count = 0;
    foreach (int v in values)
    {
        if (test(v))
        {
            count++;
        }
    }
    return count;
}

delegate bool Condition(int value);

앞에서 같은 일을 이름 붙인 메서드 셋으로 했던 것과 견주면 차이가 분명함. 조건이 코드를 읽는 자리에 그대로 보임.

익명 메서드

람다식이 나오기 전에는 delegate 키워드로 이름 없는 메서드를 적었음. 이것을 익명 메서드 라고 함. 람다식이 나와서 이제 잘 사용하지 않는다. 옛날 문법이지만 사용하려면 할 수는 있다. 오래된 코드에는 있을 수 있다.

csharp
// 익명 메서드 사용 예시
Transformer old = delegate (int x) { return x * x; };

// 람다식 표기 — 같은 일을 함
Transformer now = x => x * x;

Console.WriteLine(old(6));
Console.WriteLine(now(6));

delegate int Transformer(int x);

항목익명 메서드람다식
표기delegate (매개변수) { ... }(매개변수) => ...
식 하나만 적는 형태없음(항상 중괄호)있음
매개변수 타입 생략불가가능
매개변수 자체를 생략가능불가

익명 메서드는 매개변수를 아예 적지 않을 수 있다는 점만 다름. 쓰지 않는 매개변수를 생략하는 용도임.

csharp
// 익명메서드와 람다식 비교
Handler h1 = delegate { Console.WriteLine("익명 메서드 — 매개변수 생략"); };
Handler h2 = (a, b) => Console.WriteLine("람다식 — 매개변수를 적어야 함");

h1(1, 2);
h2(1, 2);

delegate void Handler(int a, int b);

이 차이 하나를 빼면 람다식이 더 짧으므로, 요즘 코드는 람다식을 씀. 익명 메서드 표기는 오래된 코드를 읽을 때 알아볼 수 있으면 됨.

내장 델리게이트 타입

매번 선언하지 않아도 됨

지금까지 예제마다 delegate int Transformer(int x); 같은 선언을 새로 적었음. 그런데 자주 쓰이는 시그니처는 .NET 이 델리게이트 타입으로 미리 만들어 두었음.

타입의미
Action매개변수 없음, 반환값 없음
Action<T>T하나를 받고 반환값 없음
Action<T1, T2>매개변수 둘, 반환값 없음
Func<TResult>매개변수 없음, TResult반환
Func<T, TResult>T하나를 받고 TResult반환
Func<T1, T2, TResult>매개변수 둘, TResult반환
Predicate<T>T하나를 받고 bool반환
Action 과 Func는 매개변수를 16개까지 받는 형태가 준비되어 있음.
  • 참고: 꺾쇠 안에 타입을 적어 넣는 표기를 제네릭 이라고 함. 여기서는 “이 자리에 타입 이름을 적는다”는 것만 알면 됨.

Action - 반환값이 없을 때

csharp
// Action 사용 예시
Action greet = () => Console.WriteLine("안녕하세요!");
greet();

Action<string> print = msg => Console.WriteLine(msg);
print("Hello, Action!");

Action<string, int> printRepeat = (msg, count) =>
{
    for (int i = 0; i < count; i++)
    {
        Console.WriteLine(msg);
    }
};
printRepeat("반복", 3);

Func - 반환값이 있을 때

타입 인수 중 맨 마지막이 반환 타입임. 앞의 것들은 매개변수 타입임.

csharp
// Func 사용 예시
Func<int> getNumber = () => 42;
Console.WriteLine(getNumber());

Func<int, int> square = x => x * x;
Console.WriteLine(square(5));

Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 7));

// 받는 타입과 돌려주는 타입이 달라도 됨
Func<int, string> describe = n => $"숫자 {n}";
Console.WriteLine(describe(9));

Func<int, string> 은 int 를 받아 string 을 돌려줌. 순서를 바꾸면 뜻이 달라짐.

Predicate - 조건을 나타낼 때

bool 을 돌려주는 것이 정해져 있으므로 반환 타입을 적지 않음. Predicate<int> 는 Func<int, bool> 과 같은 모양임.

csharp
// Predicate 사용 예시
Predicate<int> isEven = x => x % 2 == 0;
Func<int, bool> isPositive = x => x > 0;

Console.WriteLine(isEven(4));
Console.WriteLine(isEven(7));
Console.WriteLine(isPositive(-3));

둘은 모양이 같지만 타입으로는 서로 다름. 한쪽에 다른 쪽을 그대로 대입할 수 없음.

직접 선언한 델리게이트 대신 쓰기

앞의 CountIf를 사용했던 예제를 내장 타입으로 다시 쓰면 델리게이트 선언이 사라짐.

csharp
// 내장 델리게이트로 변환한 예시
int[] scores = { 88, 45, 92, 67, 71, 39, 100 };

Console.WriteLine("80점 이상: " + CountIf(scores, s => s >= 80));
Console.WriteLine("낙제 점수: " + CountIf(scores, s => s < 60));

// delegate 선언 없이 Func 를 그대로 씀
static int CountIf(int[] values, Func<int, bool> test)
{
    int count = 0;
    foreach (int v in values)
    {
        if (test(v))
        {
            count++;
        }
    }
    return count;
}

새 델리게이트 타입을 선언할 이유는 이름 자체가 의미를 갖게 하고 싶을 때 정도임. Func<int, bool> 보다 Condition 이 읽기 좋다고 판단하면 선언해도 됨.

내장 델리게이트도 멀티캐스트가 됨

csharp
// 내장 델리게이트로 멀티캐스팅 하는 예시
Action driver = () => Console.WriteLine("직진");
driver += () => Console.WriteLine("좌회전");
driver += () => Console.WriteLine("우회전");

driver();

배열 메서드에 넘기기

.NET 의 Array 는 델리게이트를 받는 메서드를 여럿 갖고 있음. 람다식과 함께 쓰면 반복문을 직접 쓰지 않아도 됨.

csharp
// Array 사용 예시
int[] numbers = { 12, 7, 30, 5, 18, 25 };

// Find: 조건에 맞는 첫 요소
int firstBig = Array.Find(numbers, n => n > 15);
Console.WriteLine($"15보다 큰 첫 수: {firstBig}");

// FindAll: 조건에 맞는 모든 요소
int[] evens = Array.FindAll(numbers, n => n % 2 == 0);
Console.WriteLine($"짝수: {string.Join(", ", evens)}");

// Exists: 하나라도 있는지
Console.WriteLine($"50 초과가 있는가: {Array.Exists(numbers, n => n > 50)}");

// TrueForAll: 모두 그러한지
Console.WriteLine($"모두 양수인가: {Array.TrueForAll(numbers, n => n > 0)}");

// FindIndex: 조건에 맞는 첫 요소의 자리
Console.WriteLine($"30의 위치: {Array.FindIndex(numbers, n => n == 30)}");

// ConvertAll: 모든 요소를 바꿔 새 배열로
string[] labels = Array.ConvertAll(numbers, n => $"[{n}]");
Console.WriteLine(string.Join(" ", labels));

정렬 기준도 람다식으로 넘길 수 있음. 두 값을 받아 앞의 것이 뒤로 가야 하면 양수, 앞에 와야 하면 음수를 돌려주는 규칙임.

csharp
// Array에 람다식을 사용하는 예시
string[] names = { "김철수", "이나", "남궁민수", "박영" };

// 이름 길이가 짧은 것부터
Array.Sort(names, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", names));

// 사전 역순
Array.Sort(names, (a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(", ", names));

외부 변수 캡처

람다는 바깥의 변수를 쓸 수 있음

람다식은 자기가 적힌 자리에서 보이는 변수를 그대로 씀. 이것을 캡처(capture) 라고 하고, 바깥 변수를 붙잡은 람다식을 클로저(closure) 라고 부름.

csharp
// 캡쳐, 클로저 예시
int factor = 2;
Func<int, int> multiplier = n => n * factor;

Console.WriteLine(multiplier(3));
Console.WriteLine(multiplier(10));

factor 는 람다식의 매개변수가 아닌데도 쓰였음.

값이 아니라 변수 자체를 붙잡음

캡처는 그 시점의 값을 복사해 두는 것이 아님. 변수 자체를 붙잡으므로, 나중에 값이 바뀌면 람다식의 동작도 바뀜.

csharp
// 캡처 예시1
int factor = 2;
Func<int, int> multiplier = n => n * factor;

Console.WriteLine(multiplier(3));   // 출력결과 6

factor = 10;                        // 람다식을 만든 뒤에 바꿈
Console.WriteLine(multiplier(3));   // 출력결과 30

반대 방향도 성립함. 람다식 안에서 바깥 변수를 고치면 바깥에도 반영됨.

csharp
// 캡처 예시2
int count = 0;
Action increment = () => count++;

increment();
increment();
increment();

Console.WriteLine(count);   // 출력결과 3

지역 변수의 수명이 늘어남

지역 변수는 보통 메서드가 끝나면 사라짐. 그런데 람다식이 붙잡고 있으면 그 람다식이 살아 있는 동안 함께 남음.

csharp
// 지역변수를 캡처한 예시
Func<int, int> tracker = CreateMaxTracker();

Console.WriteLine(tracker(30));
Console.WriteLine(tracker(12));
Console.WriteLine(tracker(47));
Console.WriteLine(tracker(41));

// 지금까지 본 값 중 가장 큰 것을 기억함
static Func<int, int> CreateMaxTracker()
{
    int max = int.MinValue;        // 이 메서드의 지역 변수
    return value =>
    {
        if (value > max)
        {
            max = value;
        }
        return max;
    };
}

CreateMaxTracker 는 이미 끝났는데 max 는 살아서 값을 유지하고 있음. 돌려준 람다식이 그것을 붙잡고 있기 때문임. 호출할 때마다 새 max 가 만들어지므로, 추적기를 둘 만들면 서로 간섭하지 않음.

csharp
// 호출을 2번하여 따로 생성한 예시
Func<int, int> a = CreateMaxTracker();
Func<int, int> b = CreateMaxTracker();

Console.WriteLine(a(50));
Console.WriteLine(b(7));
Console.WriteLine(a(20));
Console.WriteLine(b(9));

static Func<int, int> CreateMaxTracker()
{
    int max = int.MinValue;
    return value =>
    {
        if (value > max)
        {
            max = value;
        }
        return max;
    };
}

반복 변수를 캡처할 때

for 문의 반복 변수를 캡처하면 예상과 다른 결과가 나옴.

csharp
// 반복변수 캡처 예시
Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
    actions[i] = () => Console.Write(i);
}

foreach (Action a in actions)
{
    a();
}
Console.WriteLine();

plain text
출력결과
333

012 가 아니라 333 임. for 문의 i 는 반복마다 새로 만들어지는 것이 아니라 하나를 계속 고쳐 쓰는 변수이고, 세 람다식이 모두 그 하나를 붙잡았기 때문임. 실행 시점에는 반복이 끝나 i 가 3 이 되어 있음. 반복 안에서 변수를 새로 선언하면 해결됨. 반복마다 새 변수가 만들어지므로 각 람다식이 서로 다른 것을 붙잡음. foreach 의 반복 변수는 이 문제가 없음. 반복마다 새 변수로 취급되기 때문임.

csharp
// foreach의 캡처 예시
Action[] actions = new Action[3];
int[] values = { 0, 1, 2 };
int index = 0;

foreach (int v in values)
{
    actions[index] = () => Console.Write(v);
    index++;
}

foreach (Action a in actions)
{
    a();
}
Console.WriteLine();

  • 주의사항: 람다식을 만든 자리와 실행되는 자리가 떨어져 있을 때 캡처한 변수가 그 사이에 바뀌면 결과가 달라짐. 델리게이트를 모아 두었다가 나중에 실행하는 코드에서 특히 주의할 것.

버블정렬

한 회차가 끝날 때마다 가장 오른쪽 숫자가 정해진다. 배열 길이만큼 순회 하면서 2개의 Index를 비교해서 조건을 만족하면 Swap 만족하지 못하면 그대로 둔채로 다음 비교로 넘어감.

csharp
// 버블정렬 구현 예시
public static void Sort<T>(T[] array, Func<T, T, int> comparison)
{
    for (int i = 0; i < array.Length; i++)
    {
        for(int j = 0; j < array.Length - 1 - i; j++)
        {
            if (comparison(array[j], array[j + 1]) > 0)
            {
                T temp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = temp;
            }
        }
    }
}