P564 C# 언어 닷넷 버전에 따른 문법 히스토리
C#언어의 로드맵
https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md
roslyn/docs/Language Feature Status.md at main · dotnet/roslyn
The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs. - dotnet/roslyn
github.com
07 C# 2.0
7.1 제네릭
C# 1.0에서 기본 형식(Eg. ArrayList등)으로 컬렉션 객체 사용 시 박싱/언박싱 문제가 있었다.
int n= 5;
ArrayList ar = new();
ar.Add(n);

ArrayList는 모든 타입의 컬렉션을 구성할 수 있기에 Add메서드 인자로 object 타입을 받는다.
int는 값 형식이고, object는 참조형식이기에 정수형 데이터 5를 추가 시 object는 참조형식에 따라 정수형 데이터 5는 박싱된다. 이에 따라 힙에 object 인스턴스를 할당하고 그 참조 개체가 ArrayList의 Add메서드에 전달된다.
그림 7.1 ArrayList에 박싱돼 저장되는 int 데이터
ArrayList가 다루는 데이터 타입이 int로 고정돼야 박싱/언박싱 문제를 해결할 수 있지만 타입이 고정되면 각 타입마다 ArrayList 코드를 구현해야 하는 단점이 있다. (eg. int를 받는 IntArrayList, long을 받는 LongArrayList)
C# 2.0에서는 ArrayList를 보완한 List<T> 컬렉션 타입 즉 제네릭이 도입되었다.
T는 타입을 대체할 수 있다.
int n = 5;
List<int> list = new();
list.Add(n);
위 코드는 object가 아닌 int를 받기에 박싱 과정을 거치지 않는다.
List<bool> boolList = new();
List<byte> byteList = new();
List<short> shortList = new();
List<double> doubleList = new();
어느 타입이든 List<T>로 대체해서 생성 가능하다.
예제7.1 박싱이 발생하는 Stack 구현
// 예제를 간단하게 하기 위해 최소한의 구현만 포함시킴.
OldStack stack = new(10);
stack.Push(1); //박싱 발생
stack.Push(2); //박싱 발생
stack.Push(3); //박싱발생
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
public class OldStack
{
object[] _objList;
int _pos;
public OldStack(int size)
{
_objList = new object[size];
}
public void Push(object newValue)
{
_objList[_pos] = newValue;
_pos++;
}
public object Pop()
{
_pos--;
return _objList[_pos];
}
}
성능 문제:
- 박싱 및 언박싱 비용: Push 메서드에서 int 같은 값 타입을 object로 박싱하고, Pop 메서드에서 다시 언박싱을 해야 합니다. 이 과정은 메모리 할당을 발생시키고, 값 타입을 참조 타입으로 변환하는 추가적인 비용이 발생한다.
- 메모리 할당: 박싱된 값 타입은 힙에 할당되기 때문에, 스택을 사용할 때보다 힙 할당과 가비지 컬렉션이 추가적으로 발생할 수 있다.
예제 7.1의 성능을 좀 더 개선한 코드
// 예제를 간단하게 하기 위해 최소한의 구현만 포함시킴.
IntStack stack = new(10);
stack.Push(1);
stack.Push(2);
stack.Push(3);
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
public class DoubleStack
{
double[] _list;
int _pos;
public DoubleStack(int size)
{
_list = new double[size];
}
public void Push(double newValue)
{
_list[_pos] = newValue;
_pos++;
}
public double Pop()
{
_pos--;
return _list[_pos];
}
}
public class IntStack
{
int[] _list;
int _pos;
public IntStack(int size)
{
_list = new int[size];
}
public void Push(int newValue)
{
_list[_pos] = newValue;
_pos++;
}
public int Pop()
{
_pos--;
return _list[_pos];
}
}
위의 코드는 코드 중복이란 문제가 있다.
예제 7.1 기반으로 제네릭 코드를 구현하자.
클래스 명 다음에 <T>를 넣고, 내부 코드의 자료형을 T문자로 대체하기.
예제7.2 제네릭을 이용한 Stack 자료구조
// 예제를 간단하게 하기 위해 최소한의 구현만 포함시킴.
NewStack<int> stack = new NewStack<int>(10);
stack.Push(1);
stack.Push(2);
stack.Push(3);
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
public class NewStack<T>
{
T[] _list;
int _pos;
public NewStack(int size)
{
_list = new T[size];
}
public void Push(T newValue)
{
_list[_pos] = newValue;
_pos++;
}
public T Pop()
{
_pos--;
return _list[_pos];
}
}
상기 코드 빌드하고 실행하면 CLR은 JIT 컴파일 시에 클래스가 타입에 따라 정의될 때마다 T에 대응되는 타입을 대체해서 확장한다. NewStack<T>에 int, double을 전달한 코드 사용 시 아래와 같이 2개의 클래스에 해당하는 기계어 코드가 자동으로 만들어진다.

제네릭 클래스 : 제네릭이 클래스 수준에서 지정된 문법
class 클래스_명<형식매개변수[, ...]>
{
// 형식 매개변수를 멤버의 타입 위치에 지정
}
설명: 1개 이상의 형식 매개변수를 <>안에 지정 할 수 있다.
이때 사용되는 형식 매개변수의 이름은 임의로 지정할 수 있다.
public class GenericSample<Type>
{
Type item;
public GenericSample(Type value)
{
item = value;
}
}
public class TwoGeneric<K, V> // 2개 이상 지정
{
K key;
V value;
public void Set(K key, V value)
{
this.key = key;
this.value = value;
}
}
제네릭은 클래스 뿐만 아니라 메서드에서도 지정이 가능하다. 이를 제네릭 메서드라 한다.
형식 매개변수가 클래스 수준이 아닌 메서드 수준에서 부여되는 것이 특징이다.
class 클래스_명
{
[접근제한자] 반환타입 메서드명<형식매개변수[, ...]>([타입명])[매개변수명], ...)
{
//지역 변수
}
}
설명: 메서드 명 다음에 형식 매개변수를 지정할 수 있으며, 이때 지정된 형식 매개변수는 반환 타입,
메서드의 매개변수 타입, 메서드의 지역 변수 타입에 사용할 수 있다.
1) c#1.0에서의 WriteLog 메서드
public class Utility
{
public static void WriteLog(object item)
{
string output = string.Format("{0}: {1}", DateTime.Now, item);
Console.WriteLine(output);
}
}
2) 1)의 박싱/언박싱 개선한 코드
public static void WriteLog(bool item) { ...}
public static void WriteLog(byte item) { ...}
public static void WriteLog(short item) { ...}
public static void WriteLog(ushort item) { ...}
//...생략...
3) 제네릭 타입을 통한 1), 2) 개선
using static System.Net.Mime.MediaTypeNames;
Utility.WriteLog<bool>(true);
Utility.WriteLog<int>(0x05);
Utility.WriteLog<float>(3.14159f);
Utility.WriteLog<string>("test");
public class Utility
{
public static void WriteLog<T>(T item)
{
string output = string.Format("{0}: {1}", DateTime.Now, item);
Console.WriteLine(output);
}
}
#출력
2025 - 04 - 23 오후 8:57:57: True
2025-04-23 오후 8:57:57: 5
2025 - 04 - 23 오후 8:57:57: 3.14159
2025 - 04 - 23 오후 8:57:57: test
WriteLog에 지정된 형식을 다음과 같이 생략가능하다.
Utility.WriteLog(true);
Utility.WriteLog(0x05);
Utility.WriteLog(3.14159f);
Utility.WriteLog("test");
제네릭 타입 명시를 생략 할 수 있는 이유는 c# 컴파일러가 WriteLog 메서드의 인자로 T타입이 전달된다는 사실을 알고 자동으로 유추해서 대신 처리해주기 때문이다.
제네릭은 박싱/언박싱으로 발생하는 비효율적인 힙 메모리 사용 문제를 없앰과 동시에 데이터 타입에 따른 코드 중복 문제도 해결해준다.
7.1.1 형식 매개변수에 대한 제약 조건
제네릭을 사용하다 보면 형식 매개변수로 받아들이는 타입이 특정 조건을 만족해야 하는 경우가 있다.
public class Utility
{
public static int Max(int item1, int item2)
{
if (item1.CompareTo(item2) >= 0) return item1;
return item2;
}
}
위 코드를 double타입에도 사용할 수 있게 작성해보자.
public class Utility
{
public static T Max<T>(T item1, T item2)
{
if (item1.CompareTo(item2) >= 0) // 컴파일 에러 발생 ( CS7036)
{
return item1;
}
return item2;
}
}
컴파일 에러가 발생하는데 T로 대체될 타입이 모두 CompareTo 메서드를 지원하는 것이 아니기에 미리 컴파일 단계에서 에러를 발생시켜 잘못된 사용을 막는다.
이 경우는 'T'에 입력될 수 있는 타입의 조건을 where 예약어를 사용해 제한 할 수 있다.
예제7.3 IComparable 인터페이스를 상속받은 타입만 T에 대입
Console.WriteLine(Utility.Max(5,6));
Console.WriteLine(Utility.Max("Abc", "def"));
public class Utility
{
public static T Max<T>(T item1, T item2) where T : IComparable
{
if (item1.CompareTo(item2) >= 0) // 컴파일 에러 발생 ( CS7036)
{
return item1;
}
return item2;
}
}
#출력
6
def
where 예약어 다음에 형식 매개변수를 지정하고 콜론(:)을 구분자로 써서 제약조건을 걸 수 있다.
상기 코드에서 컴파일러는 T 타입으로 지정된 item1과 item2는 IComaparable 인터페이스를 상속받은 타입의 인스턴스라고 가정하게 되고 코드에서 IComparable.CompareTo 메서드를 호출하는 것을 허용한다.
where 형식매개변수 : 제약조건[, ....]
설명: 제네릭 구문이 사용된 메서드와 클래스에 대해 모두 where 예약어를 사용해 형식 매개변수가 따라야 할 제약
조건을 1개 이상 지정할 수 있고, 형식 매개변수의 수만큼 where 조건을 지정할 수 있다.
예1) 제네릭 클래스에 사용된 형식 매개변수에 제약 조건을 명시한 경우
public class MyClass<T> where T : ICollection
{
}
예2) 형식 매개변수에 2개 이상의 제약 조건을 명시한 경우
public class MyType<T> where T : ICollection, IConvertible
{
}
예3) 형식 매개변수 2개에 대해 각각 제약 조건을 명시한 경우
public class Dict<K, V> where K: ICollection
where V: IComparable
{
}
제약 조건으로 명시되는 타입에는 인터페이스나 클래스가 올 수 있지만, 표 7.1 처럼 특별한 제약조건도 가능하다.
** C# 7.3 부터 신규 제약 3가지가 추가됨(15.1절 신규 제네릭 제약 조건 - Delegate, Enum, unmanaged 참고)
표 7.1 제네릭 형식 매개변수에 대한 특별한 제약 조건
| 제약 조건 | 설명 |
| where T: struct | T 형식 매개변수는 반드시 값 형식만 가능 |
| where T: class | T 형식 매개변수는 반드시 참조 형식만 가능 |
| where T: new() | T 형식 매개변수의 타입에는 반드시 매개변수 없는 공용 생성자가 포함돼 있어야 한다. 즉 기본 생성자가 정의돼 있어야 한다. |
| where T: U | T 형식 매개변수는 반드시 U 형식 인수에 해당하는 타입이거나 그것으로부터 상속받은 클래스만 가능하다. |
public struct MyStruct
{
public int Value;
public int Value2;
public MyStruct(int value, int value2)
{
Value = value;
Value2 = value2;
}
public override string ToString()
{
return $"MyStruct {{ Value = {Value}, Value2 = {Value2} }}";
}
}
public class TEST
{
public void test<T>(T id) where T : struct
{
if(id is MyStruct)
{
Console.WriteLine(id);
return;
}
var t = id;
Console.WriteLine(t);
}
}
class Program
{
static void Main()
{
TEST t = new();
// 기본 값 타입 (double, int)
t.test(3.5); // 3.5 출력
t.test(2); // 2 출력
// 사용자 정의 구조체 타입
MyStruct structInstance = new MyStruct(10, 21);
t.test(structInstance); // MyStruct { Value = 10, Value2 = 21 } 출력
}
}
- **where T : struct는 값 타입(기본 타입 및 사용자 정의 struct)만을 허용하는 제약 조건이다. where T : int 같은 기본 타입은 안됨.
- 따라서, T는 int, double, decimal, bool 같은 기본 값 타입뿐만 아니라 사용자 정의 구조체 타입도 될 수 있다.
- 값 타입인 경우 모두 박싱 없이 처리되므로 성능 상 유리하다.
struct로 제약을 거는 경우 해당 형식 매개변수는 반드시 값 형식만 받을 수 있다. 예로 System.Runtime.InteropServices 네임스페이스에 정의된 Marshal 타입은 값 형식의 바이트 크기를 반환하는 SizeOf 메서드를 제공한다.
이를 이용한 제네릭 메서드를 만들면 모든 값 형식의 크기를 구할 수 있다.
using System.Runtime.InteropServices;
public static int GetSizeOf<T>(T item)
{
return Marshal.SizeOf(item);
}
다만, 참조형 변수를 무심코 GetSizeOf 메서드에 전달할 수 있기에 오류가 발생할 수 있다.
using System.Runtime.InteropServices;
class Program
{
static void Main(string[] args)
{
Console.WriteLine(GetSizeOf(0.5f)); // float 타입으로 4
Console.WriteLine(GetSizeOf(4m)); // decimal 타입으로 16
Console.WriteLine(GetSizeOf("My")); // 컴파일은 잘되나 런타임에러 발생
}
public static int GetSizeOf<T>(T item)
{
return Marshal.SizeOf(item);
}
}
#출력
4
16
Unhandled exception. System.ArgumentException: Type 'System.String' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.
at System.Runtime.InteropServices.Marshal.SizeOfHelper(Type t, Boolean throwIfNotMarshalable)
at System.Runtime.InteropServices.Marshal.SizeOf[T] (T structure)
at Program.GetSizeOf[T] (T item) in C:\Users\asd57\source\repos\ConsoleApp5\ConsoleApp5\Program.cs:line 13
at Program.Main(String[] args) in C: \Users\asd57\source\repos\ConsoleApp5\ConsoleApp5\Program.cs:line 9
** C# 9.0 부터 default 제약 조건이 추가됐다. ( 17.15절 '제약 조건이 없는 형식 매개변수 주석 (Unconstrained type parameter annotation)')
이러한 런타임에러 방지를 위해 컴파일 시점에 에러를 다음과 같이 짚어주면 개발자의 실수를 미연에 방지한다.
using System.Runtime.InteropServices;
class Program
{
static void Main(string[] args)
{
Console.WriteLine(GetSizeOf(0.5f)); // float 타입으로 4
Console.WriteLine(GetSizeOf(4m)); // decimal 타입으로 16
Console.WriteLine(GetSizeOf("My")); // 컴파일 에러 발생 (where 예약어에 struct 값형식만 허용하기에)
}
public static int GetSizeOf<T>(T item) where T: struct
{
return Marshal.SizeOf(item);
}
}
where T: class 제약조건은 참조 형식을 대상으로 한다.
using System.Runtime.InteropServices;
class Program
{
static void Main(string[] args)
{
int a = 5;
string b = "My";
CheckNull(a); // 컴파일 : 정상
CheckNull(b); // 컴파일: 정상
}
public static void CheckNull<T>(T item)
{
if (item == null)
{
throw new ArgumentNullException();
}
Console.WriteLine(item);
}
}
#출력
5
My
모든 값 형식은 null 상태를 갖지 않으므로 이 메서드의 사용은 항상 참조 형식에 적용하는 것이 올바르다.
using System.Runtime.InteropServices;
class Program
{
static void Main(string[] args)
{
int a = 5;
string b = "My";
CheckNull(a); // 컴파일에러
CheckNull(b); // 컴파일: 정상
}
public static void CheckNull<T>(T item) where T:class
{
if (item == null)
{
throw new ArgumentNullException();
}
Console.WriteLine(item);
}
}
where 제약을 사용하면 메서드나 클래스에서 값 타입과 참조 타입을 구분할 수 있어, 코드에서 타입의 특성에 맞는 처리가 가능하다.
where T: new() 제약 조건은 T타입의 객체를 제네릭 메서드/클래스 내부에서 new 연산자를 통해 생성할 때 사용한다.
public static T AllocateIfNull<T>(T item) where T : class
{
if(item == null)
{
item = new T();
}
return item;
}
c# 컴파일러는 형식 매개변수 T로 대체되는 모든 타입이 기본 생성자를 갖고 있다고 장담할 수 없기에 컴파일 시에 T 변수 형식에 new() 제약조건이 없으므로 이 변수 형식의 인스턴스를 만들 수 없다라는 오류가 발생할 수 있다. 이를 대비하고자 new() 제약 조건을 통해 기본생성자를 갖는 인자만 적용될 수 있게 다음과 같이 코드로 구성한다.
public static T AllocateIfNull<T>(T item) where T : class, new()
{
if(item == null)
{
item = new T();
}
return item;
}
형식 매개변수 2개를 사용해 제약조건 설정하는 법
BaseClass dInst = new DerivedClass();
public class BaseClass { }
public class DerivedClass : BaseClass { }
다형성을 이용한 인스턴스 할당 예제이다. 상기 new 코드를 제네릭으로 처리해보자.
BaseClass dInst = Utility.Allocate<BaseClass, DerivedClass>();
public class Utility
{
public static T Allocate<T, V>() where V : T, new()
{
return new V();
}
}
public class BaseClass { }
public class DerivedClass : BaseClass { }
Allocate 메서드는 DerivedClass를 new로 할당해 BaseClass로 형변환해서 반환하는 역할을 한다.
1. T와 V의 의미:
- T: 메서드의 반환 타입으로 사용된다. T는 메서드가 반환하는 객체의 타입을 정의한다.
- V: 메서드의 입력 타입(또는 실제 인스턴스를 생성하는 타입)으로 사용된다. V는 메서드가 new 키워드로 생성하려는 실제 타입을 나타낸다.
2. where V : T, new() 제약 조건:
- where V : T: V는 T의 자식 클래스이어야 한다는 제약이다. 즉, V는 T를 상속하거나 T 자체여야 한다. 이 제약은 V가 T 타입의 하위 클래스(또는 T와 동일한 타입)임을 보장한다.
- new(): V는 매개변수 없는 생성자가 있어야 한다는 제약이다. 즉, V는 기본 생성자를 가진 클래스여야만 한다.
왜 T, V의 순서가 이래야 하는가?
T와 V는 제네릭 타입 매개변수로서 두 개의 다른 역할을 한다:
- **T**는 반환 타입이다. 즉, 메서드가 반환하는 객체의 타입을 결정한다.
- **V**는 인스턴스를 생성할 타입으로, 실제로 new V()로 인스턴스를 생성하려고 한다.
이 순서가 중요한 이유는, V는 T의 자식 타입이거나 T와 동일해야 하기 때문이다. V가 T의 자식 타입이므로 V는 T를 상속할 수 있고, 그 반대는 불가능하다. 즉, T는 상위 클래스 또는 인터페이스로 사용되며, V는 실제 객체 타입으로 사용된다.
7.1.2 BCL에 적용된 제네릭
제네릭이 등장하면서 MS에서는 6.4 컬렉션 박싱/언박싱 문제를 개선하여, 닷넷 프레임워크 2.0에서 제네릭 타입을 새롭게 만들어 System.Collections.Generic 네임스페이스에 추가한다.
표 7.2 기존 컬렉션에 대한 제네릭 버전 A
| 닷넷 1.0 컬렉션 | 대응되는 제네릭 버전의 컬렉션 |
| ArrayList | List<T> |
| Hashtable | Dictionary<TKey, TValue> |
| SortedList | SortedDictionary<TKey, TValue> |
| Stack | Stack<T> |
| Queue | Queue<T> |
사용법은 기존 컬렉션과 동일하며 박싱/언박싱 문제가 발생하지 않아 힙 메모리의 부담이 적어 가비지 수집의 횟수가 줄어든다.
참조 형식을 사용했을 때는 박싱/언박싱이 발생하지 않기에 이러한 효과는 값 형식에서만 나타난다.
박싱(Boxing)과 언박싱(Unboxing):
- 박싱(Boxing): 값 타입을 object와 같은 참조 타입으로 변환하는 과정이다. 예를 들어, int와 같은 값을 object에 할당하면 박싱이 발생한다.
- 언박싱(Unboxing): 박싱된 참조 타입을 다시 값 타입으로 변환하는 과정이다. 예를 들어, object 타입에 저장된 int 값을 다시 int로 변환하면 언박싱이 발생한다.
참조 형식에서 박싱/언박싱이 발생하지 않는 이유:
- 참조 타입은 이미 참조로 저장되기 때문에 값 타입을 참조 타입으로 변환할 필요가 없다. 예를 들어, class나 string과 같은 참조 타입은 값이 아닌 참조를 저장하므로 박싱/언박싱이 필요 없다.
int a = 10;
object obj = a; // 박싱 발생, 값 타입을 참조 타입으로 변환
int b = (int)obj; // 언박싱 발생, 참조 타입을 값 타입으로 변환
string str = "Hello"; // 참조 타입 사용, 박싱이나 언박싱이 필요 없음
object objStr = str; // 참조 타입을 다른 참조 타입으로 할당
참조 형식(class, string, object 등)에서는 박싱/언박싱이 발생하지 않는다. 박싱/언박싱은 값 타입에서 발생하며, 참조 타입은 이미 참조를 통해 데이터를 다루기 때문에 박싱/언박싱의 과정이 필요 없다.
원칙상 제네릭 지원하지 않는 기존 컬렉션을 사용하지 말고, 제네릭을 사용하자.
표 7.3 기존 인터페이스에 대한 제네릭 버전 B
| 기존 인터페이스 | 대응되는 제네릭 버전의 인터페이스 |
| IComparable | IComparable<T> |
| IComparer | IComparer<T> |
| IEnumerable | IEnumerable<T> |
| IEnumerator | IEnumeratror<T> |
| ICollection | ICollection<T> |
7.2 ?? 연산자(null 병합 연산자)
?? 연산자는 null 값을 가진 참조형 변수를 손쉽게 처리 가능한 연산자다.
//null 연산 활용 전
string txt = null;
if(txt == null)
{
Console.WriteLine("null");
}
else
{
Console.WriteLine(txt);
}
//null 연산 활용
Console.WriteLine(txt ?? "null");
피연산자1 ?? 피연산자2
설명: 참조형식의 피연산자1이 null이 아니라면 그 값을 그대로 반환하고, null이라면 피연산자2의 값 반환
7.3 default 예약어
C# 7.1부터 타입 추론 기능을 적용(13.2절 참고)
변수를 초기화하지 않은 경우 값 형식은 0, 참조 형식은 null로 초기화되지만 제네릭의 경우 매개변수로 전달된 경우 코드에서 미리 타입을 알수 없기에 그에 대응되는 초깃값을 결정할 수 없다. 컴파일러가 자동으로 T형식에 따라 타입을 결정하는 default 예약어를 사용하면 해결이 가능하다.
예로 일반적으로 배열 범위를 벗어나는 인덱스가 지정되면 System.IndexOutOfRangeException 예외가 발생하는데 이 예외가 발생하지 않도록 배열을 감싼 자료구조를 만들면 다음과 같다.
예제 7.4 인덱스를 벗어나도 예외가 발생하지 않는 배열
// 0~9 범위의 인덱스를 사용하는 배열 생성
ArrayNoException<int> list = new ArrayNoException<int>(10);
list[15] = 5; //일반적인 배열이었다면 예외 발생
Console.WriteLine(list[15]);
class ArrayNoException<T>
{
int size;
T[] items;
public ArrayNoException(int size)
{
this.size = size;
this.items = new T[size];
}
public T this[int index]
{
get
{
if(index >= this.size)
{
return default(T);
}
return items[index];
}
set
{
if(index >= this.size)
{
return;
}
items[index] = value;
}
}
}
#출력
0 // 배열 범위 벗어나는 인덱스일 경우
인덱스 범위 벗어날 시 해당 타입의 기본값을 반환하게 코드를 구성했는데, array가 값타입이기에 기본값인 0을 반환한다.
default 예약어는 타입을 인자로 받기에 다음과 같이 임의로 타입을 지정하는 방식으로 사용 가능하다.
using System.Numerics;
int intValue = default(int);
BigInteger bigIntValue = default(BigInteger);
Console.WriteLine(intValue); // 0
Console.WriteLine(bigIntValue); // 0
string txt = default(string);
Console.WriteLine(txt ?? "(null)"); // (null)
int a;
Console.WriteLine(a); // 컴파일에러 (지역변수 초기화 안되었으니)
default 키워드는 주로 값 타입과 참조 타입의 기본값을 설정할 때 사용됩니다. 이를 통해 코드에서 명시적으로 기본값을 할당하는 경우 유용합니다.
- 값 타입 (예: int, BigInteger): default(int) 또는 default(BigInteger)를 사용하면, 각 타입의 기본값인 0을 설정합니다. int와 같은 값 타입은 기본적으로 0으로 초기화됩니다.
- 참조 타입 (예: string): default(string)은 참조 타입의 기본값인 null을 설정합니다.
이러한 사용은 주로 초기화되지 않은 상태에서 변수의 기본값을 설정하려는 경우, 또는 제네릭 코드에서 타입이 무엇이든 상관없이 기본값을 설정하려는 경우에 유용합니다.
7.4 yield return/break
yield return과 yield break 예약어를 이용하면 기존의 IEnumerable, IEnumerator 인터페이스를 이용해 구현한 열거 기능을 쉽게 구현 가능하다.
int[] intList = new int[] {1,2,3 ...} 과 같은 배열과 List<T>에 담긴 요소가 foreach로 열거될 수 있는 이유는 해당 타입에서 IEnumerable, IEnumerator 인터페이스를 구현하고 있기 때문이다.
예제 7.5 IEnumerable 인터페이스를 이용한 자연수 표현
using System.Collections;
//해당 예제는 int 범위 자연수 표현.
// 큰 자연수 필요 시 ulong 또는 BigInteger 사용하기.
NaturalNumber num = new();
foreach(int n in num) // 출력 결과 : 1부터 자연수 무한 출력.
{
Console.WriteLine(n);
}
class NaturalNumber : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
return new NaturalNumberEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return new NaturalNumberEnumerator();
}
}
internal class NaturalNumberEnumerator : IEnumerator<int>
{
int current;
public int Current => current;
object IEnumerator.Current => current;
public void Dispose()
{
}
public bool MoveNext()
{
current++;
return true;
}
public void Reset()
{
current = 0;
}
}
IEnumerable과 IEnumerator 조합으로 현재 존재하지 않는 요소를 필요할 때마다 열거할 수 있다.
그러나 이를 구현하는 코드가 번거롭기에 이를 보완하고자 yield return/break 예약어가 추가 됐다.
예제 7.6 yield return을 이용한 자연수 표현
foreach(int n in YieldNaturalNumber.Next())
{
Console.WriteLine(n);
}
class YieldNaturalNumber
{
public static IEnumerable<int> Next()
{
int start = 0;
while (true)
{
start++;
yield return start;
}
}
}
Next 메서드 호출 시 yield return에서 값이 반환되어 메서드 실행이 중지된다. 그러나 내부적으로 yield return이 실행된 코드 위치를 기억하여 다음에 다시 메서드 호출 시 처음부터 코드가 시작되지 않고 마지막 yield return문이 호출됐던 코드의 다음줄 부터 실행을 재개한다.
참고로 실제 내부 구현은 이러한 설명과는 좀 다르다. c# 컴파일러는 yield 문이 사용된 메서드를 컴파일 시에 예제 7.5와 유사한 코드로 치환하여 처리한다.
결과적으로, yield는 IEnumerable/IEnumerator로 구현한 코드에 대한 간편 표기법(syntatic sugar)이다.
yield break 사용시 열거를 끝낼 수 있다.
foreach(int n in YieldNaturalNumber.Next(100))
{
Console.WriteLine(n);
}
class YieldNaturalNumber
{
public static IEnumerable<int> Next(int max)
{
int start = 0;
if(max < start)
{
yield break; // max만큼 루프 수행 후 열거 중지
}
while (true)
{
start++;
yield return start;
}
}
}
#출력
1~100
yield문법은 필수 사용은아니나 간결하게 작성하는 방식이다.
7.5 부분(partial) 클래스
* C# 3.0에서는 클래스뿐만 아니라 메서드에 대해서도 partial 기능을 추가함(8.6절 부분 메서드 참고)
partial 예약어를 클래스에 적용 시 클래스의 소스코드를 2개 이상 나눌 수 있다. (p588)
~ 책 읽기. 기록할 필요 없을정도로 문법 설명 간단.
8.6 부분메서드
메서드를 partial로 만들 수 있는데 선언만하고 정의는 다른 partial 메서드에서 정의가 가능하다.
특이점은 partial 클래스 안에서 가능, 반환값이 없어야함(void) , 매개변수를 ref 가능하나 out은 불가.
abc a = new();
a.test();
partial class abc
{
public partial void test();
}
partial class abc
{
public partial void test()
{
Console.WriteLine("Test");
}
}
#출력 Test
특이점. 부분메서드에서는 private 접근자만 허용된다 했는데, public도 되는데?
Program.cs (실행 파일)
abc a = new();
a.test();
partial class abc
{
public partial void test()
{
Console.WriteLine("Test");
publicField = 12;
privateField = 13;
}
}
Class1.cs
partial class abc
{
public partial void test();
public int publicField = 5;
private int privateField = 10;
}
컴파일 에러없이 출력 " Test " 정상 출력됨.
해당 문법은 사실 사용을 잘 안함.
8.7 확장메서드
상속은 다음 조건에서는 좋은 선택이 아니다.
- sealed로 봉인된 클래스는 확장 불가
-> eg. BCL의 System.String 타입은 sealed 클래스로 상속 불가.
- 클래스로 상속받아 확장하면 기존 소스코드를 새롭게 상속받은 클래스명으로 바꿔야한다.
( BCL중 값 타입(구조체, 기본타입등)이나 seald클래스(eg. System.String )은 상속 불가이다. 참조타입(eg. Exception등)은 상속 가능)
확장메서드는 위와 같은 조건들로 상속 불가(확장 불가)일 경우 사용한다. 클래스 내부 구조를 전형 바꾸지 않고 새 인스턴스를 정의한 것처럼 추가할 수 있다.
예제 8.4 확장 메서드를 이용해 기존의 string타입에 공용메서드 추가.
string txt = "Hello World";
//string 타입의 instance 메서드를 호출하듯 확장 메서드 사용
Console.WriteLine($"Count: {txt.GetWordCount()}");
//상기 확장메서드 코드는 아래 코드로 컴파일러가 변환해서 출력함.
Console.WriteLine($"Count: {ExtensionMethodSample.GetWordCount(txt)}");
static class ExtensionMethodSample
{
//확장 메서드는 static 표기
//확장 메서드 매개변수는 this 예약어와 함께 명시
public static int GetWordCount(this string text)
{
return text.Split(' ').Length;
}
}
# 출력
Count: 2
Count: 2
static 메서드 호출을 인스턴스 메서드를 호출하듯 문법 지원을 하는 것이 확장메서드이다.
클래스 상속에선 가능했던 부모 클래스의 protected 멤버호출이나 메서드 재정의(override)가 불가하다.
네임스페이스 하에 ExtensionMethodSample 클래스를 정의하면, 확장메서드를 사용하는 소스코드에서는 반드시 using 네임스페이스를 사용해야한다.
using test;
string txt = "Hello World";
//string 타입의 instance 메서드를 호출하듯 확장 메서드 사용
Console.WriteLine($"Count: {txt.GetWordCount()}");
//상기 코드는 아래 코드로 컴파일러가 변환해서 출력함.
Console.WriteLine($"Count: {ExtensionMethodSample.GetWordCount(txt)}");
namespace test
{
static class ExtensionMethodSample
{
//확장 메서드는 static 표기
//확장 메서드 매개변수는 this 예약어와 함께 명시
public static int GetWordCount(this string text)
{
return text.Split(' ').Length;
}
}
}
intellisense에서 보이는 메서드들은 모두 확장메서드이다.
8.8 람다식
1. 코드로서의 람다식
-익명 메서드의 간편표기 용도로 사용
2. 데이터로서의 람다식
- 람다식 자체가 데이터가 되어 구문분석 대상이됨. 해당 람다식은 별도 컴파일이 가능하며, 이를 통해 메서드로 실행 가능
8.8.1 코드로서의 람다식
Thread thread = new Thread(
delegate (object obj)
{
Console.WriteLine("ThreadFunc is anonymous method called!");
});
thread.Start();
c# 컴파일러는 Thread 타입의 생성자가 'void(object obj)' 형식의 델리게이트 인자를 하나 받는 다는 사실을 알고 있다. 따라서 익명메서드의 구문을 더 단순하게 하면 다음과 같다.
Thread thread = new Thread(
(obj) =>
{
Console.WriteLine("ThreadFunc is anonymous method called!");
});
thread.Start();
delegate 생략과 인자 타입을 명시할 필요가 없다. 람다구문임을 컴파일러에게 알려주고자 => 를 표시했다.
예제 8.5 익명메서드를 람다 구문의 메서드로 대체
MyDelegate myFunc = (a, b) =>
{
if (b == 0) return null;
return a / b;
};
Console.WriteLine(myFunc(10, 5));
delegate int? MyDelegate(int a, int b);
#출력
2
C# 컴파일러는 내부적으로 익명메서드와 동일하게 확장해서 컴파일이 가능하다.
람다구문의 메서드는 내부코드가 식(Expression)으로 평가될 수 있는 경우에 한해 약식 표현을 하나 더 제공한다.
이러한 경우 반환 여부 상관없이 return문을 생략할 수 있고, 식이라는 특성으로 중괄호를 허용되지 않는 람다식으로 코드 작성이 가능하다.
MyAdd myFunc = (a, b) => a + b;
Console.WriteLine(myFunc(50, 100));
delegate int MyAdd(int a, int b);
#출력
150
수학 함수 표현 f(x, y, z) = (x + y)/z;
c# 람다식 표현 (x, y, z) => (x+y)/z;
8.8.1.1 람다 전용을 위한 전용 델리게이트
람다 메서드는 델리게이트에 대응된다.
예8.5, 8.6에서 사용한 델리게이트
delegate int? MyDivide(int a, int b);
delegate int MyAdd(int a, int b);
람다메서드는 일회성으로 간단한 코드를 표현할 때 사용하나 이러한 이유로 델리게이트를 일일이 정의하는 것은 불편함이 있다.
MS에선 이러한 불편함을 덜어주고자 자주 사용되는 델리게이트 형식을 제네릭의 도움으로 일반화해서 BCL에 Action과 Func으로 포함시켰다.
public delegate void Action<T>(T obj);
==> 반환값이 없는 델리게이트로서 T형식 매개변수는 입력될 인자 1개의 타입을 지정
public delegate TResult Func<TResult>();
==> 반환값이 있는 델리게이트로서 TResult 형식 매개변수는 반환될 타입을 지정
Action과 Func의 차이는 반환값의 유무에 있다. 이를 이용하면 별도 delegate를 정의할 필요가 없다.
예 8.7 Func, Action 델리게이트
using static System.Net.Mime.MediaTypeNames;
Action<string> logout =
(txt) =>
{
Console.WriteLine(DateTime.Now + ": " + txt);
};
logout("test");
Func<double> pi = () => 3.141592;
Console.WriteLine(pi());
#출력
2025 - 04 - 26 오후 6:40:53: test
3.141592
MS는 인자를 16개까지 받을 수 있는 Action, Func을 미리 정의해두었다.
public delegate void Action<T>(T arg);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3);
//.... T1 ~ T16 까지 Action 델리게이트 정의
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T arg);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
//.... T1 ~ T16 까지 Func 델리게이트 정의
2개 인자 입력에 따른 반환값 1개 출력
//2개는 입력에 대응되고 마지막 int 타입 1개는 반환값에 대응
Func<int, int, int> myFunc = (a,b)=> a + b;
Console.WriteLine(myFunc(3, 5));
출력
8
8.8.1.2 컬렉션과 람다메서드
확장메서드, 람다 메서드, Action, Func은 기존 컬렉션의 기능을 더욱 풍부하게 개선했다.
기존 List<T>는 foreach를 통해 collection 요소를 순차적으로 뽑아냈다.
그러나 이젠 Array 또는 List<T>의 컬렉션에 추가돤 ForEach 메서드를 이용해
# List<t>에 정의된 ForEach
public void ForEach(Action<T> action);
# Array에 정의된 ForEach
public static void ForEach<T>(T []array, Action<T> action);
다음과 같이 처리할 수 있다.
List<int> lst = new List<int> { 1, 2, 3, 5 };
lst.ForEach((elem) =>
{
Console.WriteLine($"{elem} * 2 = {elem * 2}");
});
//Array.ForEach 표기법
Array.ForEach(lst.ToArray(),
(elem) =>
{
Console.WriteLine($"{elem} * 2 = {elem * 2}");
});
//람다식이 아닌 익명 메서드 표기법
lst.ForEach(delegate (int elem)
{
Console.WriteLine($"{elem} * 2 = {elem * 2}");
});
#출력
1 * 2 = 2
2 * 2 = 4
3 * 2 = 6
5 * 2 = 10
ForEach 메서드는 Action<T> 델리게이트를 인자로 받아 그림 8.2 처럼 컬렉션의 모든 요소를 열람하면서, 하나씩 Action<T>의 인자로 요소 값을 전달한다.
즉, 요소 수만큼 Action<T> 델리게이트가 수행된다.
그림 8.2 ForEach 동작방식

예쩨 8.8 짝수로 구성된 List<T>를 반환하는 코드
List<int> list = new List<int>{ 1, 2, 3, 5, 6, 8, 12, 15 };
List<int> evenList = new();
foreach(var item in list)
{
if(item %2 == 0)
{
evenList.Add(item);
}
}
위 코드를 다음과 같이 FindAll 메서드를 사용해 간단 표기 가능하다.
public List<T> FindAll(Predicate<T> match);
List<int> list = new List<int>{ 1, 2, 3, 5, 6, 8, 12, 15 };
List<int> evenList = list.FindAll((elem) => elem % 2 == 0);
evenList.ForEach((elem) => { Console.WriteLine(elem + ", "); });
FindAll 메서드는 별도로 'delegate bool Predicate<T>(T obj)'로 정의된 델리게이트 타입을 인자로 받는다. Predicate 델리게이트는 한 개의 인자를 받고 bool 타입을 반환하는데, Func<T, bool> 델리게이트로 생각해도 된다.
기존의 컬렉션 크기만을 단순하게 반환하는 Count 속성은 Enumerable 타입의 확장 메서드를 통해 특정 조건을 만족하는 요소의 개수를 반환할 수 있게 됐다.
public static int Count<TSource>(this IEnumerable<TSource> source);
public static int Count<TSource>(this IEnumerable<TSource> source.Func<TSource, bool> predicate);
숫자 3보다 큰 요소 갯수를 반환하는 코드
List<int> list = new List<int>{ 1, 2, 3, 5, 6, 8, 12, 15 };
int count = 0;
foreach(var item in list)
{
if(item > 3) count++;
}
상기 코드를 간편 표기로 다음과 같이 작성한다.
List<int> list = new List<int>{ 1, 2, 3, 5, 6, 8, 12, 15 };
int count = list.Count((elem) => elem > 3);
Console.WriteLine("3보다 큰 수는 " + count + "개 있음.");
#출력
3보다 큰 수는 5개 있음.
Enumerable 타입에 추가된 Where 확장 메서드 정의
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
Where 메서드는 Func<T, bool> 타입을 인자로 받는데 ,이는 FindAll 메서드가 받는 Predicate<T> 델리게이트와 유사하다. 실제로 FindAll 이 특정 조건을 만족하는 요소를 반환하는 것처럼 Where 메서드도 같은 동작을 수행한다.
FindAll 메서드 반환값은 List<T>이나, Where는 IEnumerable<T>로 열거형을 반환한다.
예제 8.9 Where 사용 예
List<int> list = new List<int> { 1, 2, 3, 5, 6, 8, 12, 15 };
IEnumerable<int> enumList = list.Where((elem) => elem % 2 == 0);
Array.ForEach(enumList.ToArray(), (elem) => { Console.WriteLine(elem); });
#출력
2 4 6 8 12
FindAll과 Where 메서드가 표면상 유사한 기능을 구현하는 것처럼 보이지만 Where 메서드의 반환값이 IEnumerable<T>를 주목하자.
엄밀히 보면 예제 8.9와 동일한 소스코드는 예제 8.8이 아닌 yield를 사용한 다음 코드와 가깝다.
using System.Linq;
List<int> list = new List<int> { 1, 2, 3, 5, 6, 8, 12, 15 };
IEnumerable<int> enumList = list.WhereFunc(); // 확장 메서드 호출
Array.ForEach(enumList.ToArray(), (elem) => { Console.WriteLine(elem); });
public static class ExtensionMethods
{
public static IEnumerable<int> WhereFunc(this IEnumerable<int> inst)
{
foreach (var item in inst)
{
if (item % 2 == 0)
{
yield return item;
}
}
}
}
FindAll의 경우 메서드 실행이 완료된 순간 람다 메서드가 컬렉션의 모든 요소를 대상으로 실행되어 조건을 만족하는 목록을 반환하는 반면, Where의 경우 메서드가 실행됐을 때는 어떤 코드도 실행되지 않은 상태이고 이후 열거자를 통해 요소를 순회헀을 때에야 비로소 람다 메서드가 하나씩 실행된다는 차이다.
이를 가리켜 '지연된 평가(lazy evaluation)'라 하고 IEnumerable<T>를 반환하는 모든 메서드가 이러한 방식으로 동작한다.
지연된 평가의 장점은 최초 메서드 호출로 인한 직접적인 성능 손실이 발생하지 않고 실제로 데이터가 필요한 순간에만 코드가 CPU에 의해 실행된다는 점이다.
소수 1만개 반환 하는 메서드 구현 시 List<T>를 반환하는 FindAll 방식으로 구하면 데이터가 500개만 필요해도 1만개의 소수를 구할 때까지 cpu가 실행돼야 하지만, IEnumerable<T> 처럼 지연 평가를 사용해 결과가 반환된 경우 500개까지의 데이터만 반환받고 끝낼 수 있다.
IEnumerable<T> 타입에 정의된 Select 확장 메서드는 컬렉션의 개별요소를 다른 타입으로 변환할 때 사용할 수 있으며, 형변환 뿐만 아니라 객체 반환도 가능하다. 또한 익명타입으로 구성해 반환할 수 있다.
사용법은 다음과 같다.
1. 형변환
List<int> list = new List<int> { 1, 2, 3, 5, 6, 8, 12, 15 };
IEnumerable<double> doubleList = list.Select((elem) => (double)elem);
/*
* typeof(elem) -> elem.GetType()으로 변경: typeof는 타입 정보를 가져오는 것이고, elem.GetType()은 객체의 실제 타입을 반환합니다.
* typeof(elem)을 할시 컴파일 에러가 났음. typeof(float) 은 가능
*/
Array.ForEach(doubleList.ToArray(), (elem) => { Console.WriteLine($"{elem.GetType()}"); });
#출력
System.Double
System.Double
System.Double
System.Double
System.Double
System.Double
System.Double
System.Double
2. 객체반환
List<int> list = new List<int> { 1, 2, 3, 5, 6, 8, 12, 15 };
IEnumerable<Person> personList = list.Select((elem) => new Person { Age = elem, Name = Guid.NewGuid().ToString() });
Array.ForEach(personList.ToArray(), (elem) => { Console.WriteLine($"{elem.Name}, {elem.Age}"); });
class Person
{
public int Age;
public string Name;
}
#출력
0879b8bc - 5f5c - 4d4c - b1fc - 3fb21cbff57a, 1
f133035c-c374-40c5-b233-5d572672aabc, 2
6043bfe8-26b7-441e-938b-8f54c1d7b41d, 3
414f84eb-97f5-4840-8acc-c7559ce6391f, 5
122ca0f9-56ef-47f8-8d77-f3cda85a3535, 6
207b482c-38e6-46b1-b837-c57b12d7c036, 8
8ca9bf1e-fe78-49b5-bd27-1ca6730792ad, 12
60a6bc37-f24b-41cc-ae61-76de62da300f, 15
3. 익명타입
List<int> list = new List<int> { 1, 2, 3, 5, 6, 8, 12, 15 };
var itemList = list.Select(
(elem) => new { TypeNo = elem, CreatedDate = DateTime.Now.Ticks });
Array.ForEach(itemList.ToArray(), (elem) => { Console.WriteLine(elem.TypeNo); });
#출력
1
2
3
5
6
8
12
15
Select 또한 Where과 마찬가지로 IEnumerable<T> 타입을 반환하므로 지연평가에 해당한다.
FindAll의 지연평가 버전이 Where 메서드 인것 처럼, ConvertAll의 지연 평가 버전이 Select 메서드이다.
지연 평가 필요 시 상황에 맞게 골라 사용하자.
8.8.2 데이터로서의 람다 식
람다 메서드 구현은 중괄호를 가질 수 있는 문(statement)과 그렇지 않는 식(expression)으로 구현된다고 설명했고 후자의 경우 '람다 식'이라고부른다.
람다식의 장점은 CPU에 의해 실행되는 코드가 아닌 그 자체로 '식을 표현한 데이터'로도 사용 가능하다. 이 처럼 데이터 구조로 표현 된 것을 '식 트리(expression tree)'라고 한다.
식 트리로 담긴 람다 식은 익명 메서드의 대체물이 아니기에 델리게이트 타입으로 전달되는 것이 아니라 식에 대한 구문 분석을 할 수 있는 System.Linq.Expressions.Expression 타입의 인스턴스가 된다.
이 내용은 람다 식과 그에 해당하는 **식 트리(Expression Tree)**에 관한 설명입니다. 간단히 말해, 람다 식이 바로 실행되는 메서드가 아니라, 구문 분석을 위한 식 트리로 처리된다는 것입니다. 이를 좀 더 쉽게 설명하겠습니다.
1. 람다 식과 익명 메서드
- 람다 식은 일반적으로 **익명 메서드(anonymous method)**를 작성하는 간결한 방법입니다. 예를 들어, x => x + 1과 같은 간단한 람다 식은 익명 메서드의 축약형입니다.
- 익명 메서드는 실행할 수 있는 델리게이트 객체를 반환합니다. 예를 들어, Func<int, int, int>와 같은 델리게이트 타입을 반환합니다.
2. 식 트리(Expression Tree)
- 그러나 람다 식은 식 트리로 변환될 수 있습니다. 식 트리는 람다 식 자체를 분석하고 표현할 수 있는 트리 구조입니다. 즉, 식 트리는 람다 식의 구문을 표현하는 객체입니다.
- 이때, System.Linq.Expressions.Expression 타입은 람다 식을 구문 분석하여, 실행하지 않고도 식의 구성 요소를 식 트리로 변환할 수 있도록 도와줍니다.
3. 왜 식 트리가 중요한가?
식 트리를 사용하면 람다 식을 실행하는 대신 그 구문을 분석하고, 이를 다른 목적으로 사용할 수 있게 됩니다. 예를 들어, LINQ에서 식 트리를 사용하면 데이터베이스 쿼리로 변환하거나 식을 동적으로 평가하는 등의 작업을 할 수 있습니다. 따라서 식 트리는 람다 식의 실행을 넘어서서 구조를 분석하고 조작할 수 있도록 돕습니다.
4. 델리게이트와 식 트리의 차이
- 델리게이트는 메서드를 실행할 수 있는 참조로, 람다 식을 실행 가능하게 만듭니다. 예를 들어, Action, Func<T>, Predicate<T> 등이 델리게이트입니다.
- 식 트리는 람다 식의 구문 분석된 구조입니다. 이를 통해 실행 전에 식을 분석하고 변형할 수 있습니다.
예시
1. 델리게이트 사용:
Func<int, int> addOne = x => x + 1;
Console.WriteLine(addOne(5)); // 6
위 코드에서는 람다 식이 델리게이트로 변환되어 addOne에 저장됩니다. 이때 즉시 실행됩니다.
2. 식 트리 사용:
Expression<Func<int, int>> addOneExpression = x => x + 1;
Console.WriteLine(addOneExpression.ToString()); // x => (x + 1)
위 코드에서는 식 트리로 람다 식을 나타냅니다. 식 트리는 구문 분석된 형태로 람다 식을 나타내므로 실행되지 않으며, 트리의 구조를 분석하거나 조작할 수 있습니다.
결론
- 람다 식을 식 트리로 다루면, 그것을 실행하는 대신 구문 분석을 하고 그 구조를 조작하거나 다른 목적으로 사용할 수 있게 됩니다. 델리게이트는 단순히 람다 식을 실행할 수 있게 하는 객체라면, 식 트리는 람다 식을 구문 분석하여 동적으로 활용할 수 있는 구조를 제공합니다.
식 트리(Expression Tree)는 실무에서 매우 유용하게 사용될 수 있는 기능으로, 주로 동적 쿼리 생성, 리팩토링, 동적 평가와 같은 다양한 용도로 활용됩니다. 다음은 식 트리가 실무에서 활용될 수 있는 주요 사례입니다:
1. 동적 쿼리 생성
식 트리는 동적 쿼리 생성에서 가장 많이 사용됩니다. 예를 들어, LINQ 쿼리나 데이터베이스 쿼리에서 조건을 동적으로 생성할 때 유용합니다.
- 사용 예시:
- LINQ to SQL이나 Entity Framework에서 데이터베이스 쿼리를 동적으로 생성하는 경우, 조건이나 필드를 동적으로 바꾸어야 할 때 식 트리를 사용하면 런타임에 쿼리 조건을 동적으로 구성할 수 있습니다.
- 사용자가 입력한 검색 조건을 기반으로 동적으로 필터링된 쿼리를 생성하려면, 이 쿼리를 식 트리로 표현한 뒤 SQL로 변환하여 실행할 수 있습니다.
- 예시 코드
var parameter = Expression.Parameter(typeof(Person), "p");
var property = Expression.Property(parameter, "Age");
var constant = Expression.Constant(30);
var condition = Expression.GreaterThan(property, constant);
var lambda = Expression.Lambda<Func<Person, bool>>(condition, parameter);
var compiled = lambda.Compile();
var filteredPeople = people.Where(compiled).ToList();
- 위 코드에서 Age > 30을 조건으로 동적으로 식 트리를 생성하고 이를 실행합니다.
2. 쿼리 최적화 및 변환
식 트리는 쿼리 최적화나 쿼리 변환에도 사용됩니다. 예를 들어, 기존 쿼리에서 특정 조건을 제거하거나 변형하는 경우 식 트리를 활용할 수 있습니다.
- 사용 예시:
- Expression Visitor 패턴을 사용하여 기존 식 트리를 변형하거나 최적화합니다. 예를 들어, A + B가 있는 식을 2 * (A + B)로 변환하거나, A == B 조건을 A.Equals(B)로 변경하는 등의 작업을 할 수 있습니다.
- 필터 조건을 최적화하거나, 불필요한 조건을 제거하는 데 유용합니다.
- 예시 코드:
public class MyExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.Equal)
{
// A == B -> A.Equals(B)로 변환
return Expression.Call(node.Left, "Equals", null, node.Right);
}
return base.VisitBinary(node);
}
}
3. 동적 메서드 실행
식 트리는 동적 메서드 호출을 구현하는 데 유용합니다. 런타임에 메서드나 프로퍼티를 동적으로 호출해야 하는 경우, 식 트리를 사용하여 메서드 호출을 동적으로 생성하고 실행할 수 있습니다.
- 사용 예시:
- 디자인 패턴에서 동적 메서드 호출이 필요할 때 유용합니다. 예를 들어, Reflection을 사용하여 메서드를 동적으로 호출할 때 식 트리를 사용하여 더 최적화된 방법으로 메서드 호출을 처리할 수 있습니다.
- 예시 코드:
var parameter = Expression.Parameter(typeof(Person), "p");
var methodCall = Expression.Call(parameter, typeof(Person).GetMethod("Greet"));
var lambda = Expression.Lambda<Action<Person>>(methodCall, parameter);
var compiled = lambda.Compile();
compiled(new Person());
4. Expression<Func<T, TResult>>와 관련된 커스텀 필터링
식 트리는 동적 필터링 시스템을 구축하는 데 매우 유용합니다. 예를 들어, 사용자 인터페이스에서 선택된 필터를 기반으로 필터링 로직을 동적으로 생성하는 시스템에 사용할 수 있습니다.
- 사용 예시:
- 사용자가 입력한 값에 따라 동적 필터링을 수행해야 할 때 식 트리를 사용하여 필터 조건을 동적으로 구성하고 이를 기반으로 데이터를 필터링합니다.
- 예를 들어, 여러 개의 검색 조건을 동적으로 추가하거나 제거할 수 있습니다.
- 예시 코드:
var param = Expression.Parameter(typeof(Product), "p");
Expression filter = Expression.Constant(true); // 기본 필터는 항상 true (모든 항목)
if (filterCategory != null)
{
var categoryExpr = Expression.Equal(Expression.Property(param, "Category"), Expression.Constant(filterCategory));
filter = Expression.AndAlso(filter, categoryExpr);
}
var lambda = Expression.Lambda<Func<Product, bool>>(filter, param);
var compiled = lambda.Compile();
var filteredProducts = products.Where(compiled).ToList();
5. Expression을 사용한 동적 Caching
식 트리는 캐싱 시스템에서 동적 캐시 키 생성에 사용될 수 있습니다. 예를 들어, 복잡한 조건에 따라 캐시 키를 동적으로 생성할 수 있습니다.
- 사용 예시:
- 동적 캐시 키를 생성할 때, 여러 조건을 식 트리로 표현하여 캐시를 더 효율적으로 관리할 수 있습니다.
- 예시 코드:
var param1 = Expression.Parameter(typeof(int), "a");
var param2 = Expression.Parameter(typeof(int), "b");
var sum = Expression.Add(param1, param2);
var lambda = Expression.Lambda<Func<int, int, int>>(sum, param1, param2);
var compiled = lambda.Compile();
var result = compiled(5, 10); // 15
결론
식 트리는 동적 쿼리 생성, 리팩토링, 동적 메서드 실행, 동적 필터링, 동적 캐싱 등에서 매우 강력한 도구로 활용됩니다. 식 트리를 사용하면 구문 분석 및 변형을 통해 동적으로 실행할 수 있는 코드를 생성할 수 있어 매우 유용합니다. 특히 LINQ, Entity Framework, 동적 쿼리 생성, 필터링 시스템 등에서 구조적이고 효율적인 방법으로 많이 사용됩니다.
람다 식이 코드가 아니라 Expression 객체의 인스턴스 데이터의 역할을 한다.
예제 8.6 람다식을 Expression 객체로 다루면 다음과 같은 코드가 된다.
Expression<Func<int, int, int>> exp = (a, b) => a + b;
Expression<T> 타입의 형식 매개변수는 람다식이 표현하는 델리게이트 타입이 되고, exp 변수는 코드를 담지 않고 람다식을 데이터로서 담고 있다.
데이터로서 동작하기에, 해당 데이터를 분석하는 것도 가능하다. '식 트리'는 코드 데이터를 '트리(tree)' 자료구조의 형태로 담고 있기에 'a + b'는 내부적으로 다음과 같은 트리 형태로 보관된다.

그림 8.3 람다 식의 트리 표현
Expression 객체에 담긴 각각의 노드를 다음과 같이 순회할 수 있다.
using System.Linq.Expressions;
Expression<Func<int, int, int>> exp = (a, b) => a + b;
//람다 식 본체의 루트는 이항 연산자인 + 기호
BinaryExpression opPlus = exp.Body as BinaryExpression;
Console.WriteLine(opPlus.NodeType); // 출력 결과: Add
//이항 연산자의 좌측 연산자를 나타내는 표현식
ParameterExpression left = opPlus.Left as ParameterExpression;
Console.WriteLine(left.NodeType + ": " + left.Name); // 출력 결과: Parameter: a
//이항 연산자의 우측 연산자를 나타내는 표현식
ParameterExpression right = opPlus.Right as ParameterExpression;
Console.WriteLine(right.NodeType + ": " + right.Name); // 출력 결과: Parameter: b
데이터로 담겨 있는 람다 식은 컴파일하는 것도 가능하며, 이를 위해 Expression<T> 타입에서는 Compile 메서드를 제공한다.
이 메서드를 호출하면 Expression<T>의 형식 매개변수 타입에 해당하는 델리게이트가 반환되고 자연스럽게 함수 호출도 가능해진다.
using System.Linq.Expressions;
Expression<Func<int, int, int>> exp = (a, b) => a + b;
Func<int, int, int> func = exp.Compile();
Console.WriteLine(func(10, 2)); // 출력 결과: 12
또 다른 특징으로는 Expression<T> 객체를 람다식으로 초기화 하지 않고, 직접 코드와 관련된 Expression 객체로 구성할 수 있다.
따라서 'a + b' 에 해당하는 코드를 직접 예제 8.10과 같이 구성할 수 있고, 이것 역시 컴파일해서 델리게이트에 담아 호출 가능하다.
예제 8.10 Expression 타입으로 직접 구성한 식 트리
using System.Linq.Expressions;
ParameterExpression leftExp = Expression.Parameter(typeof(int), "a");
ParameterExpression rightExp = Expression.Parameter(typeof(int), "b");
BinaryExpression addExp = Expression.Add(leftExp, rightExp);
Expression<Func<int, int, int>> addLambda =
Expression<Func<int, int, int>>.Lambda<Func<int, int, int>>(
addExp, new ParameterExpression[] { leftExp, rightExp }
);
Console.WriteLine(addLambda.ToString()); // 출력 결과 : (a, b) => (a+b)
Func<int, int, int> addFunc = addLambda.Compile();
Console.WriteLine(addFunc(10, 12)); // 출력 결과: 22
예제 8.10에서는 Expression 타입의 정적 메서드로 제공되는 Parameter, Add 메서드를 이용해 'a + b'에 해당하는 ParameterExpression, BinaryExpression을 각각 생성한다.
이렇게 개별 Expression을 생성하는 Parameter와 Add 메서드를 일컬어 Expression 타입의 팩토리 메서드(factory method)라고 한다. 이 밖에 System.Linq.Expressions에는 다음과 같은 타입이 정의되어 있어 그에 해당하는 팩토리 메서드도 함께 Expression 타입의 정적 메서드로 제공된다.
표 8.1 System.Linq.Expressions 네임스페이스에 정의된 타입 및 대응되는 팩토리 메서드
| System.Linq.Expressions | 설명 | 대응되는 팩토리 메서드 |
| BinaryExpression | 이항 연산자에 해당하는 식 | Add, AddChecked, Divide, Modulo, Multiply, MultiplyChecked, Power, Subtract, SubtractChecked, And, Or, ExcusiveOr, LeftShift, RightShift, AndAlso, OrElse, Equal, NotEqual, GreaterThanOrEqual, GraterThan, LessThan, LessThanEqual, Coalesce, ArrayIndex |
| BlockExpression | 중괄호의 블록 표현 | Block |
| CatchBlock | 예외처리의 catch 구문 표현 | Catch |
| ConditionalExpression | 삼항 조건 연산자 구문 표현 | Condition |
| ConstantExpression | 상수식 표현 | Constant |
| DebugInfoExpression | 디버그 정보 표현 | DebugInfo |
| DefaultExpression | default 예약어를 표현 | Default |
| DynamicExpression | C# 4.0의 dynamic 예약어 표현 | Dynamic |
| ElementInit | IEnumerable 컬렉션의 단일 요소에 대한 초기화 표현 | ElementInit |
| GotoExpression | 점프문과 관련된 식을 표현 | Break, Continue, Goto, Return |
| IndexExpression | 배열의 인덱스 구문 표현 | ArrayAccess |
| InvocationExpression | 델리게이트 또는 람다 식의 호출 표현 | Invoke |
| LabelExpression | 라벨을 표현 | Label |
| LambdaExpression | 람다 식을 표현 | Lambda |
| ListInitExpression | 컬렉션 초기화 구문을 표현 | ListInit |
| LoopExpression | 루프 구문을 표현 | Loop |
| MemberAssignment | 대입 연산자 표현 | Bind |
| MemberExpression | 필드/속성의 접근 표현 | Field, Property, PropertyOrField |
| MemberInitExpression | 객체 초기화 구문 표현 | MemberInit |
| MemberMemberBinding | 클래스의 멤버 변수 내부에 있는 멤버를 재귀적으로 초기화하는 구문 표현 | MemberBind |
| MethodCallExpression | 정적 또는 인스턴스 메서드에 대한 호출을 표현 | Call, ArrayIndex |
| NewArrayExpression | 배열 생성 및 초기화 표현 | NewArrayBounds, NewArrayInit |
| NewExpression | 생성자를 호출하는 코드 표현 | New |
| ParameterExpression | 이름이 부여된 매개변수 표현 | Parameter |
| SwitchCase | switch 구문의 case 절 표현 | SwitchCase |
| SwitchExpression | switch 구문 표현 | Switch |
| TryExpression | 예외 처리 구문을표현 | TryCatch |
| TypeBinaryExpression | is 연산자 표현 | TypeIs |
| UnaryExpression | 단항 연산자 표현 | ArrayLength, Convert, ConvertChecked, Negate, NegateChecked, Not, Quote, TypeAs, UnaryPlus |
표 8.1 팩토리 메서드를 이용하면 일반적인 메서드 내부의 C# 코드를 Expression의 조합만으로도 프로그램 실행 시점에 만들어 내는 것이 가능하다.
8.9 LINQ
C# 과 VB.NET 컴파일러가 데이터 열거/ 정보 선택 작업을 일관된 방법으로 다루고자 확장한 문법.
LINQ는 전형적으로 컬렉션을 대상으로 쿼리를 수행한다.
예제 8.11 LINQ 쿼리 - 컬렉션의 모든 요소 선택
using System;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
//예제 8.11 LINQ 쿼리 - 컬렉션의 모든 요소 선택
var all = from person in people select person;
foreach(var item in all)
{
Console.WriteLine(item);
}
}
}
#출력
Tom: 63 in Korea
Winnie: 33 in Korea
Ais: 43 in CHINA
Aily: 65 in JAPAN
Benson: 25 in USA
from person in people == foreach(var person in people)
select person == yield return person
yield return 구문이 IEnumerable<T>를 반환한다. 이를 통해 예제 8.11의 all 변수(var)값에 담긴 자료형식은 IEnumerable<Person>이다.
IEnumerable<Person> all = from person in people select person;
LINQ 쿼리도 간편표기법이다. LINQ 쿼리는 C#컴파일러에 의해 빌드 시에 원래의 확장 메서드를 사용하는 코드로 변경되어 컴파일이 된다.
using System;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
//예제 8.11 LINQ 쿼리 - 컬렉션의 모든 요소 선택
var all = from person in people select person;
foreach(var item in all)
{
Console.WriteLine(item);
}
Console.WriteLine();
var all2 = people.Select((elem) => elem);
foreach (var item in all2)
{
Console.WriteLine(item);
}
}
}
from person in people select person; == people.Select((elem) => elem);
표 8.2 LINQ 대응 표현
| LINQ 표현 | from person in people select person; |
| 확장 메서드 표현 | people.Select((elem) => elem); |
| 일반 메서드 표현 | IEnumerable< Person > SelectFunc(List < Person > people){ foreach(var item in people) { yield return item; } } |
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
// SelectFunc 메서드 정의
static IEnumerable<Person> SelectFunc(List<Person> people)
{
foreach (var item in people)
{
yield return item;
}
}
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
//예제 8.11 LINQ 쿼리 - 컬렉션의 모든 요소 선택
var all = from person in people select person;
foreach (var item in all)
{
Console.WriteLine(item);
}
Console.WriteLine();
var all2 = people.Select((elem) => elem);
foreach (var item in all2)
{
Console.WriteLine(item);
}
Console.WriteLine();
// SelectFunc 메서드 호출하여 IEnumerable<Person> 할당
var all3 = SelectFunc(people);
// 결과 출력
foreach (var person in all3)
{
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}
}
}
#출력
Tom: 63 in Korea
Winnie: 33 in Korea
Ais: 43 in CHINA
Aily: 65 in JAPAN
Benson: 25 in USA
Tom: 63 in Korea
Winnie: 33 in Korea
Ais: 43 in CHINA
Aily: 65 in JAPAN
Benson: 25 in USA
Name: Tom, Age: 63
Name: Winnie, Age: 33
Name: Ais, Age: 43
Name: Aily, Age: 65
Name: Benson, Age: 25
people 목록을 사람 이름만 담긴 string 타입의 목록을 바꾸기.
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
// nameList 타입은 IEnumerable<string>
//LINQ 표현
var nameList = from person in people
select person.Name;
foreach(var item in nameList)
{
Console.WriteLine(item);
}
}
}
#출력
Tom
Winnie
Ais
Aily
Benson
확장 메서드 표현
var nameList = people.Select((elem) => elem.Name);
익명 타입을 select에 사용하기.
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
var dataList = from person in people
select new { Name = person.Name, Year = DateTime.Now.AddYears(-person.Age).Year };
foreach(var item in dataList)
{
Console.WriteLine($"{item.Name}: {item.Year}");
}
}
}
# 출력
Tom: 1962
Winnie: 1992
Ais: 1982
Aily: 1960
Benson: 2000
상기 LINQ 표현을 확장메서드로 표현시 다음과 같다.
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
var dataList = people.Select(
(elem) => new { Name = elem.Name, Year = DateTime.Now.AddYears(-elem.Age).Year });
foreach(var item in dataList)
{
Console.WriteLine($"{item.Name}: {item.Year}");
}
}
}
#출력
Tom: 1962
Winnie: 1992
Ais: 1982
Aily: 1960
Benson: 2000
LINQ는 C# 3.0에 추가된 문법으로 새로운 문법들이 LINQ와 혼합돼 사용된다. MS에선 LINQ 도입 위해 var 예약어, 객체 초기화, 익명 타입, 람다식, 확장메서드를 넣을 수 밖에 없었다.
8.9.1 where, orderby, group by, join
where
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
var endWithS = from person in people
where person.Name.EndsWith("s")
select person;
Console.WriteLine(string.Join(Environment.NewLine, endWithS));
}
}
# 출력
Ais: 43 in CHINA
order by
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
var endWithS = from person in people
orderby person.Age // 나이순 정렬
select person;
Console.WriteLine(string.Join(Environment.NewLine, endWithS));
}
}
#출력
Benson: 25 in USA
Winnie: 33 in Korea
Ais: 43 in CHINA
Tom: 63 in Korea
Aily: 65 in JAPAN
기본은 오름차순 (ascending), 내림차순 하고자 하면 다음과 같이 descending을 지정한다.
from person in people
order by person.Age descending
select person;
orderby에 올 수있는 값은 IComparable 인터페이스가 구현된 타입이면 된다.
group ... by
using System;
using System.Collections.Generic;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
var addGroup = from person in people
group person by person.Address;
foreach(var itemGroup in addGroup) // group by로 묶여진 그룹 나열
{
Console.WriteLine(string.Format($"[{itemGroup.Key}]"));
foreach(var item in itemGroup) // 해당 그룹 내에 속한 항목 나열
{
Console.WriteLine(item);
}
Console.WriteLine();
}
}
}
#
[Korea]
Tom: 63 in Korea
Winnie: 33 in Korea
[CHINA]
Ais: 43 in CHINA
[JAPAN]
Aily: 65 in JAPAN
[USA]
Benson: 25 in USA
그룹을 in으로 구분해 표시.
상기 코드는 컬렉션의 항목을 주소별로 그루핑하는 방법이다. 그룹화 시 select 구문은 올 수 없다. group ... by 기능이 select를 담당하고 있기 때문이다. 그래서 group ... by를 select ... by로 봐도 된다.(물론 LINQ에선 허용되지 않음.)
using ConsoleApp1;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "Benson", Language= "CPP"},
};
var addGroup = from person in people
group new{Name = person.Name, Age = person.Age } by person.Address;
foreach(var itemGroup in addGroup) // group by로 묶여진 그룹 나열
{
Console.WriteLine(string.Format($"[{itemGroup.Key}]"));
foreach(var item in itemGroup) // 해당 그룹 내에 속한 항목 나열
{
Console.WriteLine(item);
}
Console.WriteLine();
}
}
}
#출력
[Korea]
{ Name = Tom, Age = 63 }
{ Name = Winnie, Age = 33 }
[CHINA]
{ Name = Ais, Age = 43 }
[JAPAN]
{ Name = Aily, Age = 65 }
[USA]
{ Name = Benson, Age = 25 }
Join
using ConsoleApp1;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "eigen", Language= "CPP"},
};
var nameToLangList = from person in people
join language in languages on person.Name equals language.Name
select new { Name = person.Name, Age = person.Age, Language = language.Language };
Console.WriteLine(string.Join(Environment.NewLine, nameToLangList));
}
}
#출력
{ Name = Winnie, Age = 33, Language = C# }
{ Name = Ais, Age = 43, Language = JAVA }
{ Name = Aily, Age = 65, Language = PYTHON }
join 특징
1. on .... equals .... 조건을 만족하는 모든 레코드를 찾는다.
2. on ... equals ... 조건을 만족하는 레코드가 없다면 제외시킨다.
위와 같은 Join은 내부 조인(Inner Join)이다.
반면, 해당 레코드를 누락시키지 않고 포함시키는 것을 외부 조인(Outer Join이라한다.)
SQL 쿼리는 외부조인을 위한 구문 존재하나 LINQ는 별도의 예약어가 없어 이를 위해 join으로 엮이는 컬렉션을 한번 더 후 처리해야하는데 코드는 다음과 같다.
using ConsoleApp1;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "eigen", Language= "CPP"},
};
var nameToLangAllList = from person in people
join language in languages on person.Name equals language.Name into lang
from language in lang.DefaultIfEmpty(new MainLanguage())
select new { Name = person.Name, Age = person.Age, Language = language.Language };
Console.WriteLine(string.Join(Environment.NewLine, nameToLangAllList));
}
}
#출력
{ Name = Tom, Age = 63, Language = }
{ Name = Winnie, Age = 33, Language = C# }
{ Name = Ais, Age = 43, Language = JAVA }
{ Name = Aily, Age = 65, Language = PYTHON }
{ Name = Benson, Age = 25, Language = }
상기 결과는 LEFT OUTER JOIN
표 8.3 LINQ 예약어와 확장메서드의 관계
| LINQ | IEnumerable<T> 확장 메서드 |
| Select | Select |
| Where | Where |
| orderby[ascending] | OrderBy |
| orderby[descending] | OrderByDescending |
| group ... by | GroupBy |
| join ... in ... on ... equals | Join |
| join ... in ... on ... equals ... into | GroupJoin |
8.9.2 표준 쿼리 연산자
LINQ 쿼리 대상은 IEnumerable<T> 타입이거나 그것을 상속한 객체여야 한다.
그와 같은 객체에 LINQ쿼리를 사용하면 C# 컴파일러는 내부적으로 IEnumerable<T> 확장 메서드로 변경해 소스코드를 빌드한다.
이 때문에 IEnumerable<T>에 정의된 확장 메서드는 표준 쿼리 연산자(standard query operators)라고 한다.
표 8.4 표준 쿼리 연산자(P635 ~637 참고)
| 종류 | 표준 쿼리 연산자 | 반환 형식 | 설명 |
| 정렬(Sorting DatA) | OrderBy | IOrderedEnumerable <TElement> |
오름차순 정렬 |
| OrderByDescending | IOrderedEnumerable <TElement> |
내림차순 정렬 | |
| ThenBy | IOrderedEnumerable <TElement> |
2차 오름차순 정렬 | |
| ThenByDescending | IOrderedEnumerable <TElement> |
2차 내림차순 정렬 | |
| Reverse | IEnumerable<T> | 역방향 열거 | |
| 집합(Set) | Distinct | IEnumerable<T> | 중복값 제거 |
| Except | IEnumerable<T> | 두 컬렉션의 차집합에 해당하는 요소의 목록을 반환 | |
| Intersect | IEnumerable<T> | 두 컬렉션의 교집합에 해당하는 요소의 목록을 반환 | |
| Union | IEnumerable<T> | 두 컬렉션의 합집합에 해당하는 요소의 목록을 반환 | |
| 필터링 | OfType | ... | ... |
| Where | |||
| 수량 | All | ||
| Any | |||
| Contains | |||
| 프로젝션(Projection) | Select | ||
| SelectMany | |||
| 분할(Partitioning Data) | Skip | ||
| SkipWhile | |||
| Take | |||
| TakeWhile | |||
| Join | Join | ||
| GroupJoin | |||
| 그룹화 | GroupBy | ||
| ToLookup | |||
| 생성 | DefaultIfEmpty | ||
| Empty | |||
| Range | |||
| Repeat | |||
| 비고 | SequenceEqual | ||
| 요소 | ElementAt | ||
| ElementAtOrDefault | |||
| First | |||
| FirstOrDefault | |||
| Last | |||
| LastOrDefault | |||
| Single | |||
| SingleOrDefault | |||
| 형변환 | AsEnumerable |
LINQ 쿼리에 대응하지 않는 표준 연산자는 IEnumerable<T> 타입을 대상으로 동작하기에 LINQ 쿼리의 결과와 함께 쓸 수 있다.
예제 8.12 Max와 LINQ 쿼리의 조합 과 확장 메서드만의 조합
using ConsoleApp1;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "eigen", Language= "CPP"},
};
var all = from person in people
where person.Address == "Korea"
select person;
//주소가 Korea인 사람 가운데 가장 높은 연령 추출
var oldestAge = all.Max((elem) => elem.Age);
Console.WriteLine(oldestAge); // 출력 63
//확장 메서드의 조합
var oldestAge2 = people.Where((elem) => elem.Address == "Korea")
.Max((elem) => elem.Age);
Console.WriteLine(oldestAge2); // 출력 63
}
}
Max와 같은 단일 값을 반환하는 메서드는 LINQ 쿼리가 수행되자 마자 결괏값이 생성된다.
표 8.4 표준 쿼리 연산자 가운데 IEnumerable<T>, IOrderedEnumerable<TElement>를 반환하는 메서드를 제외한 다른 모든 것들은 LINQ 식이 평가되면서 곧바로 실행된다.
static bool IsEqual(string arg1, string arg2)
{
Console.WriteLine("Executed");
return arg1 == arg2;
}
이 메서드를 이용해 지연테스트의 효과를 테스트 해보자.
예제 8.13 LINQ 쿼리의 지연 실행 테스트
using ConsoleApp1;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace ConsoleApp1;
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
public override string ToString()
{
return string.Format("{0}: {1} in {2}", Name, Age, Address);
}
}
class MainLanguage
{
public string Name { get; set; }
public string Language { get; set; }
}
class Program
{
static bool IsEqual(string arg1, string arg2)
{
Console.WriteLine("Executed");
return arg1 == arg2;
}
static void Main(string[] args)
{
List<Person> people = new List<Person>
{
new Person{Name="Tom", Age=63, Address = "Korea"},
new Person{Name="Winnie", Age=33, Address = "Korea"},
new Person{Name="Ais", Age=43, Address = "CHINA"},
new Person{Name="Aily", Age=65, Address = "JAPAN"},
new Person{Name="Benson", Age=25, Address = "USA"},
};
List<MainLanguage> languages = new List<MainLanguage>
{
new MainLanguage{Name = "Anderson", Language= "C#"},
new MainLanguage{Name = "Winnie", Language= "C#"},
new MainLanguage{Name = "Ais", Language= "JAVA"},
new MainLanguage{Name = "Aily", Language= "PYTHON"},
new MainLanguage{Name = "eigen", Language= "CPP"},
};
//LINQ 쿼리가 바로 실행됨
Console.WriteLine("ToList() executed");
var inKorea = (
from person in people
where IsEqual(person.Address, "Korea")
select person).ToList();
// IsEqual 메서드의 실행으로 인해 화면에는 "Executed" 문자열 출력됨.
Console.ReadLine();
Console.WriteLine("IEnumerable<T> Where/Select evaluated");
// IEnumerable<T>를 반환하므로 LINQ 쿼리가 평가만 되고 실행되지 않음.
var inKorea2 = from person in people
where IsEqual(person.Address, "Korea")
select person;
//따라서 IsEqual 메서드가 실행되지 않으므로 화면에는 "Executed" 문자열이 실행되지 않음.
Console.ReadLine();
// Take 확장 메서드 역시 IEnumerable<T>를 반환하기에 "Executed" 문자열 출력 x
Console.WriteLine("IEnumerable<T> Take evaluated");
var firstPeople = inKorea2.Take(1);
Console.ReadLine();
//열거를 시작했을 때 LINQ 쿼리가 실제로 실행됨.
//이때서야 비로소 "Executed" 문자열이 화면에 출력됨.
foreach(var item in firstPeople)
{
Console.WriteLine(item);
}
//단일 값을 반환하는 Single 메서드의 호출은 곧바로 LINQ 쿼리가 실행되게 만듦.
Console.WriteLine(firstPeople.Single());
}
}
#출력
ToList() executed
Executed
Executed
Executed
Executed
Executed
d
IEnumerable<T> Where/Select evaluated
d
IEnumerable<T> Take evaluated
d
Executed
Tom: 63 in Korea
Executed
Tom: 63 in Korea
Linq 쿼리라 해서 모두 지연된 연산(Lazy evaluation) 또는 지연 실행(deferred execution) 방식으로 동작하지 않는다. 쿼리의 반환 타입이 IEnumerable<T>, IOrderedEnumerable<TElement>가 아니라면 그 즉시 실행되어 실행 결과가 반환된다는 점을 알아두자.
8.9.3 일관된 데이터 조회
LINQ 쿼리가 IEnumerable<T> 타입과 그것을 상속 받은 타입을 대상으로 동작한다. 닷넷 응용프로그램에서 IEnumerable<T> 대상이 되는 타입은 배열, List<T>, Dictionary<TKey, TValue> 등의 컬렉션이기에 지금까지 아무 문제 없이 LINQ 코드를 실습했다.
MS는 일부 자료형에 대해 LINQ를 위한 IEnumerable<T> 상속 타입을 정의해두었다.
예) XML 자료형에 LINQ 쿼리 수행 목적으로 'System.Xml.Linq' 네임스페이스 아래 IEnumerable<T>와 연동 가능한 XElement, XAttribute, XDocument 등의 타입을 만듦.
LINQ 제공자(provider)란 XML 자료형에 타입을 만든 것과 같이 특정 자료형에 대해 LINQ를 사용할 수 있게 별도 타입을 정의해 둔 것이다. System.Xml.Linq에 대해선 LINQ to XML이라고 명명된다.
MS에선 SQL 데이터베이스를 위한 LINQ 제공자를 'System.Data.Linq' 네임스페이스 아래에 구현 했고 이를 LINQ to SQL이라 한다.
지금까지 LINQ 제공자없이 닷넷 컬렉션에 대해 수행했는데, 이를 LINQ to Objects라는 이름으로 구분해 칭한다.
LINQ 기술은 갖가지 다양한 데이터 원본에 대한 접근법을 단일화했다는 점이 의미가 있다. 이는 데이터 원본마다 접근 시 별도의 API 사용법을 익힐 필요가 없다.

그림 8.6 LINQ 관계도 (P641)
앞서 LINQ to Objects 사용법을 익혔기에 나머지 데이터 원본에 대해서도 유사 쿼리를 수행할 수 있는 기초는 마련됐다.
예제 8.14 LINQ to XML
using System.Xml.Linq;
string txt = @"
<people>
<person name = 'anderson' age = '15' />
<person name = 'mike' age = '21' />
</people>
";
StringReader sr = new StringReader(txt);
var xml = XElement.Load(sr);
var query = from person in xml.Elements("person")
select person;
foreach(var item in query)
{
Console.WriteLine(item.Attribute("name").Value +
": " +
item.Attribute("age").Value);
}
# 출력
anderson: 15
mike: 21'C#(.Net)' 카테고리의 다른 글
| [시작하세요 C# 12 프로그래밍 ] (#10) C# 5.0 (0) | 2025.05.02 |
|---|---|
| [시작하세요 C# 12 프로그래밍 ] (#9) C# 4.0 (0) | 2025.04.30 |
| [시작하세요 C# 12 프로그래밍 ] #6 BCL - 04 (6.9 ~ 6.10) (0) | 2025.04.20 |
| [시작하세요 C# 12 프로그래밍 ] #6 BCL - 03 (6.7 ~ 6.8) (0) | 2025.04.12 |
| [시작하세요 C# 12 프로그래밍 ] #6 BCL - 02 (6.4~6.6) (0) | 2025.04.05 |