C#

[C#] 람다식, 식 트리

sunlight-dby 2025. 5. 12. 17:26

 

'이것이 C#이다 개정판'의 chapter 14를 정리한 글입니다.

 


 

람다식 (Lambda Expression)

람다식은 익명 메서드를 만들기 위해 사용됩니다.

다만, 람다식으로 만드는 익명 메서드는 무명 함수(Anonymous Function)이라는 이름으로 불립니다.

매개변수_목록 => 식

 

※ '=> 연산자'는 '입력 연산자'로 매개변수를 전달하는 역할을 합니다.

 

[예시]

// 익명 메서드를 만들기 위한 대리자 생성
delegate int Calculate(int a, int b);

// ...

static void Main(string[] args)
{
    Calculate calc = (int a, int b) => a + b;  // 람다식으로 만든 익명 메서드
}

 

예시의 코드처럼, 두 개의 int 형식 매개변수 a, b를 받아 더해 반환하는 익명 메서드를 람다식으로 간결하게 만들었습니다.

 

여기에 더해, C# 컴파일러는 이 코드를 "형식 유추(Type Inference)"라는 기능을 제공합니다.

C# 컴파일러는 대리자의 선언 코드로부터 람다식이 만드는 익명 메서드의 매개변수의 형식을 유추합니다.

 

형식 유추로 위의 예시 코드의 람다식에서 매개변수의 형식을 제거할 수 있습니다.

 

[예시]

delegate int Calculate(int a, int b);

// ...

static void Main(string[] args)
{
    Calculate calc = (a, b) => a + b;  // 형식 유추 기능을 사용한 람다식
}

 

 

[예제]

using System;

namespace SimpleLambda
{
    class MainApp
    {
        delegate int Calculate(int a, int b);

        static void Main(string[] args)
        {
            Calculate calc_Method = delegate(int a, int b)
                                    { return a + b; };

            Console.WriteLine($"Calc_Anonymous Method : 3 + 4 = {calc_Method(3, 4)}");


            Calculate calc_Function = (a, b) => a + b;

            Console.WriteLine($"Calc_Anonymous Function : 3 + 4 =  {calc_Function(3, 4)}");
        }
    }
}

실행 결과


문 형식의 람다식 (Statement Lambda)

문 형식의 람다식은 기존 '식(Expression) 형식'이 아닌, '문(Statement) 형식'으로 람다식을 활용합니다.

(매개변수_목록) => {
                      문장1;
                      문장2;
                      문장3;
                      ...
                    }

 

※ 매개변수가 없는 경우에는 매개변수_목록을 넣는 괄호 안에 아무것도 넣지 않습니다.

 

[예제]

using System;

namespace StatementLambda
{
    class MainApp
    {
        delegate string Concatenate(string[] args);

        static void Main(string[] args)
        {
            Concatenate concat =
                (arr) =>
                {
                    string result = "";
                    foreach (string s in arr)
                        result += s;

                    return result;
                };

            Console.WriteLine(concat(args));
        }
    }
}

실행 결과


Func와 Action을 활용한 더 간편한 무명 함수

익명 메서드와 무명 함수는 코드를 더 간결하게 만들어주는 요소들입니다.

하지만 이들을 선언하기 위해서 단 하나의 익명 메서드나 무명 함수일지라도, 매번 별개의 대리자를 선언해야 합니다.

 

이 문제를 해결하기 위해 마이크로소프트는 .NET에 Func와 Action 대리자를 미리 선언해뒀습니다.

Func 대리자는 겨로가를 반환하는 메서드를, Action 대리자는 결과를 반환하지 않는 메서드를 참조합니다.


Func 대리자

Func 대리자는 결과를 반환하는 메서드를 참조하기 위해 만들어졌습니다.

.NET에는 아래와 같이 총 17가지 버전의 Func 대리자가 준비되어 있습니다.

Func 대리자의 형식 매개변수 중 가장 마지막에 있는 변수(TResult)가 반환 형식입니다.

public delegate TResult Func<out TResult>()
public delegate TResult Func<in T, out TResult>(T arg)
public delegate TResult Func<int T1, in T2, out TResult>(T1 arg1, T2 arg2)
public delegate TResult Func<int T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3)
...
public delegate TResult Func<int T1, in T2, in T3, ..., in T15, out TResult>
                            (T1 arg1, T2 arg2, T3 arg3, ..., T15 arg15)
public delegate TResult Func<int T1, in T2, in T3, ..., in T15, in T16, out TResult>
                            (T1 arg1, T2 arg2, T3 arg3, ..., T15 arg15, T16 arg16)

 

이렇듯 Func 대리자는 입력 매개변수가 하나도 없는 것부터 16개에 이르는 것까지 버전이 다양하기 때문에 어지간한 경우가 아니면 별도의 대리자를 만들어 쓸 필요가 없습니다.

 

[예제]

using System;

namespace FuncTest
{
    class MainApp
    {
        static void Main(string[] args)
        {
            Func<int> func1 = () => 12;
            Console.WriteLine($"func1() : {func1()}");

            Func<int, int> func2 = (x) => x * 2;
            Console.WriteLine($"func2(4) : {func2(4)}");

            Func<double, double, double> func3 = (x, y) => x / y;
            Console.WriteLine($"func3(22, 7) : {func3(22, 7)}");
        }
    }
}

실행 결과


Action 대리자

Action 대리자는 Func 대리자와 거의 똑같습니다. 차이점이라면 Action 대리자는 반환 형식이 없다는 것뿐입니다.

Action 대리자도 Func 대리자처럼 17개 버전이 선언되어 있습니다.

public delegate void Action<>()
public delegate void Action<in T>(T arg)
public delegate void Action<int T1, in T2>(T1 arg1, T2 arg2)
public delegate void Action<int T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3)
...
public delegate void Action<int T1, in T2, in T3, ..., in T15>
                            (T1 arg1, T2 arg2, T3 arg3, ..., T15 arg15)
public delegate void Action<int T1, in T2, in T3, ..., in T15, in T16>
                            (T1 arg1, T2 arg2, T3 arg3, ..., T15 arg15, T16 arg16)

 

[예제]

using System;

namespace ActionDelegate
{
    class MainApp
    {
        static void Main(string[] args)
        {
            Action act1 = () => Console.WriteLine("Action()");
            act1();

            int result = 0;
            Action<int> act2 = (x) => result = x * x;  
                                    // 람다식 밖에서 선언한 result에 x * x의 결과를 저장합니다.

            act2(3);
            Console.WriteLine($"result : {result}");

            Action<double, double> act3 = (x, y) =>
            {
                double pi = x / y;
                Console.WriteLine($"Action<T1, T2>({x}, {y}) : {pi}");
            };

            act3(22.0, 7.0);
        }
    }
}

실행 결과


식 트리 (Expression Tree)

식 트리는 한 부모 노드가 단 두 개만의 자식 노드를 가질 수 있는 '이진 트리(Binary Tree)'입니다.

식 트리란, 식을 트리로 표현한 자료 구조를 말합니다.

예를 들어 1 x 2 + ( 7 - 8 ) 이라는 식을 식 트리로 표현하면 다음과 같습니다.

 

루트 노드 (최상위 노드)

A

 

단말(Terminal) 노드 (잎 노드)

D, E, F, G

 

 

 

 

 

 

식 트리에서 연산자는 부모 노드가 되며, 피 연산자는 자식 노드가 됩니다.

식 트리로 표현된 식은 트리의 단말 노드부터 계산해서 루트까지 올라가 전체 식의 결과를 얻습니다.

 

식 트리 자료구조는 컴파일러나 인터프리터를 제작하는 데도 응용됩니다.

 

 

컴파일러는 프로그래밍 언어의 문법을 따라 작성된 소스 코드를 분석해서 식 트리로 만든 후 이를 바탕으로 실행 파일을 만듭니다.

컴파일러는 개발자가 .cs 파일로 작성한 소스 코드를 한 줄씩 읽어들이는 것이 아닙니다.

 

컴파일러는 언어의 문법 규칙에 따라 코드의 구조와 의미를 파악하고, 이 분석 과정의 결과물로 소스 코드의 계층적 구조를 나타내는 내부적인 데이터 구조를 만듭니다. 이는 흔히 추상 구문 트리(Abstract Syntax Tree, AST)라고 불립니다.

 

해당 트리는 코드의 각 부분(변수 선언, 연산, 메서드 호출, 조건문, 반복문 등)을 노드로 표현하고, 노드 간의 관계를 부모 - 자식 관계로 연결하여 코드의 전체적인 논리 구조를 나타냅니다.

 

예를 들어,  'int result = a + b * 5;'라는 코드를 컴파일러 내부적으로 대략 다음과 같은 트리 구조로 표현할 수 있습니다.

int result = a + b * 5; 의 트리 표현

 

 

 

소스 코드 전체를 내부 구조로 만드는 C# 컴파일러와는 다른 역할과 범위를 가지긴 하지만,
C#은 프로그래머가 C# 코드 안에서 직접 식 트리를 조립하고 컴파일해서 사용할 수 있도록 기능을 제공합니다.

 

다시말해, 그 자체로는 실행 가능한 코드가 아니지만 런타임에 식 트리 객체를 생성할 수 있습니다.

컴파일 시점에 이미 코드가 확정되는 것이 아니라, 프로그램 실행 중에 필요에 따라 코드의 논리 구조를 식 트리로 정의하고, 그 코드를 실행 가능한 형태로 만들어 사용하는 것입니다.

 

식 트리를 다루는 데 필요한 클래스들은 System.Linq.Expressions 네임스페이스에 준비되어 있습니다.

Expression 클래스는 식 트리를 구성하는 노드를 표현하는 기능을 합니다.

아래의 표에 있는 Expression을 상속받는 파생 클래스들을 통해 식 트리의 각 노드를 표현할 수 있습니다.


Expression 클래스의 파생 클래스

Expression의 파생 클래스 설명
 BinaryExpression  이항 연산자(+, –, *, /, %, |, ^, <<, >>, &&, ||, ==, !=, >, >=, <, <=)를 갖는 식을 표현합니다.
 BlockExpression  변수를 정의할 수 있는 식을 갖는 블록을 표현합니다.
 ConditionalExpression  조건 연산자가 있는 식을 나타냅니다.
 ConstantExpression  상수가 있는 식을 나타냅니다.
 DefaultExpression  형식(type)이나 비어 있는 식의 기본값을 표현합니다.
 DynamicExpression  동적 작업을 나타냅니다.
 GotoExpression  return, break, continue, goto와 같은 점프문을 나타냅니다.
 IndexExpression  배열의 인덱스 참조를 나타냅니다.
 InvocationExpression  대리자나 람다식 호출을 나타냅니다.
 LabelExpression  레이블을 나타냅니다.
 LambdaExpression  람다식을 나타냅니다.
 ListInitExpression  컬렉션 이니셜라이저가 있는 생성자 호출을 나타냅니다.
 LoopExpression  무한 반복을 나타냅니다. 무한 반복은 break 문을 이용해서 종료할 수 있습니다.
 MemberExpression  객체의 필드나 속성을 나타냅니다.
 MemberInitExpression  생성자를 호출하고 새 객체의 멤버를 초기화하는 동작을 나타냅니다.
 MethodCallExpression  메서드 호출을 나타냅니다.
 NewArrayExpression  새 배열의 생성과 초기화를 나타냅니다.
 NewExpression  생성자 호출을 나타냅니다.
 ParameterExpression  명명된 인수를 나타냅니다.
 RuntimeVariablesExpression  변수에 대한 런타임 읽기/쓰기 권한을 제공합니다.
 SwitchExpression  다중 선택 제어 식을 나타냅니다.
 TryExpression  try~catch~finally 문을 나타냅니다.
 TypeBinaryExpression  형식 테스트를 비롯한 형식(Type)과 식(Expression)의 연산을 나타냅니다.
 UnaryExpression  단항 연산자를 갖는 식을 나타냅니다.

 

Expression 클래스는 식 트리를 구성하는 노드를 표현하는 것 외에도, 표에 열거된 파생 클래스들의 객체를 생성하는 역할도 담당합니다.

Expression 클래스 자신은 abstract로 선언되어 자신의 인스턴스는 만들 수 없지만, 파생 클래스의 인스턴스를 생성하는 정적 팩토리 메서드를 제공합니다.

 

[팩토리 메서드]

팩토리 메서드(Factory MEthod)는 클래스의 인스턴스를 생성하는 일을 담당하는 메서드를 가리키는 용어입니다.

C#에는 객체를 생성하는 생성자 메서드가 있지만, 객체의 생성에 복잡한 논리가 필요한 경우, 객체 생성 과정을 별도의 메서드에 구현해 놓아 코드의 복잡도를 상당히 줄일 수 있습니다.

 

Expression 클래스의 정적 팩토리 메서드들은 Expression 클래스의 파생 클래스인 ConstantExpression, BinaryExpression 클래스 등의 인스턴스를 생성하는 기능을 제공함으로써 따로 구현해줘야할 수고를 줄여줍니다.


식 트리를 사용하는 이유

식 트리는 코드를 데이터로써 보관할 수 있습니다. C#에서 식 트리는 주로 LINQ와 관련이되어 사용됩니다.

 

LINQ는 Language Integrated Query의 약자로, 데이터에 대한 쿼리(질의) 기능을 C# 언어 자체에 통합한 기술입니다.

과거에는 다양한 종류의 데이터 소스(데이터베이스, XML, 메모리 내 객체 컬렉션 등)에 접근할 때,

데이터베이스는 SQL, XML은 XPath/Xquery로 다루는 것처럼 각각 다른 방식으로 데이터를 다루고 쿼리해야 했습니다.

 

다양한 데이터 소스를 다룰 때, 그 종류에 따라 LINQ 쿼리를 처리하는 방식이 달리지며, 이때 식 트리가 결정적인 역할을 합니다.

LINQ 쿼리는 크게 두 가지 방식으로 처리됩니다.

 

[ LINQ to Object ]

데이터 소스가 IEnumerable<T> 인터페이스를 구현하는 메모리 내 컬렉션일 때,
LINQ 쿼리는 System.Linq 네임스페이스에 정의된 표준 LINQ 확장 메서드들을 통해 처리됩니다.

이 확장 메서드들은 Func 대리자나 Action 대리자같은 대리자를 인자로 받습니다.

컴파일러는 프로그래머가 메서드 구문으로 작성한 람다식을 이러한 대리자로 변환하고, 확장 메서드는 이 대리자를 호출하여 메모리 내 컬렉션을 직접 순회하며 데이터를 처리합니다.

 

이 경우에는 식 트리가 사용되지 않습니다.

 

[ LINQ to Providers ]

LINQ to SQL, LINQ to Entity Framework와 같은 경우가 이에 해당합니다.

데이터 소스가 메모리 외부에 있고 자체적인 쿼리 엔진을 가지는 경우인데, 이 경우 LINQ 쿼리는 해당 데이터 소스에 특화된 LINQ 공급자(Provider)를 통해 처리됩니다.

 

이 LINQ 공급자들은 IQueryable<T> 인터페이스를 구현하고, IQueryable<T> 인터페이스의 확장 메서드들(Where, Select 등)은 기존에 Func 대리자 대신 Expression<Func<⋯>> 타입의 인자를 받습니다.

 

이 LINQ to Providers 시나리오의 경우에, 식 트리가 필수적입니다.


식 트리의 사용 예시

Expression const = Expression.Constant(1);                  // 상수 1
Expression param = Expression.Parameter(typeof(int), "x");  // 매개변수 x, int x

Expression exp = Expression.Add( const, param );  // 1 + x

// 아래의 코드로 lambda는 (int x) => 1 + x라는 람다식의 구조 데이터를 가지게 됩니다.
// 아직 실행 가능한 코드가 아닙니다.
Expression<Func<int, int>> lambda = 
    Expression<Func<int, int>>.Lambda<Func<int, int>>(
        exp, new ParameterExpression[]{
        (ParameterExpression)param } );

Func<int, int> compiledExp = lambda.Compile();  // 실행가능한 코드로 컴파일

Console.WriteLine( compiledExp(3) );  // 컴파일한 무명 함수 실행
                                      // x = 3이면 1 + x = 4이므로 4를 출력

 

[ 팩토리 메서드로 생성한 객체 ]

상수를 표현하는 ConstantExpression 객체 하나와 매개변수를 표현하는 ParameterExpression 객체 하나를 선언하고,

이 둘에 대해 덧셈(+) 연산을 수행하는 BinaryExpression 객체를 선언합니다.

 

const는 ConstantExpression 형식이지만, Expression 형식으로 선언되었습니다.

ConstantExpression은 Expression을 상속하기 때문에 ConstantExpression 객체는 Expression 형식의 참조를 통해 가리킬 수 있습니다.

param도 역시 ParameterExpression의 인스턴스이고, exp도 BinaryExpression의 인스턴스이지만, Expression의 파생 클래스이기 때문에 Expression 형식의 참조를 통해 가리킬 수 있습니다.

 

이렇게 Expression의 파생 클래스이기 때문에 사용자는 각 노드가 어떤 타입인지 신경 쓰지 않고 Expression 형식의 참조를 선언해 사용할 수 있습니다. 필요한 경우에는 각 세부 형식으로 형식 변환을 하면 되기 때문입니다.

이것이 팩토리 메서드 패턴의 매력이고, const, param 등의 객체들은 Expression 클래스의 팩토리 메서드를 통해 생성됩니다.

 

[ 람다식으로의 컴파일 ]

식 트리는 결국 "식"을 트리로 표현한 것에 불과합니다. 다시 말해 코드에서 exp는 실행가능한 상태가 아니고, 그저 "데이터" 상태에 머물러 있습니다.

exp가 자신의 트리 자료구조 안에 정의된 식을 실행할 수 있으려면 람다식으로 컴파일되어야 합니다. 람다식으로의 컴파일은 다음과 같이 Expression<TDelegate> 클래스를 이용합니다.

Expression<TDelegate>는 앞의 표에도 나타나 있는 LambdaExpression 클래스의 파생 클래스입니다.

 

[ lambda.Compile() ]

앞서 만든' 람다 식 트리' 데이터 구조(lambda)를 분석하여, 프로그램 실행 중에 동적으로 실행 가능한 중간 언어(IL) 코드를 생성하고, 이 생성된 코드를 호출할 수 있는 대리자(여기서는 Func<int, int>)를 반환합니다.

 

해당 과정을 통해 compiledExp 변수는 이제 '(int x) => 1 + x' 논리를 수행하는 실제 실행 가능한 메서드에 대한 참조를 가집니다.


예제

using System;
using System.Linq.Expressions;

namespace UsingExpressionTree
{
    class MainApp
    {
        static void Main(string[] args)
        {
            // 1 * 2 + (x - y)
            Expression const1 = Expression.Constant(1);
            Expression const2 = Expression.Constant(2);

            Expression leftExp = Expression.Multiply(const1, const2);  // 1 * 2

            Expression param1 = Expression.Parameter(typeof(int));  // x를 위한 변수
            Expression param2 = Expression.Parameter(typeof(int));  // y를 위한 변수

            Expression rightExp = Expression.Subtract(param1, param2);  // x - y

            Expression exp = Expression.Add(leftExp, rightExp);

            Expression<Func<int, int, int>> expression =
                Expression<Func<int, int, int>>.Lambda<Func<int, int, int>>(
                    exp, new ParameterExpression[] {
                        (ParameterExpression)param1,
                        (ParameterExpression)param2 });

            Func<int, int, int> func = expression.Compile();

            // x = 7, y = 8
            Console.WriteLine($"1 * 2 + ({7} - {8} = {func(7, 8)}");
        }
    }
}

실행 결과

 

해당 예제 코드에서 expression 객체를 생성할 때, 람다식 클래스의 파생 클래스인 Expression<TDelegate>를 사용하였습니다.

 

람다식을 이용하면 더 간편하게 식 트리를 만들 수 있습니다만, 이 경우에는 동적으로 식 트리를 만들기 어려워집니다.

Expression 형식은 불변이기 때문에 인스턴스가 한번 만들어진 후에는 변경할 수가 없기 때문입니다.

 

아래의 예제는 람다식을 이용해서 식 트리를 만드는 예제인데, 위의 예제와 동일한 식 트리를 만들어내며, 실행 결과도 동일합니다.

using System;
using System.Linq.Expressions;

namespace ExpressionTreeViaLambda
{
    class MainApp
    {
        static void Main(string[] args)
        {
            Expression<Func<int, int, int>> expression =
                (a, b) => 1 * 2 + (a - b);

            Func<int, int, int> func = expression.Compile();

            // x = 7, y = 8
            Console.WriteLine($"1 * 2 + ({7} - {8}) = {func(7, 8)}");
        }
    }
}

실행 결과

 

해당 예제에서 위의 설명을 적용하자면, expression 객체가 불변이기 때문에 변경할 수가 없습니다.


식 트리를 동적으로 만들어야 할 이유

식 트리가 필요한 이유는 결국 데이터로써 보관다가 런타임에 동적으로 만들어 사용하기 위해서 입니다.

그렇다면, 런타임에 동적으로 만들어진 식 트리가 필요한 이유는 '확정된 C# 식'의 의미를 단순히 실행하는 것 외에 다른 목적으로 활용해야 할 때가 있기 때문입니다.


런타임 정보에 기반한 쿼리 또는 로직 생성

가장 흔하고 중요한 이유입니다. 개발자가 코드를 작성할 때는 정확히 어떤 데이터가 필터링될지, 어떤 계산이 수행될지 알 수 없습니다. 이는 프로그램이 실행되고 사용자 입력, 데이터베이스 상태 등 런타임 정보가 확정된 후에야 결정됩니다.

 

예를 들어 웹사이트 검색창에서 사용자가 "가격이 100보다 비싸고, 이름에 '컴퓨터'가 포함되며, 재고가 10개 미만인 상품"을 검색했습니다. 사용자는 이 외에도 다양한 검색 조건을 조합하여 입력할 수 있습니다.

 

개발자는 컴파일 시점에 모든 가능한 검색 조건의 조합에 대한 LINQ 쿼리를 미리 C# 코드로 작성해두는 것이 불가능합니다.

조건 10개만 되어도 조합은 수천 가지 이상이 되기 때문입니다.

 

이때 프로그램은 런타임에 사용자가 입력한 검색 조건을 분석합니다. '가격', '>', '100', '이름', '포함', '컴퓨터', '재고', '<', '10'등의 키워드와 값을 파싱합니다.

그리고 이 분석 결과에 따라 해당 논리에 맞는 Expression<Func<Product, bool>> 타입의 식 트리를 Expression.GreaterThan, Expression.AndAlso, Expression.Call 드으이 메서드를 사용하여 동적으로 저립합니다.

 

이렇게 런타임에 완성된 식 트리를 데이터베이스 LINQ 공급자에게 넘기면, 공급자는 이 식 트리를 분석하여 사용자가 원하는 정확한 조건의 SQL 쿼리를 생성하고 데이터베이스에 보냅니다.

 

이렇게 사용자가 어떤 조건을 입력할지는 컴파일 시점에는 알 수 없으므로, 식 트리의 '구조' 자체를 런타임에 만들어야 하는 것입니다.

 

외부 시스템과의 동적 상호작용

데이터베이스 외에도, 어떤 API나 서비스는 자체적인 쿼리 언어나 데이터 필터링 규칙을 가질 수 있습니다.

C# 어플리케이션이 이러한 외부 시스템과 연동할 때, 사용자의 요청이나 내부 로직에 따라 외부 시스템이 이해할 수 있는 형식의 명력을 동적으로 생성해야 할 수 있습니다.

 

이 때 C#의 식 트리를 사용하여 원하는 데이터 조작 논리를 표현하고, 식 트리를 분석하여 외부 시스템의 명령 형식으로 변환하는 메커니즘을 구축할 수 있습니다.

 

이 변환 로직이 특정 상태에 따라 달라진다면 식 트리의 구조 자체도 런타임에 동적으로 결정될 필요가 있습니다.

 

실행 시점 정보에 기반한 코드 최적화 또는 생성

아주 드문 경우지만, 프로그램 실행 환경(특정 하드웨어, 라이선스 정보 등)이나 런타임 데이터를 기반으로 가장 효율적인 코드 로직을 선택하거나 새로 만들어서 실행해야 할 때 식 트리를 사용할 수 있습니다.

 


 

즉, 컴파일 시점에는 모든 식이 고정이 될테지만,

그 코드가 나타내는 논리 구조를 프로그램 실행 중에 데이터로 표현하고, 그 데이터 구조 자체를 런타임 정보에 따라 다르게 만들어서 활용하는 것이 식 트리의 목적입니다.


식 본문 멤버 (Expression-Bodied Member)

메서드를 비롯하여 프로퍼티, 인덱서, 생성자, 종료자는 모두 클래스의 멤버로서 본문이 중괄호 { }로 만들어져 있습니다.

이러한 멤버의 본문을 '식'만으로 구현할 수 있는데, 이렇게 식으로 구현된 멤버를 "식 본문 멤버"라고 합니다.

멤버 => 식;

 

[예제]

using System;
using System.Collections.Generic;

namespace ExpressionBodiedMember
{
    class FriendList
    {
        private List<string> list = new List<string>();

        public void Add(string name) => list.Add(name);  // list.Add() 메서드를 호출하는 식
        public void Remove(string name) => list.Remove(name);  // list.Remove() 메서드를 호출하는 식
        public void PrintAll()
        {
            foreach (var s in list)
                Console.WriteLine(s);
        }

        public FriendList() => Console.WriteLine("FriendList()");  // 생성자의 식 구현
        ~FriendList() => Console.WriteLine("~FriendList()");       // 소멸자의 식 구현 

        // public int Capacity => list.Capacity;  // 읽기 전용 속성의 식 구현 (get 키워드 생략)
        public int Capacity  // 속성의 식 구현
        {
            get => list.Capacity;
            set => list.Capacity = value;
        }

        // public string this[int index] => list[index];  // 읽기 전용 인덱서의 식 구현(get 키워드 생략)
        public string this[int index]  // 인덱서의 식 구현
        {
            get => list[index];
            set => list[index] = value;
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            FriendList obj = new FriendList();
            obj.Add("Eeny");
            obj.Add("Meeny");
            obj.Add("Miny");
            obj.Remove("Eeny");
            obj.PrintAll();

            Console.WriteLine($"{obj.Capacity}");
            obj.Capacity = 10;
            Console.WriteLine($"{obj.Capacity}");

            Console.WriteLine($"{obj[0]}");
            obj[0] = "Moe";
            obj.PrintAll();
        }
    }
}

실행 결과

 

※ 객체의 소멸 시점은 CLR의 GC가 관리하기에, 소멸자에 대한 출력이 되지 않은 결과입니다.

'C#' 카테고리의 다른 글

[C#] 리플렉션과 애트리뷰트  (0) 2025.05.13
[C#] LINQ  (0) 2025.05.12
[C#] 대리자와 이벤트  (0) 2025.05.09
[C#] 예외 처리  (0) 2025.05.06
[C#] 일반화 프로그래밍  (0) 2025.05.05