LINQ Fluent Syntax
개요
- Fluent Syntax는 LINQ의 가장 기본적이고 유연한 쿼리 작성 방식임
- 확장 메서드와 람다 식을 사용하여 쿼리를 작성함
- 쿼리 연산자를 체이닝하여 복잡한 쿼리를 구성할 수 있음
쿼리 연산자 체이닝
- 체이닝은 여러 쿼리 연산자를 연결하여 복잡한 쿼리를 만드는 방법임
- 데이터는 왼쪽에서 오른쪽으로 흐르며 순차적으로 처리됨
- 실제 변수가 저장되는게 아니라 해당 구성의 ‘재료’들을 가지고 있는것이고, 이것을 얻기위해 계속 반복을 돌리는 것이다.
using System;
using System.Collections.Generic;
using System.Linq;
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> query = names
.Where(n => n.Contains("a")) // "a"를 포함하는 이름 필터링
.OrderBy(n => n.Length) // 길이순으로 정렬
.Select(n => n.ToUpper()); // 대문자로 변환
foreach (string name in query)
Console.WriteLine(name);
// 실행 결과:
// JAY
// MARY
// HARRY
쿼리 연산자의 구현과 체이닝
- Where, OrderBy, Select는 System.Linq 네임스페이스의 확장 메서드임
- 람다 식의 변수 n은 각각의 람다 식에서 독립적으로 스코프가 지정됨
- 쿼리 연산자는 이터레이터를 사용하여 간단하게 구현할 수 있음:
// Select 연산자의 간단한 구현 예제
//TSource : 입력용 일반화 인자, TResult : 출력용 인반화 인자.
public static IEnumerable<TResult> MySelect<TSource,TResult>
(this IEnumerable<TSource> source, Func<TSource,TResult> selector) //TResult : 반환형
{
foreach (TSource element in source)
yield return selector(element);
}
데코레이터 시퀀스와 실행 원리
- 데코레이터 시퀀스는 입력 시퀀스를 감싸는 래퍼(wrapper) 객체임
- 일반적인 컬렉션과 달리 데코레이터는 자체 데이터 저장소를 가지지 않음
- 대신 런타임에 제공되는 입력 시퀀스에 영구적으로 의존함
데코레이터 시퀀스 예제
// 다음과 같은 쿼리가 있다고 가정:
IEnumerable<int> lessThanTen = new int[] { 5, 12, 3 }.Where(n => n < 10);
- 위 쿼리의 실행 구조:
- Where는 데코레이터 객체만 생성함
- lessThanTen은 int 배열을 참조한다.
- 가비지 컬렉션이 동작한다.
- 데코레이터는 입력 배열과 람다 식(n => n < 10)을 참조로 저장
- lessThanTen을 열거할 때만 실제로 필터링이 수행됨
데코레이터 체이닝
// 여러 연산자를 체이닝한 쿼리:
IEnumerable<int> query = new int[] { 5, 12, 3 }
.Where(n => n < 10)
.OrderBy(n => n)
.Select(n => n * 10);
- 각 연산자는 새로운 데코레이터를 만들어 이전 시퀀스를 감쌈
- 데코레이터 : 실행의 결과를 다음 연산이 감싸는 식으로 진행된다.
- 실행 순서:
- Where 데코레이터가 원본 배열을 감쌈
- OrderBy 데코레이터가 Where의 결과를 감쌈
- Select 데코레이터가 OrderBy의 결과를 감쌈
- 최종적으로 러시안 인형처럼 중첩된 데코레이터 체인이 형성됨
지연 실행(Deferred Execution)의 장점
- 쿼리 구성과 실행이 분리됨
- 쿼리를 여러 단계로 구성할 수 있음
- 데이터베이스 쿼리 등 원격 데이터 소스 지원이 가능해짐
- 필요할 때만 실제 데이터를 처리하므로 메모리 효율적임
- 주의해야 할 점: 쿼리 실행 전에 원본 데이터가 변경되면, 쿼리 결과에 영향을 줄 수 있음. 쿼리 결과를 즉시 확정하려면 ToList() 또는 ToArray() 같은 즉시 실행 메서드 사용이 필요.
- 원본 데이터가 변경되면 쿼리 실행 결과도 바뀌게된다.
- 안바뀌게 하려면? : List나 Array처럼 즉시 실행 메서드가 필요하다.
// 단계별 쿼리 구성 예제:
var numbers = new List<int>() { 1, 2 };
IEnumerable<int> query = numbers.Select(n => n * 10); // 쿼리 구성
numbers.Add(3); // 원본 데이터 수정
foreach (int n in query) // 이 시점에 실제 실행
Console.Write(n + "|"); // 출력: 10|20|30|
- query는 10, 20이 저장되는게 아니라 1, 2와 *10을 하는 메서드가 같이 저장이 되는 것이다.
- 이후, 인자를 호출할때 메서드가 실행되어 호출되는 것이다.
- 같은 쿼리를 단계별로 작성할 수도 있음:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> filtered = names.Where(n => n.Contains("a"));
IEnumerable<string> sorted = filtered.OrderBy(n => n.Length);
IEnumerable<string> finalQuery = sorted.Select(n => n.ToUpper());
foreach (string name in filtered)
Console.Write (name + "|"); // Harry|Mary|Jay|
Console.WriteLine();
foreach (string name in sorted)
Console.Write (name + "|"); // Jay|Mary|Harry|
Console.WriteLine();
foreach (string name in finalQuery)
Console.Write (name + "|"); // JAY|MARY|HARRY|
확장 메서드의 중요성
- 확장 메서드 구문을 사용하면 쿼리가 자연스럽게 왼쪽에서 오른쪽으로 흐름
- 일반적인 정적 메서드로도 동일한 쿼리를 작성할 수 있으나 가독성이 떨어짐:
// 확장 메서드를 사용하지 않은 경우:
IEnumerable<string> query =
Enumerable.Select(
Enumerable.OrderBy(
Enumerable.Where(
names, n => n.Contains("a")
), n => n.Length
), n => n.ToUpper()
);
람다 식 작성
- 람다 식은 쿼리 연산자에 로직을 제공하는 방법임
- 각 연산자마다 람다 식의 목적이 다름:
- Where: 요소를 포함할지 결정하는 조건 (bool 반환)
- OrderBy: 정렬 키 지정
- 값을 받으면 그 값을 기준으로 정렬되는 것이다.
- Select: 각 요소의 변환 방법 지정
- 원하는 결과물의 데이터형을 람다식에 연결해서 사용한다.
람다 식과 타입 매개변수
- 표준 쿼리 연산자는 다음과 같은 타입 매개변수 명명 규칙을 사용함:
- TSource: 입력 시퀀스의 요소 타입
- TResult: 출력 시퀀스의 요소 타입 (TSource와 다른 경우)
- TKey: 정렬, 그룹화 또는 조인에 사용되는 키의 타입
Select 연산자의 타입 추론 예제
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" }; //TSource
// 문자열을 정수로 변환하는 예제
IEnumerable<int> query = names.Select(n => n.Length); //TResult
foreach (int length in query)
Console.Write(length + "|"); // 3|4|5|4|3|
- TSource는 입력 시퀀스(names)로부터 string으로 결정됨
- TResult는 람다 식의 반환값(n.Length)으로부터 int로 추론됨
Where 연산자의 특징
- Where는 입력과 출력의 요소 타입이 동일함
- 요소를 필터링만 하고 변환하지 않기 때문임:
public static IEnumerable<TSource> Where<TSource>
(this IEnumerable<TSource> source, Func<TSource,bool> predicate)
OrderBy 연산자의 특징
- OrderBy는 정렬 키의 타입을 별도로 지정할 수 있음:
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
IEnumerable<string> sortedByLength, sortedAlphabetically;
sortedByLength = names.OrderBy(n => n.Length); // int 키 / int 정렬
sortedAlphabetically = names.OrderBy(n => n); // string 키 / string 정렬
람다 식과 Func 시그니처
- 표준 쿼리 연산자는 제네릭 Func 대리자를 사용함
- Func의 타입 인자는 람다 식과 동일한 순서로 나타남:
- Func<TSource,bool>는 TSource => bool 람다 식과 일치
- Func<TSource,TResult>는 TSource => TResult 람다 식과 일치
- 컴파일러는 람다 식의 반환값으로부터 TResult 타입을 자동으로 추론함
입력 순서
- 입력 시퀀스의 입력 순서가 LINQ에서 중요함
- Take, Skip, Reverse와 같은 연산자는 이 순서에 의존함:
- 위의 3개는 LINQ의 연산자다.
- Take(count - 갯수) : 갯수만큼 반환하는 것이다.
- Skip(갯수) : 갯수만큼 건너뛰어 반환한다.
- Reverse() : 역순정렬
int[] numbers = { 10, 9, 8, 7, 6 };
// 처음 3개 요소 선택
IEnumerable<int> firstThree = numbers.Take(3); // { 10, 9, 8 }
// 처음 3개를 제외한 나머지 선택
IEnumerable<int> lastTwo = numbers.Skip(3); // { 7, 6 }
// 순서 뒤집기
IEnumerable<int> reversed = numbers.Reverse(); // { 6, 7, 8, 9, 10 }
기타 연산자
- 시퀀스를 반환하지 않는 연산자들도 있음
- 요소 연산자: 입력 시퀀스에서 단일 요소를 추출함
- First() : 첫번쨰 요소 반환
- Last() : 마지막 요소 반환
- ElementAt(인덱스) : 인덱스의 요소 반환
- 이렇게 LINQ를 쓰지 않는게 좋다.
- 그냥 배열의 인덱스로 써서 호출해라
- 인덱서가 없는 경우에만 사용하는 것이다.
int[] numbers = { 10, 9, 8, 7, 6 };
int firstNumber = numbers.First(); // 10
int lastNumber = numbers.Last(); // 6
int secondNumber = numbers.ElementAt(1); // 9
- 집계 연산자: 보통 숫자 형식의 스칼라 값을 반환함:
- 반환 데이터형이 IEnumerable이라면 그에 맞는 결과가 반환된다.
int count = numbers.Count(); // 5
int min = numbers.Min(); // 6
- 한정자: bool 값을 반환함:
- 컬렉션에 따라서 없는 컬렉션도 있으니 그때 사용하면 좋은 연산자들이다.
- Any : 비어있지 않으면 true
- Any(bool 반환 람다식) : 람다식이 true라면 true 반환
bool hasTheNumberNine = numbers.Contains(9); // true
bool hasMoreThanZeroElements = numbers.Any(); // true
bool hasAnOddElement = numbers.Any(n => n % 2 != 0); // true
- 두 개의 입력 시퀀스를 받는 연산자들도 있음:
- Concat : 연결 함수 - 중복 허용
- Union : 합치는 함수 - 중복 제거
int[] seq1 = { 1, 2, 3 };
int[] seq2 = { 3, 4, 5 };
// 두 시퀀스 연결
IEnumerable<int> concat = seq1.Concat(seq2); // { 1, 2, 3, 3, 4, 5 }
// 중복 제거하며 연결
IEnumerable<int> union = seq1.Union(seq2); // { 1, 2, 3, 4, 5 }