공부/C#

LINQ, 쿼리 연산자, 람다 식

월러비 2025. 9. 9. 18:35

LINQ

  • 확장 메서드의 집합이다.
    • 매개변수로 this가 들어가는 메서드다.
  • CSV처럼 데이터 테이블의 정보를 LINQ 함수를 이용해서 가져오는것으로 사용한다.
  • 게임개발에서는 실시간으로 사용은 무리지만, 로딩시간이나 비 실시간에 한정으로 사용해도 괜찮다.

LINQ의 개념

  • LINQ (Language Integrated Query) 는 컬렉션에 대한 구조화된 타입 안전 쿼리를 작성하기 위한 C# 언어 및 .NET 런타임 기능임.
  • 배열, 리스트, XML DOM 등 IEnumerable<T>를 구현하는 모든 컬렉션 쿼리 가능.
    • foreach의 인자인 컬렉션에 들어가는것처럼 사용할 수 있는것이다.
  • 컴파일 타임의 타입 검사와 동적 쿼리 구성의 장점을 제공함.
  • LINQ의 장점:
    • 코드 가독성 향상: 쿼리를 간결하고 직관적으로 표현 가능.
    • 생산성 향상: 데이터 쿼리 코드를 빠르고 쉽게 작성 가능.
      • 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 연산자의 시그니처

  • 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과 달리 변수를 선언하기 전에 사용할 수 없으며, 데이터는 왼쪽에서 오른쪽으로 논리적으로 흐름.