LINQ
- 확장 메서드의 집합이다.
- CSV처럼 데이터 테이블의 정보를 LINQ 함수를 이용해서 가져오는것으로 사용한다.
- 게임개발에서는 실시간으로 사용은 무리지만, 로딩시간이나 비 실시간에 한정으로 사용해도 괜찮다.
LINQ의 개념
- LINQ (Language Integrated Query) 는 컬렉션에 대한 구조화된 타입 안전 쿼리를 작성하기 위한 C# 언어 및 .NET 런타임 기능임.
- 배열, 리스트, XML DOM 등 IEnumerable<T>를 구현하는 모든 컬렉션 쿼리 가능.
- foreach의 인자인 컬렉션에 들어가는것처럼 사용할 수 있는것이다.
- 컴파일 타임의 타입 검사와 동적 쿼리 구성의 장점을 제공함.
- LINQ의 장점:
- 코드 가독성 향상: 쿼리를 간결하고 직관적으로 표현 가능.
- 생산성 향상: 데이터 쿼리 코드를 빠르고 쉽게 작성 가능.
- 컴파일 타임 타입 검사: 컴파일 시점에 쿼리 오류를 검출하여 런타임 오류 방지 가능.
기본 단위
- 시퀀스 (Sequence): IEnumerable<T> 인터페이스를 구현하는 객체임.
- 열거 가능한 컬렉션이다.
- foreach로 순회가 가능한 객체다.
- LINQ는 확장 메서드로 구현되어있다.
- 즉, 확장메서드의 반환이 확장메서드가 아니라 인스턴스 메서드를 반환하는것처럼, 시퀀스도 자신을 반환할때 인스턴스 메서드를 반환하는 것이다.
- 요소 (Element): 시퀀스의 각 항목임.
- 로컬 시퀀스 (Local sequence):
- 메모리에 이미 로드되어 있는 데이터 컬렉션을 의미함.
- 배열, List<T>, Dictionary<K,V> 등 로컬 컬렉션에 대한 쿼리를 LINQ to Objects 쿼리라고 함.
- 로컬 시퀀스 쿼리는 즉시 실행 가능하며 네트워크 지연이 없음.
using System.LINQ;
// 로컬 시퀀스의 예
string[] namesArray = { "Tom", "Dick", "Harry" }; // 배열
List<string> namesList = new List<string> { "Tom", "Dick", "Harry" }; // 리스트
Dictionary<int, string> namesDict = new Dictionary<int, string> // 딕셔너리
{
{ 1, "Tom" },
{ 2, "Dick" },
{ 3, "Harry" }
};
// 각각의 컬렉션에 대한 LINQ 쿼리 (플루언트 구문)
var arrayQuery = namesArray.Where(n => n.Length > 3); // 길이가 3보다 큰 요소만 선택
var listQuery = namesList.Where(n => n.Length > 3); // 길이가 3보다 큰 요소만 선택
var dictQuery = namesDict.Where(kvp => kvp.Value.Length > 3); // 값이 3보다 큰 요소만 선택
// 결과 출력
foreach (var name in arrayQuery)
Console.WriteLine(name); // Dick, Harry
foreach (var name in listQuery)
Console.WriteLine(name); // Dick, Harry
foreach (var kvp in dictQuery)
Console.WriteLine($"{kvp.Key}: {kvp.Value}"); // 2: Dick, 3: Harry
쿼리 연산자 (Query Operator)
- 쿼리 연산자는 시퀀스를 변환하는 메서드임.
- 일반적으로 입력 시퀀스를 받아 변환된 출력 시퀀스를 반환함.
- System.Linq의 Enumerable 클래스에는 약 40개의 표준 쿼리 연산자가 정의되어 있음.
- 모든 연산자는 정적 확장 메서드로 구현됨.
using System;
using System.Collections.Generic;
using System.Linq;
string[] names = { "Tom", "Dick", "Harry" };
// System.Linq.Enumerable.Where() 직접 호출
IEnumerable<string> filteredNames = System.Linq.Enumerable.Where(
names, n => n.Length >= 4);
foreach (string n in filteredNames)
Console.WriteLine(n);
// 출력:
// Dick
// Harry
- 표준 쿼리 연산자는 확장 메서드로 구현되어 있어서 names에서 직접 Where를 호출할 수 있음. (플루언트 구문)
using System;
using System.Collections.Generic;
using System.Linq;
string[] names = { "Tom", "Dick", "Harry" };
// 확장 메서드를 사용한 Where() 호출 (플루언트 구문)
IEnumerable<string> filteredNames = names.Where(n => n.Length >= 4);
foreach (string name in filteredNames)
Console.WriteLine(name);
// 출력:
// Dick
// Harry
람다 식
- 대부분의 쿼리 연산자는 람다 식을 인자로 받음.
- 람다 식은 쿼리의 형태를 결정하는데 도움을 줌.
- Where 연산자는 람다 식이 bool 값을 반환하도록 요구함.
using System;
using System.Collections.Generic;
using System.Linq;
string[] names = { "Tom", "Dick", "Harry" };
// Where() 연산자에 람다 식 사용
IEnumerable<string> filteredNames = names.Where(n => n.Contains("a"));
foreach (string name in filteredNames)
Console.WriteLine(name); // Harry
- filteredNames를 타입 추론을 사용하여 더 간단히 작성할 수 있음.
var filteredNames = names.Where(n => n.Length >= 4);
Where 연산자의 시그니처
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source, //확장메서드 매개변수
Func<TSource, bool> predicate) //펑션 델리게이트
- TSource: 입력 시퀀스의 요소 타입
- predicate: 각 요소를 bool 값으로 매핑하는 델리게이트
쿼리 작성 방법
- C#은 쿼리를 작성하기 위한 두 가지 방법을 제공함.
- 플루언트 구문 (Fluent Syntax): 확장 메서드와 람다 식을 사용함.
- 쿼리 구문 (Query Syntax): SQL과 유사한 구문임.
- DB의 쿼리문을 흉내낸것이다.
- from절부터 select 절로 끝나는 구문이다.
- 2가지의 구문을 섞어서 쓸 수도 있다.
- 쿼리 구문과 플루언트 구문의 장단점 비교:
특징 쿼리 구문 플루언트 구문
| 가독성 |
좋음 (특히 SQL에 익숙한 경우) |
쿼리 구문보다 떨어질 수 있음 (특히 복잡한 쿼리의 경우) |
| 유연성 |
낮음 (복잡한 쿼리 표현에 한계) |
높음 (모든 LINQ 쿼리를 표현 가능) |
| 기능 |
일부 LINQ 기능만 지원 |
모든 LINQ 기능 지원 |
| 컴파일러 변환 |
플루언트 구문으로 변환됨 |
|
| 적합한 상황 |
간단하고 직관적인 쿼리 |
복잡하거나 동적인 쿼리 |
using System;
using System.Collections.Generic;
using System.Linq;
string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
// 쿼리 구문 (Query Syntax)
var querySyntaxResult = from n in names
where n.Contains("a") // "a"를 포함하는 요소 선택
orderby n // 오름차순 정렬
select n.ToUpper(); // 대문자로 변환
// 플루언트 구문 (Fluent Syntax) - 가독성 및 편의성이 좋다.
var fluentSyntaxResult = names
.Where(n => n.Contains("a")) // "a"를 포함하는 요소 선택
.OrderBy(n => n) // 오름차순 정렬
.Select(n => n.ToUpper()); // 대문자로 변환
// 결과 출력 (두 구문 모두 동일한 결과)
Console.WriteLine("쿼리 구문 결과:");
foreach (string name in querySyntaxResult)
Console.WriteLine(name); // HARRY, JAY, MARY
Console.WriteLine("\\\\n플루언트 구문 결과:");
foreach (string name in fluentSyntaxResult)
Console.WriteLine(name); // HARRY, JAY, MARY
//결과가 바뀐다.
name[0 = 'AAAA";
foreach (string name in fluentSyntaxResult)
Console.WriteLine(name); // HARRY, JAY, MARY
- 쿼리 구문의 구성:
- from 절: 범위 변수(range variable)를 선언하며, foreach 문의 반복 변수와 유사함.
- select 절: 결과값을 선택하며, 모든 쿼리 식은 select나 group으로 끝나야 함.
- 출력하는 결과값을 어떻게 반환할지 결정하는 함수다.
- 컴파일러는 쿼리 구문을 플루언트 구문으로 변환함.
- 플루언트 단점 ; 변수를 생성하기에 가비지 컬렉션의 재상이다.
// 쿼리 구문:
from n in names
where n.Contains("a")
select n;
// 컴파일러가 변환한 플루언트 구문:
names.Where(n => n.Contains("a"))
- System.Linq 네임스페이스를 임포트해야 쿼리가 컴파일됨.
- 쿼리 구문은 SQL에서 영감을 받았지만, C# 식으로 변환되어 C#의 표준 규칙을 따름.
- SQL과 달리 변수를 선언하기 전에 사용할 수 없으며, 데이터는 왼쪽에서 오른쪽으로 논리적으로 흐름.