본문 바로가기

C#/이것이 C#이다

[ 이것이 C#이다 ] Chapter 15 . LINQ

 [ 학습 흐름 ]
  1. LINQ의 기본
  2. from
  3. where
  4. order By
  5. select
  6. 여러개의 데이터 원본에 질의하기
  7. group by로 데이터 분류하기
  8. 내부 조인
  9. 외부 조인
  10. LINQ의 비밀과 LINQ 표준 연산자

[ 데이터! ]

[ LINQ ]

- LINQ는 Language INtegrated Query (통합 언어 퀴리)로 C#의 통합된 데이터 질의 기능을 말한다 .

- 데이터 질의는 데이터에 대해 물어본다는 말 . 기본적으로 질의는 다음 내용을 포함한다 .

- From : 어떤 데이터 집합에서 찾을 것인가?

- Where : 어떤 값의 데이터를 찾을 것인가?

- Select : 어떤 항목을 추출할 것인가?


[ 예시 ]

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

Profile[] arrProfile = 
{
    new Profile(){Name="정우성",Height=186},
    new Profile(){Name="김태희",Height=158},
    new Profile(){Name="고현정",Height=156}
};

- arrProfile에서 Height 프로퍼티가 175 미만인 데이터만 골라 새 컬렉션으로 추가해보자

List<Profile>profiles = new List<Profile>();
foeach(Profile profile in arrProfile)
{
	if(profile.Height < 175)
    	profiles.Add(profile);
}

profile.Sort(
	(profile1,profile2)=>
    {
    	return profile1.Height - profile2.Height;
    });

 - 다음과 같이 정렬 할 수도 있다 .

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

- LINQ를 사용하면 위와 같이 단순하게 만들 수 있다. 


[ From ]

[ from ]

- 모든 LINQ 퀴리식은 반드시 from 절로 시작한다 .

- 퀴리식의 대상이 될 데이터 원본과 데이터 원본 안에 들어 있는 각 요소 데이터를 나타내는 범위변수를 지정해야 한다 .

- from 데이터 원본은 IEnumerable<T> 인터페이스를 상속하는 형식이어야만 한다 .

- 범위 변수는 퀴리 변수라고도 한다 .


[ LINQ의 범위 변수와 foreach문의 반복변수의 차이점 ]

- foreach 문의 반복 변수는 데이터 원본으로부터 데이터를 담지만 범위변수는 실제로 데이터를 담지는 않는다

- 퀴리식 외부에서 선언된 변수에 범위 변수의 데이터를 복사하는 행위는 할 수 없다 .

- 범위 변수는 오직 LINQ 질의 안에서만 통용된다 .


[ from 절의 사용 ]

var result = from 범위변수 in 데이터 원본

- from 절은 다음과 같이 from <범위변수> in <데이터 원본>의 형식으로 사용한다 .

- 데이터 원본으로부터 범위변수를 뽑은 후 LINQ 가 제공하는 수십가지 연산자로 데이터를 가공/추출 한다 .

using System;
using System.Linq;

namespace ConsoleApp;

class MainApp
{
    static void Main(string[] args)
    {
        int[] numbers = { 9, 2, 6, 4, 5, 3, 7, 8, 1, 10 };
        var result =from number in numbers
                    where number%2==0
                    orderby number
                    select number;
        foreach (var number in result)
        {
            Console.WriteLine($"짝수 : {number}");
        }
    }
}

[ Where ]

[ where ]

- 필터 역할을 하는 연산자이다 .

- from 절이 데이터 원본으로부터 뽑아낸 범위변수가 가져야하는 조건을 where 연산자에 인수로 입력하면

LINQ는 해당 조건에 부합하는 데이터만을 걸러낸다 .

var profiles = from profile in arrProfile
                where profile < 175

[ orderby ]

[ orderby ]

- 데이터 정렬을 수행하는 연산자이다 .

- orderby 연산자는 기본적으로 오름차순으로 데이터를 정렬하지만 , 키워드로 명시적으로 표현할 수 있다 .

- ascending은 오름차순을 descending은 내림차순으로 정렬한다 .

var profiles = from profile in arrProfile
                where profile.heignt < 175 
                orderby profile.height ascending
                orderby profile.height descending

[ select ]

[ select ]

var profiles = from profile in arrProfiles
                where profile %2==0
                orderby profile.height
                select profile
                //select profile.Name

- select 절은 최종 결과를 추출하는 퀴리식의 마침표 같은 존재이다 .

- LINQ 질의의 결과는 IEnumerable<T>로 반환되는데 , 이때 형식 매개변수 T는 select 문에 의해 결정된다 .

- profile select시 IEnumerable<Profile> 형식이 , profile.Name 선택시 IEnumerable<string>형식으로 컴파일된다 .

select new {Name = profile.Name , InchHeight =profile.Height * 0.393};

- 또한 무명형식을 이용해 새로운 형식을 즉석에서 만들수도 있다 .

using System;
using System.Linq;

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

class MainApp
{
    static void Main(string[] args)
    {
        Profile[] arrProfile =
        {
            new Profile(){Name="정우성",Height=186 },
            new Profile(){Name="김태희",Height=158 },
            new Profile(){Name="고현정",Height=172 },
            new Profile(){Name="이문세",Height=178 },
            new Profile(){Name="하하",Height=171 },
        };

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

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

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

[ select ]

using System;
using System.Linq;

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

class MainApp
{
    static void Main(string[] args)
    {

        Class[] arrClass =
        {
            new Class(){ Name="연두반",Score=new int[]{ 99,80,70,24} },
            new Class(){ Name="분홍반",Score=new int[]{ 60,45,87,72} },
            new Class(){ Name="파랑반",Score=new int[]{ 92,30,85,94} },
            new Class(){ Name="노랑반",Score=new int[]{ 90,88,0,17} }
        };

        var classes = from c in arrClass
                      from s in c.Score
                      where s < 60
                      orderby s
                      select new { c.Name,Lowest = s};

        foreach(var c in classes)
            Console.WriteLine($"낙제 : {c.Name},({c.Lowest})");

    }
}

- 여러 개의 데이터 원본에 접근하려면 from 문을 중첩해서 사용하면 된다 .

- from 절의 중첩으로 낙제점을 맞은 학생의 학급과 점수를 담아낸다. 


[ groupby로 데이터 분류하기 ]

[ groupby ]

group 범위변수 by 기준 into 그룹 변수

- groupby절을 통해 분류기준에 따라 데이터를 그룹화 할 수 있다 .

using System;
using System.Linq;
using System.Text.RegularExpressions;

namespace ConsoleApp;

class MainApp
{
    class Profile
    {
        public string Name { get; set; }
        public int Height { get; set; }
    }
    static void Main(string[] args)
    {
        Profile[] arrProfile =
        {
            new Profile(){Name="정우성",Height=186 },
            new Profile(){Name="김태희",Height=158 },
            new Profile(){Name="고현정",Height=172 },
            new Profile(){Name="이문세",Height=178 },
            new Profile(){Name="하하",Height=171 },
        };

        var listProfile = from profile in arrProfile
                          orderby profile.Height
                          group profile by profile.Height < 175 into g
                          //listProfile에는 조건(175 이하)에 따라 두 그룹이 형성
                          select new {GroupKey=g.Key,Profiles=g};

        foreach(var group in listProfile)
        {
            Console.WriteLine($" - 175 미만 ? : {group.GroupKey}");
            foreach(var profile in group.Profiles)
                Console.WriteLine($">>> {profile.Name} , {profile.Height}");
        }

    }
}


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

[ join ]

- 각 데이터 원본에서 특정 필드의 값을 비교하여 일치하는 데이터끼리 연결한다 .


[ 내부 join ]

- 교집합과 같다 . 

- 내부 조인은 첫번째 데이터 원본과 두번째 데이터 원본의 특정 필드를 비교 , 일치하는 데이터를 반환한다 .

- 이때의 기준은 첫 번째 원본 데이터이다 .

- 내부 조인을 수행시 기준 데이터 원본에는 있지만 연결할 데이터 원본에는 없다면 조인 결과에 포함 안된다 .

- join 절의 on 키워드는 조인 조건을 수반하는데 이때 , 비교가 아닌 동등의 키워드 equlas만 가능하다 .

from a in A
join b in B on a.xxx equlas b.yyy

[ 외부 join ]

- 조인 기준이 되는 데이터의 원본이 모두 포함된다 .

- 외부 조인의 기준이 되는 데이터 원본의 모든 데이터를 조인결과에 반드시 포함시킨다 

- 일치값이 없다면 빈 값으로 결과를 채우게 된다 

- join 절을 이용해서 조인을 수행 , 해당 결과를 임시 컬렉션에 저장한다 .

- 임시 컬렉션에 DefaultIfEmpty연산을 수행 , 빈 조인결과에 빈 값을 채워넣는다 .

from a in A
join b in B on a.xxx equlas b.yyy into c
from b in c.DefaultIfEmpty(new B(){빈값})

[  join예시 ]

using System;
using System.Linq;
using System.Text.RegularExpressions;

namespace ConsoleApp;

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

    class Product
    {
        public string Title { get; set; }
        public string Star { get; set; }
    }
    static void Main(string[] args)
    {
        Profile[] arrProfile =
        {
            new Profile(){Name="정우성",Height=186 },
            new Profile(){Name="김태희",Height=158 },
            new Profile(){Name="고현정",Height=172 },
            new Profile(){Name="이문세",Height=178 },
            new Profile(){Name="하하",Height=171 },
        };

        Product[] arrProduct =
        {
            new Product(){ Title="비트",Star="정우성"},
            new Product(){ Title="CF 다수",Star="김태희"},
            new Product(){ Title="아이리스",Star="김태희"},
            new Product(){ Title="모래시계",Star="고현정"},
            new Product(){ Title="Solo 예찬",Star="이문세"},
        };

        var listProfile =
            from profile in arrProfile
            join product in arrProduct on profile.Name equals product.Star
            select new
            {
                Name = profile.Name,
                Work = product.Title,
                Height = profile.Height
            };

        Console.WriteLine("--- 내부 조인 결과 ---");
        foreach(var profile in listProfile)
        {
            Console.WriteLine($"이름 : {profile.Name} 키 : {profile.Height} 작품 : {profile.Work}");
        }

        listProfile = from profile in arrProfile
                      join product in arrProduct on profile.Name equals product.Star into ps
                      from product in ps.DefaultIfEmpty(new Product() { Title = "그런거 없음" })
                      select new
                      {
                          Name = profile.Name,
                          Work=product.Title,
                          Height=profile.Height,
                      };
        Console.WriteLine("--- 외부 조인 결과 ---");

        foreach (var profile in listProfile)
        {
            Console.WriteLine($"이름 : {profile.Name} 키 : {profile.Height} 작품 : {profile.Work}");
        }

    }
}

[ LINQ의 비밀과 LINQ 표준 연산자 ]

[ LINQ의 비밀 ]

- LINQ는 다른 .NET 언어에서는 사용 할 수 없다 (C# , VB만 가능)

- 마이크로소프트는 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(profiles => profiles.Height)
                     .Select(profiles =>
                     new
                     {
                         Name = profile.Name,
                         InchHeight = profile.Height * 0.393
                     });

- where은 Where()메소드 / orderBY는 OrderBy() / select는 Select() /from절의 범위변수는

각 메소드에 입력되는 람다식의 매개변수로 바뀌었다. 

- from 절의 매개변수는 IEnumerable<T>의 파생형식이어야 한다 .

- 위 예제의 배열은 IEnumerable<T>의 파생형식이며 , System.Collections.Generic의 소속이다 .

- Where(),OrderBy(),Select() 메서드는 System.Linq에 정의되어 있는 IEnumerable의 확장메소드이다 .

using System;
using System.ComponentModel.Design;
using System.Linq;
using System.Text.RegularExpressions;
using static System.Runtime.InteropServices.JavaScript.JSType;

namespace ConsoleApp;

class Profile
{
    public int Height { get; set; }
    public string Name { get; set; }
}
class MainApp
{   
    static void Main(string[] args)
    {
        Profile[] arrProfile =
        {
            new Profile(){Name="정우성",Height=186 },
            new Profile(){Name="김태희",Height=158 },
            new Profile(){Name="고현정",Height=172 },
            new Profile(){Name="이문세",Height=178 },
            new Profile(){Name="하하",Height=171 },
        };


       var profiles = arrProfile
                            .Where(profile => profile.Height < 175)
                            .OrderBy(profile => profile.Height)
                            .Select(profile =>
                            new
                            {
                                Name = profile.Name,
                                InchHeight = profile.Height * 0.393
                            });
                     
        foreach(var profile in profiles)
            Console.WriteLine($"{profile.Name} , {profile.InchHeight}");


      
    }
}

[ LINQ 표준 연산 메소드와 C#의 퀴리식 ]

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

- LINQ 퀴리식과 메소드를 함께 사용한다면 더욱 편할 것이다 .

using System;
using System.Linq;

namespace ConsoleApp;

class Profile
{
    public int Height { get; set; }
    public string Name { get; set; }
}
class MainApp
{   
    static void Main(string[] args)
    {
        Profile[] arrProfile =
        {
            new Profile(){Name="정우성",Height=186 },
            new Profile(){Name="김태희",Height=158 },
            new Profile(){Name="고현정",Height=172 },
            new Profile(){Name="이문세",Height=178 },
            new Profile(){Name="하하",Height=171 },
        };

        var heightStat = from profile in arrProfile
                         group profile by profile.Height < 175 into g
                         select new
                         {
                             Group = g.Key == true ? "175미만" : "이상",
                             Count=g.Count(),
                             Max=g.Max(profile=>profile.Height),
                             Min=g.Min(profile=>profile.Height),
                             Average=g.Average(profile=>profile.Height),
                         };

        foreach(var stat in heightStat)
        {
            Console.Write("{0} - Count : {1},Max:{2}",stat.Group,stat.Count,stat.Max);
            Console.WriteLine("Min : {0} Average : {1}", stat.Min, stat.Average);

        }


    }
}