C# Journey
[실습] 처음부터 지금까지, C# 정리하기
소개
이번 실습에서는 C#의 주요 기능 발전을 단계별로 살펴보며, C# 프로그래밍 스타일이 어떻게 변화했는지를 보여줍니다. 초기 버전부터 최신 버전까지 점진적으로 개선된 코드를 통해, C#이 제공하는 다양한 기능을 정리할 수 있습니다.
따라하기: 단계별 연습을 위한 GitHub 리포지토리 생성
GitHub에 csharp-journey 이름으로 리포지토리를 생성합니다.
필자의 리포지토리는 다음 경로입니다.
https://github.com/VisualAcademy/csharp-journey
따라하기: C# 콘솔 응용 프로그램 설정
이 실습은 각 C# 버전에서 도입된 기능을 직접 실행해볼 수 있도록 설계되었습니다. 이를 위해 CsharpJourney라는 C# 콘솔 응용 프로그램을 만들어 각 단계별 기능을 테스트할 수 있습니다.
CsharpJourney 프로젝트 생성
먼저, Visual Studio 또는 .NET CLI를 사용하여 새 C# 콘솔 애플리케이션을 만듭니다.
.NET CLI를 사용하는 경우
mkdir CsharpJourney
cd CsharpJourney
dotnet new console
Visual Studio를 사용하는 경우
- Visual Studio를 실행합니다.
- 새 프로젝트 만들기에서 콘솔 앱(.NET)을 선택합니다.
- 프로젝트 이름을
CsharpJourney
로 설정하고 생성합니다.
각 C# 버전별 기능을 단계적으로 실행
이제 Program.cs
파일을 열고, C# 1.0부터 최신 버전까지의 주요 기능을 하나씩 추가하면서 실행해 볼 수 있습니다. 각 따라하기의 코드를 작성하고 실행하면, C#이 어떻게 발전해왔는지 직접 경험할 수 있습니다.
따라하기: C# 1.0 - 기본적인 함수형 접근
C# 1.0에서는 제네릭 기능이 없었기 때문에, 컬렉션을 다룰 때 ArrayList
를 사용해야 했습니다. 또한 Predicate
과 같은 대리자를 직접 정의해야 했습니다.
코드: Program.cs
using System;
using System.Collections;
class Program
{
static void Main()
{
int[] array = { 4, 8, 15, 16, 23 };
int[] query = Filter(array, GreaterThanFive);
foreach (int value in query) { Console.WriteLine(value); }
}
static bool GreaterThanFive(int i) { return i > 5; }
static int[] Filter(int[] src, Predicate p)
{
ArrayList dst = new ArrayList();
foreach (int value in src) { if (p(value)) dst.Add(value); }
int[] result = new int[dst.Count];
for (int i = 0; i < result.Length; i++) { result[i] = (int)dst[i]; }
return result;
}
delegate bool Predicate(int i);
}
8
15
16
23
이 코드는 C# 1.0 환경에서 제네릭 없이 ArrayList
와 사용자 정의 대리자를 활용해 5보다 큰 값만 선별하는 필터 함수를 구현하고, 해당 결과를 출력하는 예제입니다.
따라하기: C# 9.0 - 최상위 문 도입
C# 9.0에서는 최상위 문(Top-Level Statements)을 도입하여, Main
메서드 없이도 코드 실행이 가능해졌습니다. 이를 통해 C# 기본(보일러플레이트) 코드를 줄이고, 보다 간결한 스크립트 스타일의 코드를 작성할 수 있습니다.
using System;
using System.Collections;
int[] array = { 4, 8, 15, 16, 23 };
int[] query = Filter(array, GreaterThanFive);
foreach (int value in query) { Console.WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
int[] Filter(int[] src, Predicate p)
{
ArrayList dst = new ArrayList();
foreach (int value in src) { if (p(value)) dst.Add(value); }
int[] result = new int[dst.Count];
for (int i = 0; i < result.Length; i++) { result[i] = (int)dst[i]; }
return result;
}
delegate bool Predicate(int i);
실행 결과는 처음 코드와 동일합니다.
따라하기: C# 6.0 - using static
도입
C# 6.0에서는 using static
키워드를 활용하여 Console.WriteLine
을 줄여 쓸 수 있습니다. 이를 통해 보다 깔끔한 코드를 작성할 수 있습니다.
using System;
using System.Collections;
using static System.Console;
int[] array = { 4, 8, 15, 16, 23 };
int[] query = Filter(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
int[] Filter(int[] src, Predicate p)
{
ArrayList dst = new ArrayList();
foreach (int value in src) { if (p(value)) dst.Add(value); }
int[] result = new int[dst.Count];
for (int i = 0; i < result.Length; i++) { result[i] = (int)dst[i]; }
return result;
}
delegate bool Predicate(int i);
따라하기: C# 10.0 - Global Usings 도입
C# 10.0에서는 global using
을 지원하여, 모든 파일에서 반복적으로 사용해야 하는 using
문을 별도로 지정할 수 있습니다. 이를 활용하면 코드의 가독성을 높이고 유지보수를 쉽게 할 수 있습니다.
프로젝트 Program.cs와 함께 추가로 GlobalUsings.cs 파일을 생성하고 다음과 같이 코드를 작성합니다.
코드: CsharpJourney\GlobalUsings.cs
global using System;
global using System.Collections;
global using static System.Console;
코드: CsharpJourney\Program.cs
int[] array = { 4, 8, 15, 16, 23 };
int[] query = Filter(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
int[] Filter(int[] src, Predicate p)
{
ArrayList dst = new ArrayList();
foreach (int value in src) { if (p(value)) dst.Add(value); }
int[] result = new int[dst.Count];
for (int i = 0; i < result.Length; i++) { result[i] = (int)dst[i]; }
return result;
}
delegate bool Predicate(int i);
따라하기: C# 2.0 - 제네릭 도입
C# 2.0에서는 List<T>
와 같은 제네릭이 도입되면서, ArrayList
를 사용할 필요가 없어졌습니다. 이를 통해 박싱/언박싱을 줄이고 성능을 향상시킬 수 있었습니다. 또한, 제네릭 대리자와 IEnumerable<T>
를 활용한 보다 유연한 데이터 처리가 가능해졌습니다.
제네릭을 활용한 컬렉션 처리
기존의 ArrayList
대신 List<T>
를 사용하여 타입 안정성을 보장할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
int[] query = Filter(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
int[] Filter(int[] src, Predicate p)
{
List<int> dst = new List<int>();
foreach (int value in src) { if (p(value)) dst.Add(value); }
int[] result = new int[dst.Count];
for (int i = 0; i < result.Length; i++) { result[i] = dst[i]; }
return result;
}
delegate bool Predicate(int i);
제네릭 헬퍼 함수
리스트를 배열로 변환하는 과정에서 반복적인 코드 작성을 줄이기 위해 ToArray()
를 직접 활용할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
int[] query = Filter(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
int[] Filter(int[] src, Predicate p)
{
List<int> dst = new List<int>();
foreach (int value in src) { if (p(value)) dst.Add(value); }
return dst.ToArray();
}
delegate bool Predicate(int i);
NOTE
C# 12.0부터는 ToArray()
대신 컬렉션 식을 써서 배열을 더 간단하게 만들 수 있어요. 예를 들어, dst.ToArray()
는 [..dst]
처럼 바꿔 쓸 수 있죠.
또, new List<int>()
처럼 빈 리스트를 만들던 것도 이제는 []
로 훨씬 간단하게 표현할 수 있어요.
컬렉션 식에 대한 더 많은 정보는 아래 링크를 참고해 보세요.
C# 12.0의 컬렉션 식을 사용해서 최종 다듬어진 Filter
메서드의 코드는 다음과 같습니다.
int[] Filter(int[] src, Predicate p)
{
List<int> dst = [];
foreach (int value in src) { if (p(value)) dst.Add(value); }
return [.. dst];
}
부작용(Side Effect)
이번에는 프로그래밍에서 말하는 "부작용(Side Effect)"에 대해 간단히 살펴보겠습니다.
int[]
처럼 배열을 반환하는 경우, 반환된 배열이 외부에서 수정될 수 있기 때문에 예기치 않은 부작용이 발생할 수 있습니다.
int[] query = Filter(array, GreaterThanFive);
query[0] = 1234; // 부작용: 출력 전에 배열 값이 변경됨
foreach (int value in query) { WriteLine(value); }
1234
15
16
23
물론 위 코드는 부작용을 인위적으로 발생시키기 위한 예시이지만, 이와 유사한 상황은 실제 개발 환경에서도 충분히 발생할 수 있습니다.
배열은 참조형이기 때문에, 반환된 배열이 외부 코드에 의해 수정되면 이후 해당 배열을 사용하는 코드의 동작 결과가 달라질 수 있습니다.
이로 인해 코드의 예측 가능성과 안정성이 낮아질 수 있으므로, 이러한 부작용에 주의할 필요가 있습니다.
IEnumerable<T>
사용
IEnumerable<T>
는 필요할 때까지 데이터를 계산하거나 처리하지 않는 지연 실행(Lazy Evaluation)을 통해 성능을 최적화하고, 수정 없이 반복 처리에만 집중함으로써 코드에서 읽기 전용의 의도를 분명히 할 수 있습니다. 이를 통해 부작용 없는 안전한 데이터 흐름이 가능합니다.
int[] array = { 4, 8, 15, 16, 23 };
IEnumerable<int> query = Filter(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
IEnumerable<int> Filter(IEnumerable<int> src, Predicate p)
{
List<int> dst = [];
foreach (int value in src) { if (p(value)) dst.Add(value); }
return [.. dst];
}
delegate bool Predicate(int i);
제네릭 대리자 활용
제네릭 대리자를 사용하면 다양한 형식에 대해 재사용 가능한 필터링 로직을 구현할 수 있습니다. 조건을 전달하는 부분도 형식에 맞게 유연하게 처리할 수 있어, 코드의 재사용성과 형식 안정성을 높여줍니다.
int[] array = { 4, 8, 15, 16, 23 };
IEnumerable<int> query = Filter<int>(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Predicate<T> p)
{
List<T> dst = [];
foreach (T value in src) { if (p(value)) dst.Add(value); }
return [.. dst];
}
delegate bool Predicate<T>(T i);
이 예제는 int
뿐 아니라 string
, double
등 다른 형식에도 동일한 방식으로 적용할 수 있습니다.
yield
키워드를 활용한 성능 최적화
yield return
을 사용하면 데이터를 필요할 때마다 한 개씩 반환하는 방식으로 성능을 향상시킬 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
IEnumerable<int> query = Filter(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Predicate<T> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
delegate bool Predicate<T>(T i);
무명 메서드(Anonymous Method) 활용
익명 메서드를 사용하면 대리자를 직접 정의하지 않고 바로 조건을 지정할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
IEnumerable<int> query = Filter(array, delegate (int i) { return i > 5; });
foreach (int value in query) { WriteLine(value); }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Predicate<T> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
delegate bool Predicate<T>(T i);
System.Predicate<T>
사용
기본 제공되는 Predicate<T>
를 활용하면 별도의 대리자 선언 없이도 조건을 지정할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
IEnumerable<int> query = Filter(array, delegate (int i) { return i > 5; });
foreach (int value in query) { WriteLine(value); }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Predicate<T> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
Func<T>
사용
C# 2.0에서는 Predicate<T>
뿐만 아니라 Func<T>
대리자를 활용하여 보다 직관적인 방식으로 필터링 로직을 정의할 수 있습니다. Func<T, bool>
을 사용하면 람다식을 보다 쉽게 적용할 수 있으며, System.Predicate<T>
와 달리 다양한 시나리오에서 활용할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
IEnumerable<int> query = Filter(array, delegate (int i) { return i > 5; });
foreach (int value in query) { WriteLine(value); }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Func<T, bool> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
Func<T, bool>
을 활용하면 익명 메서드 대신 람다식을 간편하게 사용할 수 있으며, 함수형 스타일의 코드 작성이 더욱 자연스러워집니다.
따라하기: C# 3.0 - 람다식과 LINQ를 활용한 간결한 데이터 처리
C# 3.0에서는 람다식(Lambda Expressions) 과 LINQ(Language Integrated Query) 가 도입되면서 함수형 프로그래밍 스타일이 더욱 간결해졌습니다. 이를 통해 데이터를 보다 직관적이고 선언적인 방식으로 필터링하고 변환할 수 있습니다.
기존 방식: 명시적 대리자 사용
기존 방식에서는 Predicate
, Func
대리자 등을 사용하여 조건을 정의하고 필터링해야 했습니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = Filter(array, GreaterThanFive);
foreach (int value in query) { WriteLine(value); }
bool GreaterThanFive(int i) { return i > 5; }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Func<T, bool> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
람다식을 활용한 간결한 코드
C# 3.0에서는 람다식을 사용하여 필터링 조건을 간단하게 정의할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = Filter(array, i => i > 5);
foreach (int value in query) { WriteLine(value); }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Func<T, bool> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
람다식 파이프라인을 통한 체이닝(Chaining)
람다식을 활용하면 여러 개의 필터를 파이프라인 방식으로 연결할 수도 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = Filter(Filter(array, i => i > 5), i => i % 2 == 0);
foreach (int value in query) { WriteLine(value); }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Func<T, bool> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
확장 메서드를 활용한 필터링
C# 3.0에서는 확장 메서드(Extension Methods) 를 사용하여 기존 타입을 확장할 수 있습니다. 아래 예제에서는 Filter
메서드를 IEnumerable<T>
에 확장 메서드로 추가하여 더 간결한 체이닝이 가능하도록 했습니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = array.Filter(i => i > 5).Filter(i => i % 2 == 0);
foreach (int value in query) { WriteLine(value); }
static class MyExtensions
{
public static IEnumerable<T> Filter<T>(
this IEnumerable<T> src, Func<T, bool> p)
{
foreach (T value in src) { if (p(value)) yield return value; }
}
}
LINQ를 활용한 데이터 필터링
C# 3.0에서는 AsQueryable()
를 활용하여 LINQ 메서드를 사용할 수 있습니다. 이를 통해 SQL과 유사한 방식으로 데이터를 필터링할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = array.AsQueryable().Where(i => i > 5).Where(i => i % 2 == 0);
foreach (int value in query) { WriteLine(value); }
이와 같은 방식은 필자가 개인적으로 가장 선호하는 코드 스타일 중 하나입니다. C# 1.0 시절에는 여러 줄에 걸쳐 작성해야 했던 로직을, 이제는 세 줄의 간결한 코드로 동일하거나 더 나은 기능을 구현할 수 있습니다.
Query Syntax를 활용한 가독성 높은 코드
LINQ의 쿼리 문법(Query Syntax) 을 사용하면 SQL과 비슷한 스타일로 데이터를 처리할 수 있습니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = from i in array.AsQueryable()
where i > 5
where i % 2 == 0
select i;
foreach (int value in query) { WriteLine(value); }
따라하기: C# 4.0 - 선택적 매개변수를 활용
C# 4.0에서는 기본값을 가진 선택적 매개변수를 지원하여, 함수 호출 시 특정 매개변수를 생략할 수 있도록 했습니다. 이를 통해 코드의 가독성이 향상되고, 오버로드된 메서드를 줄일 수 있습니다.
아래 예제에서는 ExecuteQuery
함수의 printMessage
매개변수에 기본값 true
를 설정하여, 인자를 생략하면 기본적으로 출력을 수행하도록 합니다.
int[] array = { 4, 8, 15, 16, 23 };
ExecuteQuery(array); // printMessage 기본값(true) 사용
void ExecuteQuery(int[] array, bool printMessage = true)
{
var query = array.AsQueryable().Where(i => i > 5 && i % 2 == 0);
if (printMessage)
foreach (int value in query)
WriteLine(value); // 기본적으로 값 출력
}
이 방식은 기본적인 동작을 유지하면서도 필요할 때만 특정 기능을 변경할 수 있도록 유연성을 제공합니다.
따라하기: C# 5.0 - async/await
을 활용한 비동기 프로그래밍
C# 5.0에서는 async/await
키워드가 도입되어, 비동기 작업을 보다 간결하고 직관적으로 작성할 수 있게 되었습니다. 이를 통해 UI 응답성을 개선하고, 비동기 실행을 효율적으로 관리할 수 있습니다.
아래 예제에서는 ExecuteQueryAsync
메서드를 async
로 선언하고, 데이터를 비동기적으로 처리하여 출력합니다.
int[] array = { 4, 8, 15, 16, 23 };
await ExecuteQueryAsync(array);
async Task ExecuteQueryAsync(int[] array, bool printMessage = true)
{
var query = array.AsQueryable().Where(i => i > 5 && i % 2 == 0);
if (printMessage)
foreach (int value in query)
await Task.Run(() => WriteLine(value)); // 비동기적으로 출력
}
이 코드에서는 Task.Run
을 사용하여 각 값을 별도의 작업으로 실행함으로써, 메인 스레드를 차단하지 않고 병렬 처리할 수 있도록 구현하였습니다.
따라하기: C# 6.0 - 식 본문 메서드
C# 6.0에서는 식 본문 메서드(Expression-bodied Methods) 가 도입되어 단순한 메서드를 더욱 간결하게 작성할 수 있습니다. 이를 활용하면 코드의 가독성이 향상되고, 불필요한 {}
블록을 줄일 수 있습니다.
아래 예제에서는 ExecuteQueryAsync
메서드를 식 본문으로 변환하여 한 줄로 작성하였습니다.
int[] array = { 4, 8, 15, 16, 23 };
await ExecuteQueryAsync(array);
async Task ExecuteQueryAsync(int[] array, bool printMessage = true) =>
await (printMessage ? Task.WhenAll(array.AsQueryable()
.Where(i => i > 5 && i % 2 == 0)
.Select(i => Task.Run(() => WriteLine(i)))) : Task.CompletedTask);
이 방식은 간결한 로직을 처리하는 메서드에 적합하며, 특히 람다 식과 함께 사용하면 더욱 직관적인 코드를 작성할 수 있습니다.
따라하기: C# 7.0 - 패턴 매칭을 활용한 타입 검사
C# 7.0에서는 패턴 매칭이 도입되어 is
키워드를 활용한 타입 검사를 더욱 간결하게 작성할 수 있습니다. 이를 통해 if-else
문 없이도 특정 타입을 판별하고, 조건에 맞는 로직을 실행할 수 있습니다.
아래 예제에서는 input
이 int[]
배열인지 확인한 후, 특정 조건을 만족하는 값만 필터링하여 출력합니다.
int[] array = { 4, 8, 15, 16, 23 };
await ExecuteQueryAsync(array);
async Task ExecuteQueryAsync(object input, bool printMessage = true)
{
if (input is int[] array) // input이 int[] 형식인지 확인
await (printMessage
? Task.WhenAll(array.AsQueryable()
.Where(i => i > 5 && i % 2 == 0)
.Select(i => Task.Run(() => WriteLine(i))))
: Task.CompletedTask);
}
패턴 매칭을 활용하면 타입 변환(cast
) 없이도 객체의 타입을 확인할 수 있어 코드가 더 직관적이고 간결해집니다.
따라하기: C# 8.0 - switch
식을 활용한 간결한 조건 처리
C# 8.0에서는 switch
식이 도입되어 조건 분기를 더 간결하게 작성할 수 있습니다. 특히 패턴 매칭과 결합하면 다양한 조건을 깔끔하게 처리할 수 있습니다.
아래 예제에서는 switch
식을 사용하여 입력 데이터를 검사하고, 특정 조건을 만족하면 비동기 작업을 실행합니다.
int[] array = { 4, 8, 15, 16, 23 };
await ExecuteQueryAsync(array);
// C# 8.0: switch 식 적용
async Task ExecuteQueryAsync(object input, bool printMessage = true) =>
await (input switch
{
int[] array when printMessage => Task.WhenAll(array.AsQueryable()
.Where(i => i > 5 && i % 2 == 0)
.Select(i => Task.Run(() => WriteLine(i)))), // 조건을 만족하는 값 출력
int[] array => Task.CompletedTask, // printMessage가 false일 때 실행 안 함
_ => throw new ArgumentException("잘못된 입력") // 잘못된 입력 처리
});
switch
식을 사용하면 if-else
문보다 간결하고 가독성이 뛰어난 코드 작성이 가능합니다.
따라하기: C# 9.0 - record
타입으로 불변 객체 만들기
C# 9.0에서는 record
타입이 추가되어 불변(immutable) 개체를 쉽게 만들 수 있습니다. 아래 예제에서는 QueryInput
을 record
로 정의하여 데이터를 변경할 때 with
식을 사용합니다.
int[] array = { 4, 8, 15, 16, 23 };
var input = new QueryInput { Data = array };
// 기존 데이터를 변경하여 새로운 record 생성
var modifiedInput = input with { Data = array.Where(i => i % 2 == 0).ToArray() };
await ExecuteQueryAsync(modifiedInput);
async Task ExecuteQueryAsync(object input, bool printMessage = true) =>
await (input switch
{
QueryInput { Data: int[] array } when printMessage
=> Task.WhenAll(array.AsQueryable()
.Where(i => i > 5 && i % 2 == 0)
.Select(i => Task.Run(() => WriteLine(i)))),
QueryInput => Task.CompletedTask,
_ => throw new ArgumentException("Invalid input type")
});
// C# 9.0: record 타입 사용
record QueryInput
{
public int[] Data { get; init; } = Array.Empty<int>();
}
위 코드에 컬렉션 식을 적용하면 다음과 같습니다.
int[] array = { 4, 8, 15, 16, 23 };
var input = new QueryInput { Data = array };
// 기존 데이터를 변경하여 새로운 record 생성
var modifiedInput = input with { Data = [.. array.Where(i => i % 2 == 0)] };
await ExecuteQueryAsync(modifiedInput);
async Task ExecuteQueryAsync(object input, bool printMessage = true) =>
await (input switch
{
QueryInput { Data: int[] array } when printMessage
=> Task.WhenAll(array.AsQueryable()
.Where(i => i > 5 && i % 2 == 0)
.Select(i => Task.Run(() => WriteLine(i)))),
QueryInput => Task.CompletedTask,
_ => throw new ArgumentException("Invalid input type")
});
// C# 9.0: record 타입 사용
record QueryInput
{
public int[] Data { get; init; } = [];
}
record
타입은 값 변경 시 새로운 인스턴스를 생성하는 방식으로 동작하므로, 데이터 무결성을 유지하면서도 유연한 변경이 가능합니다.
따라하기: C# 11.0 - 리스트 패턴을 활용한 데이터 매칭
C# 11.0에서는 리스트 패턴(List Patterns)이 도입되어, 배열이나 리스트에서 특정 패턴을 손쉽게 매칭할 수 있습니다. 아래 예제에서는 필터링된 데이터를 추출하고, 특정 패턴과 일치하는지 확인하는 기능을 구현합니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = Filter(array, i => i > 5);
foreach (int value in query) { Console.WriteLine(value); }
IEnumerable<T> Filter<T>(IEnumerable<T> src, Predicate<T> p)
{
foreach (T value in src)
{
if (p(value))
yield return value;
}
}
bool ContainsPattern(int[] numbers)
{
// 첫 번째가 3, 세 번째가 5, 마지막이 30이면 true
return numbers is [3, _, 5, .., 30];
}
WriteLine(ContainsPattern(array));
8
15
16
23
False
List Patterns을 활용하면 배열 내 특정 요소들의 위치와 값을 쉽게 확인할 수 있습니다. 이를 통해 조건에 맞는 데이터를 더 직관적으로 처리할 수 있습니다.
따라하기: C# 12.0 - 기본 생성자를 활용한 간결한 클래스 정의
C# 12.0에서는 기본 생성자(Primary Constructor) 기능이 도입되어, 생성자에서 필드를 선언하고 초기화하는 과정을 더욱 간결하게 작성할 수 있습니다. 이를 활용하면 불필요한 필드 선언을 줄이고, 클래스의 가독성을 높일 수 있습니다.
아래 코드에서는 QueryProcessor
클래스가 기본 생성자를 사용하여 data
배열을 직접 받아 저장합니다. 그리고 Filter
메서드를 통해 주어진 조건에 맞는 값만 필터링하여 반환합니다.
int[] array = { 4, 8, 15, 16, 23 };
var processor = new QueryProcessor(array);
var query = processor.Filter(i => i > 5);
foreach (int value in query) { WriteLine(value); }
// C# 12.0: Primary Constructor 적용
public class QueryProcessor(int[] data)
{
public IEnumerable<int> Filter(Func<int, bool> predicate)
{
foreach (var value in data)
if (predicate(value))
yield return value;
}
}
위 코드에서는 QueryProcessor
가 생성자에서 매개변수를 바로 받아 필드 없이 활용하는 방식으로 설계되었습니다. 이를 통해 기존보다 코드가 더욱 간결해지며, 객체 생성 시 불필요한 필드 선언 없이 데이터를 처리할 수 있습니다.
따라하기: C# 13.0 - 더욱 유연한 데이터 처리
C# 13.0에서는 함수형 스타일을 더욱 간결하게 만들고 가변 인자를 활용한 데이터 처리가 가능하도록 개선되었습니다. 아래 예제에서는 params
키워드를 사용하여 여러 개의 IEnumerable<int>
를 받아 필터링한 후, 원하는 조건에 맞는 값들만 추출하는 방식을 보여줍니다.
int[] array = { 4, 8, 15, 16, 23 };
var query = FilterAndProcess(array);
foreach (int value in query) { WriteLine(value); }
static IEnumerable<int> FilterAndProcess(params IEnumerable<int>[] numbers)
{
return numbers.SelectMany(n => n).Where(i => i > 5).Where(i => i % 2 == 0);
}
8
16
위 코드에서는 SelectMany
를 사용하여 모든 입력 컬렉션을 하나의 스트림으로 펼친 뒤, Where
메서드를 두 번 적용하여 5보다 크고 짝수인 값만 필터링합니다. 이를 통해 보다 유연하고 직관적인 데이터 처리가 가능합니다.
마무리
C#은 1.0 버전부터 최신 버전까지 점진적으로 발전해 오며, 함수형 프로그래밍 스타일과 다양한 편의 구문을 점차 수용해 왔습니다. 이번 실습을 통해 각 버전의 주요 변화와 그에 따른 코드 스타일의 흐름을 보다 쉽게 이해할 수 있었기를 바랍니다.