출시·고도화 중
C# 안내서 · 6/6
C#을 C#답게 만드는 세 가지 기능이 있습니다. 함수를 값처럼 주고받는 델리게이트와 람다, 컬렉션을 선언적으로 다루는 LINQ, 그리고 기다리는 동안 스레드를 붙잡지 않는 async/await입니다. 세 기능은 서로 이어져 있어서 람다를 이해하면 LINQ가 읽히고, LINQ와 Task를 함께 쓰면 비동기 코드도 간결해집니다.
델리게이트는 메서드를 가리키는 타입 안전한 참조입니다. 직접 선언할 수도 있지만, 대부분은 기본 제공되는 Func<...>(값을 돌려줌)와 Action<...>(돌려주지 않음)을 씁니다. 람다 식 (매개변수) => 식은 이름 없는 메서드를 그 자리에서 만드는 문법입니다.
Func<int, int, int> add = (a, b) => a + b;
Func<string, bool> isLong = s => s.Length > 5;
Action<string> log = message => Console.WriteLine($"[로그] {message}");
Console.WriteLine(add(2, 3)); // 5
Console.WriteLine(isLong("TASTEDEV")); // True
log("시작합니다");
// 메서드를 인수로 받는 메서드
static List<int> Apply(List<int> items, Func<int, int> transform)
{
var result = new List<int>();
foreach (var item in items)
result.Add(transform(item));
return result;
}
var doubled = Apply([1, 2, 3], x => x * 2); // [2, 4, 6]
// 람다는 바깥 변수를 붙잡을(capture) 수 있습니다
int threshold = 10;
Func<int, bool> overThreshold = x => x > threshold;이벤트도 델리게이트 위에 만들어진 기능입니다. button.Click += (sender, e) => ...처럼 +=로 처리기를 등록하는 코드가 바로 델리게이트에 람다를 더하는 모습입니다.
LINQ(Language Integrated Query)는 배열, 리스트, 데이터베이스 등 여러 데이터 원본을 같은 방식으로 질의하는 기능입니다. System.Linq의 확장 메서드에 람다를 넘기는 메서드 구문이 가장 흔합니다.
List<Order> orders =
[
new("하늘", "도서", 18_000m),
new("바다", "전자", 320_000m),
new("하늘", "전자", 45_000m),
new("구름", "도서", 22_000m),
];
var bigOrders = orders
.Where(o => o.Amount >= 20_000m)
.OrderByDescending(o => o.Amount)
.Select(o => $"{o.Customer}: {o.Amount:N0}원")
.ToList();
var byCategory = orders
.GroupBy(o => o.Category)
.Select(g => new { Category = g.Key, Total = g.Sum(o => o.Amount), Count = g.Count() });
foreach (var row in byCategory)
Console.WriteLine($"{row.Category}: {row.Count}건, {row.Total:N0}원");
bool anyExpensive = orders.Any(o => o.Amount > 100_000m);
var first = orders.FirstOrDefault(o => o.Customer == "구름");
record Order(string Customer, string Category, decimal Amount);최상위 문이 있는 파일에서는 이처럼 타입 선언을 문장들 뒤에 둡니다. SQL을 닮은 쿼리 구문으로도 같은 일을 할 수 있으며, 컴파일러가 메서드 구문으로 바꿔 줍니다.
var query =
from o in orders
where o.Category == "도서"
orderby o.Amount
select o.Customer;LINQ의 중요한 성질은 지연 실행입니다. Where나 Select는 질의를 정의할 뿐이고, foreach로 열거하거나 ToList, Count, First처럼 결과가 필요한 메서드를 부를 때 실제로 실행됩니다. 같은 질의를 여러 번 열거하면 매번 다시 계산되므로, 결과를 재사용할 때는 ToList()로 한 번 구체화해 둡니다.
파일 · 네트워크 · 데이터베이스 작업은 기다리는 시간이 대부분입니다. async 메서드는 await 지점에서 작업이 끝나기를 기다리는 동안 스레드를 돌려주고, 작업이 끝나면 그 다음 줄부터 이어서 실행합니다. 비동기 메서드는 결과가 없으면 Task를, 결과가 있으면 Task<T>를 돌려줍니다.
using var http = new HttpClient();
static async Task<int> GetLengthAsync(HttpClient client, string url, CancellationToken token)
{
string body = await client.GetStringAsync(url, token);
return body.Length;
}
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
// 하나씩 기다리기
int length = await GetLengthAsync(http, "https://learn.microsoft.com", cts.Token);
Console.WriteLine($"길이: {length}");
// 여러 작업을 동시에 시작하고 모두 기다리기
string[] urls = ["https://dotnet.microsoft.com", "https://www.nuget.org"];
int[] lengths = await Task.WhenAll(urls.Select(u => GetLengthAsync(http, u, cts.Token)));
Console.WriteLine(string.Join(", ", lengths));
}
catch (OperationCanceledException)
{
Console.WriteLine("시간이 초과되어 취소했습니다.");
}최상위 문에서는 await를 바로 쓸 수 있습니다. 비동기 코드를 쓸 때는 다음 규칙을 지키면 대부분의 문제를 피할 수 있습니다.
Async로 끝내고, Task 또는 Task<T>를 돌려줍니다.async void는 이벤트 처리기에서만 씁니다. 예외를 호출한 쪽에서 잡을 수 없기 때문입니다..Result나 .Wait()로 비동기 작업을 막아 세우지 말고 끝까지 await합니다. 교착 상태의 흔한 원인입니다.CancellationToken을 받아 취소할 수 있게 합니다.Task.WhenAll로 동시에 진행합니다.HttpClient는 요청마다 새로 만들지 않고 재사용하는 것이 권장됩니다. 웹 앱에서는 의존성 주입으로 IHttpClientFactory를 받아 쓰는 방법이 일반적입니다.
Func와 Action 델리게이트에 람다를 담아 함수를 값처럼 넘기고, 람다는 바깥 변수를 붙잡을 수 있습니다.Where, Select, GroupBy, OrderBy 같은 연산을 이어 붙여 데이터를 선언적으로 다루며, 결과가 필요할 때 실행됩니다.async/await와 Task로 기다리는 동안 스레드를 막지 않는 코드를 쓰고, CancellationToken으로 취소를 지원합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.