공부/C#

이것이 C#이다 Chapter 15 LINQ

bokob 2024. 3. 16. 23:29

15.1 LINQ

LINQ(Language INtegrated Query)

  • C#에 통합된 데이터 질의 기능

기본적으로 Query는 다음 내용을 포함

  • from: 어떤 데이터 집합에서 찾을 것인가?
  • where: 어떤 값의 데이터를 찾을 것인가?
  • select: 어떤 항목을 추출할 것인가?
class Profile
{
    public string Name {get; set;}
    public int Height {get; set;}
}

Profile[] arrProfile = {
                            new Profile(){Name="엄준식1", Height=170},
                            new Profile(){Name="엄준식2", Height=160},
                            new Profile(){Name="엄준식3", Height=190},
                            new Profile(){Name="엄준식4", Height=180},
                       };

// Height < 175 인 객체 찾기
var profiles = from profile in arrProfile // arrProfile 안에 있는 각 데이터로부터
               where profile.Height < 175 // Height가 175 미만인 객체만 골라
               orderby profile.Height // 키 순으로 정렬하여
               select profile; // profile 객체 추출

foreach(var profile in profiles)
    Console.WriteLine($"{profile.Name}, {profile.Height}");

 

15.2 LINQ의 기본

15.2.1 from

from

  • 모든 LINQ 쿼리식(Query Expression)은 반드시 from 절로 시작
  • 쿼리식의 대상이 될 데이터 원본(Data Source)과 데이터 원본 안에 들어 있는 각 요소 데이터를 나타내는 범위 변수(Range Variable)를 from 절에서 지정해줘야 함
    • from 데이터 원본은 IEnumerable<T> 인터페이스를 상속하는 형식이어야 한다. ex) 배열, 컬렉션 객체
    • 범위 변수는 쿼리 변수(Query Variable)라고도 하는데, foreach 문의 반복 변수를 생각하면 쉽다. ex) foreach(int x in arr)에서 x같은 것
int[] numbers = {1,2,3,4};

var result = from n in numbers // n: 범위 변수, numbers: 데이터 원본
             where n % 2 == 0
             orderby n
             select n;

 

15.2.2 where

where

  • from 절이 데이터 원본으로부터 뽑아낸 범위 변수가 가져야 하는 조건을 where 연산자에 인수로 입력하면 LINQ는 해당 조건에 부합하는 데이터만을 걸러냄
int[] numbers = {1,2,3,4};

var result = from n in numbers
             where n % 2 == 0 // 짝수인 수
             orderby n
             select n;

 

15.2.3 orderby

orderby

  • 데이터 정렬을 수행하는 연산자
  • orderby 연산제 정렬의 기준이 될 항목을 인수로 입력해줌
  • ascending: 오름차순, descending: 내림차순
int[] numbers = {1,2,3,4};

var result = from n in numbers
             where n % 2 == 0
             orderby n // 정렬이 기준이 될 항목, 기본적으로 오름차순 정렬
             select n;
             
var result2 = from n in numbers
             where n % 2 == 0
             orderby n ascending // 명시적인 방법, 내림차순일 때는 descending
             select n;

 

15.2.4 select

select

  • 최종 결과를 추출하는 쿼리식의 마침표 같은 존재
  • LINQ의 질의 결과는 IEnumerable<T>로 변환되는데, 이때 형식 매개변수 T는 select 문에 의해 결정됨. 
int[] numbers = {1,2,3,4};

// result는 IEnumerable 형식으로 컴파일 된다.
var result = from n in numbers 
             where n % 2 == 0
             orderby n
             select n; // 최종 결과
  • 무명 형식을 이용해서 새로운 형식을 즉석에서 만들 수 있음
var profiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select new {Name = profile.Name, InchHeight = profile.Height * 0.393}; // 새로운 형식 생성

 

15.3 여러 개의 데이터 원본에 질의하기

여러 개의 데이터 원본에 접근하는 쿼리식 만들기

LINQ 쿼리식은 데이터 원본에 접근하기 위해 from 절 사용하기 때문에 여러 개의 데이터 원본에 접근하려면 from 문을 중첩해서 사용한다.

class Class
{
    public string Name {get; set;}
    public int[] Score {get; set;}
}

class[] arrClass =
{
    new Class(){Name="엄준식1", Score = new int[]{0,1,2}},
    new Class(){Name="엄준식2", Score = new int[]{3,4,5}},
    new Class(){Name="엄준식", Score = new int[]{6,7, 8}}
};

var classes = from c in arrClass    // 첫 번째 데이터 원본
                  from s in c.Score // 두 번째 데이터 원본
                  where s < 5
              select new {c.Name, Lowest = s}; // 무명 형식 선언

위 예제를 보면, arrClass 객체에 from 절로 접근해서 범위 변수 c를 뽑고, 다시 c 객체의 Score 필드에 From 절로 접근해서 새로운 범위 변수 s를 뽑았다.

이 범위 변수 s는 개별 점수를 나타내고, where 절을 이용해 조건을 적용한 다음, 무명 형식을 선언했다.

 

15.4 group by로 데이터 분류하기

group by

  • 분류 기준에 따라 데이터를 그룹화하는 것
group A by B into C

A에는 from 절에서 뽑아낸 변수를, B에는 분류 기준을, C에는 그룹 변수를 위치시킨다.

 

Profile[] arrProfile =
{
    new Profile(){Name="엄준식1", Height=170},
    new Profile(){Name="엄준식2", Height=160},
    new Profile(){Name="엄준식3", Height=190},
    new Profile(){Name="엄준식4", Height=180},
};

var listProfile = from profile in arrProfile
                      group profile by profile.Height < 175 into g
                      select new {GroupKey = g.key, Profiles = g};

foreach(var Group in listProfile) // Group은 IGrouping<T> 형식
{
    Console.WriteLine(Group.GroupKey);
    
    foreach(var profile in Group.Profiles)
    {
        Console.WriteLine(profile.Height);
    }
}

위 예제를 보면, 그룹 변수 g에는 Height 값이 175 미만인 객체의 컬렉션, 175 이상인 객체의 컬렉션이 입력되고, select 문이 추출하는 새로운 무명 형식은 컬렉션의 컬렉션이 된다. 각각의 컬렉션은 IGrouping<T> 형식이다.

그리고 이 무명 형식의 Profiles 필드는 바로 이 그룹 변수 g를 담게 된다.

15.5 두 데이터 원본을 연결하는 join

join

  • 두 데이터 원본에서 특정 필드의 값을 비교하여 일치하는 데이터끼리 연결하는 연산
  • LINQ에는 내부조인, 외부조인, 왼쪽 조인이 존재

15.5.1 내부 조인

내부 조인(inner join)

교집합과 비슷, 두 데이터 원본 사이에서 첫 번째 원본 데이터를 기준으로 일치하는 데이터들만 연결한 후 반환. 즉, 첫 번째 데이터 원본을 기준으로 두 번째 데이터 원본의 특징 필드를 비교해서 일치하는 데이터를 반환.

from a in A
join b in B on a.XXXX equals b.YYYY

기준 데이터 a는 from 절에서 뽑아낸 범위 변수이고, 연결 대상 데이터 b는 join 절에서 뽑아낸 변수다.

join 절의 on 키워드는 조인 조건을 수반한다. 이때 on 절의 조인 조건은 '동등(Equality)'만 허용된다.

 

class Profile
{
    public string Name {get; set;}
    public int Height {get; set;}
}

class Menu
{
    public string food {get; set;}
    public string Cook {get; set;}
}

// ...

var listProfile =
    from profile in arrProfile // arrProfile은 Profile 배열
    join menu in arrMenu on profile.Name equals menu.Cook // arrMenu는 Menu 배열
    select new
    {
        Name = profile.Name,
        Food = menu.Food,
        Height = profile.Height
    };

 

15.5.2 외부 조인

외부 조인(outer join)

조인 결과에 기준이 되는 데이터 원본이 모두 포함

연결할 데이터 원본에 기준 데이터 원본의 데이터와 일치하는 데이터가 없다면 그 부분은 빈 값으로 결과를 채운다.

 


왼쪽 조인, 오른쪽 조인, 완전 외부 조인

LINQ는 원래 DBMS에서 사용하던 SQL(Structured Query Language)을 본떠 프로그래밍 언어 안에 통합한 것이다.

SQL에는 왼쪽 조인(left join), 오른쪽 조인(right join), 완전 외부 조인(full outer join)이 있다.

왼쪽 조인은 왼쪽 데이터 원본을 기준으로, 오른쪽 조인은 오른쪽 데이터 원본을 기준으로, 완전 외부 조인은 왼쪽과 오른쪽 데이터 원본 모두를 기준으로 한다.

LINQ는 왼쪽 조인만 지원한다.


 

class Profile
{
    public string Name {get; set;}
    public int Height {get; set;}
}

class Menu
{
    public string food {get; set;}
    public string Cook {get; set;}
}

// ...

var listProfile =
    from profile in arrProfile // arrProfile은 Profile 배열
    join menu in arrMenu on profile.Name equals menu.Cook into pm // arrMenu는 Menu 배열
    from menu in pm.DefaultIfEmpty(new Menu(){Food="없는 음식"}) // pm은 profile 객체에 대해 menu 객체를 그룹으로 묶은 결과
    select new
    {
        Name = profile.Name,
        Food = menu.Food,
        Height = profile.Height
    };

join 절을 이용해서 조인을 수행한 후 그 결과를 임시 컬렉션(위 코드에서 pm)에 저장하고, 이 임시 컬렉션에 대해 DefaultIfEmpty 연산을 수행해서 비어 있는 조인 결과에 빈 값을 채워넣고 있다.

DefaultIfEmpty 연산을 거친 임시 컬렉션에서 from 절을 통해 범위 변수를 뽑아내고, 이 범위 변수와 기준 데이터 원본에서 뽑아낸 범위 변수를 이용해서 결과를 추출한다.

 

15.6 LINQ의 비밀과 LINQ 표준 연산자

LINQ는 .NET 언어 중에서도 C#과 VB에서만 사용 가능하다. (책 집필 시점)

마이크로소프트는 LINQ 쿼리식이 실행될 수 있도록 CLR을 개선하고, LINQ 쿼리식을 CLR이 이해할 수 있는 코드로 번역해주도록 C# 컴파일러와 VB 컴파일러를 업그레이드했다.

컴파일러가 LINQ를 CLR이 이해하는 코드로 만드는 방법은 LINQ 쿼리식을 분석해서 일반적인 메소드 호출 코드로 만들어내는 것이다.

var profiles = from profile in arrProfile
               where profile.Height < 175
               orderby profile.Height
               select new {Name = profile.Name, InchHeight = profile.Height * 0.393};

// C# 컴파일러가 다음과 같은 코드로 번역함            
var profiles = arrProfile
               .Where(profile => profile.Height < 175)
               .Orderby(profile => profile.Height)
               .Select(profile =>
                   new 
                   {
                       Name = profile.Name, 
                       InchHeight = profile.Height * 0.393
                   });

Where(), OrderBy() 등의 메소드 호출 코드를 사용하려 한다면 System.Linq 네임스페이스가 필요하다.

왜냐하면 이 메소드들은 System.Linq에 정의되어 있는 IEnumerable<T>의 확장 메소드이기 때문이다.

 

MSDN의 LINQ 표준 연산자들을 보면, 표준 연산자의 수와 C#이 지원하는 쿼리식 문법의 수의 차이가 있다.

종류  메소드 이름 설명 C# 쿼리식 문법
정렬 OrderBy 오름차순으로 값 정렬 orderby
OrderByDescending 내림차순으로 값 정렬 orderby ... descending
ThenBy 오름차순으로 2차 정렬 수행 orderby ..., ...
ThenByDescending 내림차순으로 2차 정렬 수행 orderby ..., ... descending
Reverse 컬렉션 요소의 순서를 거꾸로 뒤집음  
집합 Distinct 중복값 제거  
Except 두 컬렉션 사이의 차집합 반환  
Intersect 두 컬렉션 사이의 교집합 반환  
Union 두 컬렉션 사이의 합집합 반환  
필터링 OfType 메소드의 형식 매개변수로 형식 변환이 가능한 값들만 추출  
Where 필터링할 조건을 평가하는 함수를 통과하는 값들만 추출  
수량 연산 All 모든 요소가 임의의 조건을 모두 만족하면 true, 아니면 false  
Any 모든 요소 중 하나의 요소라도 임의의 조건을 만족하면 true, 아니면 false  
Contains 명시한 요소가 포함되어 있으면 true, 아니면 false  
데이터 추출 Select 값을 추출하여 시퀀스를 만듦 select
SelectMany 여러 개의 데이터 원본으로부터 값을 추출하여 하나의 시퀀스를 만듦.
여러 개의 from 절 사용
 
데이터 분할 Skip 시퀀스에서 지정한 위치가지 요소를 건너뜀  
SkipWhile 입력된 조건 함수를 만족시키는 요소들을 건너뜀  
Take 시퀀스에서 지정한 요소까지 요소들을 취함  
TakeWhile 입력된 조건 함수를 만족시키는 요소들을 취함  
데이터 결합 Join 공통 특성을 가진 서로 다른 두 개의 데이터 소스의 객체를 연결한다.
공통 특성을 키(Key)로 삼아, 키가 일치하는 두 객체를 쌍으로 추출한다.
join ... in ... on ... equals ...
GroupJoin 기본적으로 Join 연산자와 같은 일을 하되, 조인 결과를 그룹으로 만들어 넣는다.  
데이터 그룹화 GroupBy 공통 특성을 공유하는 요소들을 각 그룹으로 묶는다.
각 그룹은 IGrouping<TKey, TElement> 객체로 표현됨
group ... by 또는 group ... by ... into ...
ToLookup 키(Key) 선택 함수를 이용하여 골라낸 요소들을 Lookup<TKey, TElement> 형식의 객체에 삽입한다(이 형식은 하나의 키에 여러 개의 객체를 대응시킬 때 사용하는 컬렉션이다).  
생성 DefaultIfEmpty 빈 컬렉션을 기본값이 할당된 싱글턴 컬렉션으로 바꾼다.
기본값이 할당된 컬렉션은 참조용으로만 사용할 것이니 여러 개의 인스턴스가 필요 없고, 싱글턴을 이용하면 메모리 낭비를 줄일 수 있다.
 
Empty 비어 있는 컬렉션 반환  
Range 일정 점위의 숫자 시퀀스를 담고 있는 컬렉션 생성  
Repeat 같은 값이 반복되는 컬렉션 생성  
동등 여부 평가 SequenceEqual 두 시퀀스가 서로 일치하는지를 평가  
요소 접근 ElementAt 컬렉션으로부터 임의의 인덱스에 존재하는 요소 반환  
요소 접근 ElementAtOrDefault 컬렉션으로부터 임의의 인덱스에 존재하는 요소를 반한하되, 인덱스가 컬렉션의 범위를 벗어날 때 기본값 반환  
First 컬렉션의 첫 번째 요소를 반환한다.
조건식이 매개변수로 입력되는 경우 이 조건을 만족시키는 첫 번째 요소 반환
 
FirstOrDefault First 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본값을 반환  
Last 컬렉션의 마지막 요소를 반환한다.
조건식이 매개변수로 입력되는 경우 이 조건을 만족시키는 마지막 요소를 반환
 
LastOrDefault Last 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 기본값 반환  
Single 컬렉션의 유일한 요소를 반환한다.
조건식이 매개변수로 입력되는 경우 이 조건을 만족시키는 유일한 요소를 반환
 
SingleOrDefault Single 연산자와 같은 기능을 하되, 반환할 값이 없거나 유일한 값이 아닌 경우 주어진 기본값을 반환  
형식 변환 AsEnumerable 매개변수를 IEnumerable<T>로 형식 변환하여 반환  
AsQueryable (일반화) IEnumerable 객체를
(일반화) IQueryable 형식으로 변환
 
Cast 컬렉션의 요소들을 특정 형식으로 변환 범위 변수를 선언할 때 명시적으로 형식을 저장하면 된다.
ex) from Profile profile in arrProfile
OfType 특정 형식으로 형식 변환할 수 있는 값만 걸러낸다.  
ToArray 컬렉션을 배열로 변환한다.
이 메소드는 강제로 쿼리를 실행한다.
 
ToDictionary 키 선택 함수에 근거해서 컬렉션의 요소를 Dictionary<TKey, TValue>에 삽입한다.
이 메소드는 강제로 쿼리를 실행한다.
 
ToList 컬렉션을 List<T> 형식으로 변환한다.
이 메소드는 강제로 쿼리를 실행한다.
 
ToLookup 키 선택 함수에 근거해서 컬렉션의 요소를 Lookup<TKey, TElement>에 삽입한다.
이 메소드는 강제로 쿼리를 실행한다.
 
연결 Concat 두 시퀀스를 하나의 시퀀스로 연결  
집계 Aggregate 컬렉션의 각 값에 대해 사용자가 정의한 집계 연산 수행  
Average 컬렉션의 각 값에 대한 평균 계산  
Count 컬렉션에서 조건에 부합하는 요소의 개수를 센다.  
LongCount Count와 동일한 기능을 하지만, 매우 큰 컬렉션을 대상으로 한다.  
Max 컬렉션에서 가장 큰 값 반환  
Min 컬렉션에서 가장 작은 값 반환  
Sum 컬렉션 내 값의 합 계산  

표에 있는 53개의 표준 LINQ 연산 메소드 중에 C#의 쿼리식에서 지원하는 것은 11개이다.

11개만으로 대부분의 데이터 처리가 가능하지만, 나머지 42개를 모두 활용할 수 있다면 편해질 것이다.

 

다음은 LINQ 쿼리식과 메소드를 함께 사용하는 예제다.

class Profile
{
    public string Name {get; set;}
    public int Height {get; set;}
}

Profile[] arrProfile = {
                            new Profile(){Name="엄준식1", Height=170},
                            new Profile(){Name="엄준식2", Height=160},
                            new Profile(){Name="엄준식3", Height=190},
                            new Profile(){Name="엄준식4", Height=180},
                       };

var profiles = from profile in arrProfile
               where profile.Height < 180
               select profile;

double Average = profiles.Average(profile => profile.Height);
Console.WriteLine(Average);

/* 한 문장으로 줄일 수 있다.
var profiles = (from profile in arrProfile
               where profile.Height < 180
               select profile).Average(profile => profile.Height);
*/