전체 페이지뷰

2017년 1월 4일 수요일

C# LINQ

LINQ란 Languqge Integrated Query의 약자입니다.
Query라는 말은 "질문", "문의"라는 뜻으로 DB 작업에서 많이 쓰이는걸 보셨을 겁니다.



일반적으로 SQL DB나 xml형식의 데이터를 프로그래밍에 잇어 자주 사용하게 되고, 그러기 위해서 프로그래머들은 또 SQL, XQuery 등의 보조 언어를 추가로 배워야 했는데 LINQ는 C# 자체에 쿼리 기능을 부여하므로 데이터를 다루는 시간을 절약해줍니다.

기본적으로 Query는
from : 어디로부터
where : 어떤 값의 데이터를
orderby : 어떤 순서로
select : 어떤 항목을 추출할 것인가...를 포함합니다.

이제 다음과 같은 클래스를 선언해 보겠습니다.

class Score
{
    public string Name { get; set; }
    public int MathScore { get; set; }


이름과 수학점수만을 프로퍼티로 갖는 간단한 클래스입니다.
이 클래스의 배열을 선언해 봅니다.

Score[] scores =
{
    new Score() { Name="A",MathScore=67 },
    new Score() { Name="B",MathScore=99 },
    new Score() { Name="C",MathScore=25 },
    new Score() { Name="D",MathScore=78 },
    new Score() { Name="E",MathScore=84 }
 };

여기서 70점 이상의 데이터를 골라 오름차순으로 정리된 새 컬렉션으로 만드는 것은 지금까지의 배워온 바에 의하면 다음과 같습니다.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace LinqExample
{
    class Program
    {
        class Score
        {
            public string Name { get; set; }
            public int MathScore { get; set; }
        }
        static void Main(string[] args)
        {
            Score[] scores ={
                new Score() { Name="A",MathScore=67 },
                new Score() { Name="B",MathScore=99 },
                new Score() { Name="C",MathScore=25 },
                new Score() { Name="D",MathScore=78 },
                new Score() { Name="E",MathScore=84 }
            };
            // 선택된 클래스를 담을 새 리스트를 생성
            List<Score> selectedScores = new List<Score>();
            // scores에서 수학점수가 70점 이상인 것을 골라 리스트에 담음
            foreach (Score s in scores)
            {
                if (s.MathScore >= 70)
                    selectedScores.Add(s);
            }
            // 성적의 오름차순 정렬
            selectedScores.Sort(
                (score1, score2) =>
                {
                    return score1.MathScore - score2.MathScore;
                });
            foreach (var score in selectedScores)
                Console.WriteLine("The Math score of {0} is {1}.", score.Name, score.MathScore);
        }
    } 
}
cs


이것을 LINQ를 이용해서 바꿔보겠습니다.

using System;
using System.Linq;
namespace LinqExample
{
    class Program
    {
        class Score
        {
            public string Name { get; set; }
            public int MathScore { get; set; }
        }
        static void Main(string[] args)
        {
            Score[] scores ={
                new Score() { Name="A",MathScore=67 },
                new Score() { Name="B",MathScore=99 },
                new Score() { Name="C",MathScore=25 },
                new Score() { Name="D",MathScore=78 },
                new Score() { Name="E",MathScore=84 }
            };
            var selectedScores = from score in scores
                                 where score.MathScore >= 70
                                 orderby score.MathScore
                                 select score;
            foreach (var score in selectedScores)
                Console.WriteLine("The Math score of {0} is {1}.", score.Name, score.MathScore);
        }
    } 
}
cs

결과)
The Math score of D is 78.
The Math score of E is 84.
The Math score of B is 99.

훨씬 간결합니다. 이제 위의 생소한 LINQ의 내용을 하나씩 살펴보도록 합시다.


from

LINQ에 쓰이는 쿼리식은 from으로 시작합니다. from은 쿼리의 대상이 되는 데이터 소스를 지정합니다. 이 데이터 소스는 반드시 IEnumerable<T>를 상속받거나 IQueryable<T>와 같은 파생형식이어야 합니다.

var result = from num in numbers
cs
이렇게 from 뒤에는 원본인 numbers와 범위변수인 num을 지정해줍니다.
이 때 numbers의 형식이 정해져 있으므로 num은 형식 추론이 이루어져 딱히 형식을 써 줄 필요가 없습니다. ArrayList와 같은 제너릭이 아닌 IEnumerable의 경우에만 형식을 명시해줍니다.

마치 foreach문 처럼 from도 중첩하여 사용 가능합니다.
var scoreQuery = from student in students
                 from score in student.Scores
cs



where

from에서 뽑아온 범위변수가 where로 넘겨지면 where은 이 변수가 조건에 부합하는가를 판정합니다.
from num in numbers
where num < 5
cs
와 같은 식으로 말입니다.

boolean으로 판정되므로 마치 if문 사용하듯이 &&이나 ||로 조건을 연결해서 사용 가능하고, boolean을 반환하는 메소드도 사용 가능합니다.


orderby

이름 그대로 데이터의 정렬을 수행합니다. 기본으로는 오름차순(ascending)이며, 내림차순(descending)도 가능합니다.

from num in numbers
where num < 5
orderby num 
cs
기본인 ascending정렬을 합니다.
다음과 같이 ascending을 명시해줘도 됩니다.
from num in numbers
where num < 5
orderby num ascending
cs

내림차순일 때는 descending을 써줍니다
from num in numbers
where num < 5
orderby num descending
cs


select

최종적으로 뭐리가 실행된 이후 골라낼 값을 지정합니다.

이제 위의 내용을 이용하여 프로그램을 작성해 보겠습니다.

using System;
using System.Linq;
namespace LinqExample
{
    class Program
    {
        class Fruit
        {
            public string Name { get; set; }
            public int CostWon { get; set; }
        }
        class program
        {
            static void Main(string[] args)
            {
                Fruit[] fruitsArray =
                {
                    new Fruit() {Name="apple",CostWon=1000 },
                    new Fruit() {Name="cherry",CostWon=600 },
                    new Fruit() {Name="banana",CostWon=350 }
                };
                // 가격 오름차순 정렬
                var fruits1 = from fruit in fruitsArray
                             orderby fruit.CostWon
                             select fruit;
                foreach (var fruit in fruits1)
                    Console.WriteLine("{0} : {1} 원", fruit.Name, fruit.CostWon);
                Console.WriteLine();
                // 이름 내림차순 정렬
                var fruits2 = from fruit in fruitsArray
                              orderby fruit.Name descending
                              select fruit;
                foreach (var fruit in fruits2)
                    Console.WriteLine("{0} : {1} 원", fruit.Name, fruit.CostWon);
                Console.WriteLine();
                // 가격 내림차순 달러로 변환
                var fruits3 = from fruit in fruitsArray
                              orderby fruit.CostWon descending
                              select new
                              {
                                  Name = fruit.Name,
                                  CostDollar = (double)fruit.CostWon / 1200
                              };
                foreach (var fruit in fruits3)
                    Console.WriteLine("{0} : ${1} ", fruit.Name, fruit.CostDollar);
            }
        }
    } 
}
cs

결과)
banana : 350 원
cherry : 600 원
apple : 1000 원

cherry : 600 원
banana : 350 원
apple : 1000 원

apple : $0.833333333333333
cherry : $0.5
banana : $0.291666666666667

orderby로 정렬을 해보았고, 마지막에는 무명형식을 이용하여 임의의 환율로 가격을 달러변환하여 내림차순으로 출력해 보았습니다.

중첩 from

foreach문 사용하듯이 from을 중첩하여 사용할 수 있습니다.

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

이 클래스를 바탕으로 배열을 선언합니다.

Class[] arrayClass = 
{
    new Class(){Name="A반", Score =  new int[]{100, 88, 72, 45}},
    new Class(){Name="B반", Score =  new int[]{65, 94, 81, 75}},
    new Class(){Name="C반", Score =  new int[]{99, 82, 79, 52}},
    new Class(){Name="D반", Score =  new int[]{88, 90, 18, 68}}
};

이 배열로부터 60점 미만인 학생이 속한 반과 해당 학생의 점수를 골라냅니다.

var classes = from c in arrayClass
                     from s in c.Score
                     where s < 60
                 select new { c.Name, lowScore = s };

from으로 뽑아낸 배열을 또 from으로 순회하여 필요한 무명형식을 골라냅니다.

using System;
using System.Linq;
namespace DoubleFrom
{
    class Program
    {
        class Class
        {
            public string Name { get; set; }
            public int[] Score { get; set; }  //배열
        }
        class program
        {
            static void Main(string[] args)
            {
                Class[] arrayClass =                    
                {
                    new Class(){Name="A반", Score =  new int[]{100887245}},
                    new Class(){Name="B반", Score =  new int[]{65948175}},
                    new Class(){Name="C반", Score =  new int[]{99827952}},
                    new Class(){Name="D반", Score =  new int[]{8890180}}
                };
                var classes = from c in arrayClass
                              from s in c.Score
                              where s < 60
                              select new { c.Name, lowScore = s };
                foreach (var c in classes)
                    Console.WriteLine("낙제 : {0} ({1})", c.Name, c.lowScore);
            }
        }
    } 
}
cs


group ~by  ~into


이름에서 유추할 수 있듯이 주어진 조건에 따라 데이터를 분류하는 구문입니다.

group 범위변수 by 조건 into 저장할그룹변수
의 형식으로 사용합니다.

using System;
using System.Linq;
namespace GroupBy
{
    class Score
    {
        public string Name { get; set; }
        public int MathScore { get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Score[] scores ={
                new Score() { Name="A",MathScore=67 },
                new Score() { Name="B",MathScore=99 },
                new Score() { Name="C",MathScore=25 },
                new Score() { Name="D",MathScore=78 },
                new Score() { Name="E",MathScore=84 }
            };
            var selectedScores = from score in scores                                
                                 orderby score.MathScore
                                 group score by score.MathScore < 60 into g
                                 select new { GroupKey = g.Key, Scores = g };
            
            foreach (var Group in selectedScores)
            {
                Console.WriteLine(" 60점 미만 : {0}", Group.GroupKey);
                foreach (var score in Group.Scores)
                {
                    Console.WriteLine("    {0}, {1}", score.Name, score.MathScore);
                }
            }
        }
    }
}
cs


결과)
 60점 미만 : True
    C, 25
 60점 미만 : False
    A, 67
    D, 78
    E, 84
    B, 99

수학점수가 60점 미만인가 아닌가를 조건으로 g라는 범위변수에 True인 컬렉션과 False인 컬렉션이 담겨집니다. 보다 자세한 group의 예제는 MSDN을 참고하세요.


join



서로 다른 두 데이터를 연결하는데 연결 방식에 따라 내부조인, 외부조인으로 나뉩니다.
이를 설명하기에 앞서 먼저 두 가지의 클래스를 한번 작성해보겠습니다.

class Product
{
    public string Name { get; set; }
    public int CategoryID { get; set; }
}

class Category
{
    public string Name { get; set; }
    public int ID { get; set; }
}

하나는 개별상품에 대한 클래스인 Product로 Name과 CategoryID의 두 프로퍼티를 갖고 있습니다.

그리고 또 하나는 Category 클래스로 Name과 ID라는 프로퍼티를 갖고 있습니다.
이를 기준으로 두개의 List를 생성하겠습니다.

먼저 상품의 분류를 위한 카테고리 리스트입니다.
List<Category> categories = new List<Category>
{
    new Category() {Name="음료",ID=001 },
    new Category() {Name="육류",ID=002 },
    new Category() {Name="채소",ID=003 },
    new Category() {Name="과일",ID=004 },
    new Category() {Name="곡물",ID=005 },
};

그리고 개별상품 목록을 위한 리스트입니다.
List<Product> products = new List<Product>
{
   new Product() {Name="콜라",CategoryID=001 },
   new Product() {Name="등심",CategoryID=002 },
   new Product() {Name="상추",CategoryID=003 },
   new Product() {Name="사과",CategoryID=004 },   
   new Product() {Name="삼겹살",CategoryID=002 },
   new Product() {Name="아이스크림",CategoryID=006 },
};

이 두 리스트를 가지고 얘기를 진행하겠습니다.

내부조인

내부조인은 첫번째 데이터를 기준으로 두번째를 비교해서 공통으로 갖고 있는 것에 정보를 덧붙여서 합쳐줍니다.


var innerJoinQuery = from category in categories

                                 join product in products on category.ID equals product.CategoryID
                                 select new
                                 {
                                     Name = product.Name,
                                     Category = category.Name,
                                     ID = product.CategoryID
                                 };

첫번째 기준이 되는 categories 데이터에 products 데이터를 비교해서 ID가 겹치는 것을 중심으로 새로운 정보를 만들어 내는 것입니다. on과 equals라는 키워드가 쓰였네요.

using System;
using System.Collections.Generic;
using System.Linq;
namespace InnerJoinExample
{
    class Product
    {
        public string Name { get; set; }
        public int CategoryID { get; set; }
    }
    class Category
    {
        public string Name { get; set; }
        public int ID { get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {
            List<Category> categories = new List<Category>
            {
                new Category() {Name="음료",ID=001 },
                new Category() {Name="육류",ID=002 },
                new Category() {Name="채소",ID=003 },
                new Category() {Name="과일",ID=004 },
                new Category() {Name="곡물",ID=005 },
            };
            List<Product> products = new List<Product>
            {
                new Product() {Name="콜라",CategoryID=001 },
                new Product() {Name="등심",CategoryID=002 },
                new Product() {Name="상추",CategoryID=003 },
                new Product() {Name="사과",CategoryID=004 },                
                new Product() {Name="삼겹살",CategoryID=002 },
                new Product() {Name="아이스크림",CategoryID=006 },
            };
            var innerJoinQuery = from category in categories
                                 join product in products on category.ID equals product.CategoryID
                                 select new
                                 {
                                     Name = product.Name,
                                     Category = category.Name,
                                     ID = product.CategoryID
                                 };
            foreach (var item in innerJoinQuery)
                Console.WriteLine("{0} : 분류 = {1} : 분류ID = {2}", item.Name, item.Category, item.ID);
        }
    }
}
cs


결과)
콜라 : 분류 = 음료 : 분류ID = 1
등심 : 분류 = 육류 : 분류ID = 2
삼겹살 : 분류 = 육류 : 분류ID = 2
상추 : 분류 = 채소 : 분류ID = 3
사과 : 분류 = 과일 : 분류ID = 4

보시다시피 양쪽 리스트 모두에 들어있는 상품만을 기준으로 새로운 데이터를 만들어 냈습니다. 일종의 교집합입니다.

외부 조인

이 외부 조인은 왼쪽우선 외부조인이라고 부릅니다. 말은 복잡하지만 실상은 간단한데,
기준이 되는 데이터는 전부 포함되고 거기에 공통으로 존재하는 데이터에 오른쪽 정보를 덧붙여 준다는 것입니다. 
따라서 왼쪽에는 데이터가 덧붙여지는 것도 있고 아닌 것도 있겠죠.
('오른쪽 우선'이면 오른쪽 데이터를 기준으로 하는 것이고, '완전'이면 합집합이 되겠죠. 그러나 C#에서는 왼쪽우선만을 지원하므로 그냥 외부조인이라고 부르기로 하겠습니다.)

var outer=from category in categories
                join product in products on category.ID equals product.CategoryID into prodGroup
                from item in prodGroup.DefaultIfEmpty(
                                         new Product() { Name = "해당없음", CategoryID = category.ID })
                select new
                {
                     Name = item.Name,
                     Category = item.CategoryID                                   
                };

join으로 결과를 합쳐서 prodGroup으로 넣고,
DefaultIfEmpty 메소드로 비어있는 값을 채워줍니다.
그리고 무명형식으로 새로운 결과를 뽑아냅니다.

using System;
using System.Collections.Generic;
using System.Linq;
namespace OuterJoinExample
{
    class Product
    {
        public string Name { get; set; }
        public int CategoryID { get; set; }
    }
    class Category
    {
        public string Name { get; set; }
        public int ID { get; set; }
    }
    class Program
    {
        static void Main(string[] args)
        {
            List<Category> categories = new List<Category>
            {
                new Category() {Name="음료",ID=001 },
                new Category() {Name="육류",ID=002 },
                new Category() {Name="채소",ID=003 },
                new Category() {Name="과일",ID=004 },
                new Category() {Name="곡물",ID=005 },
            };
            List<Product> products = new List<Product>
            {
                new Product() {Name="콜라",CategoryID=001 },
                new Product() {Name="등심",CategoryID=002 },
                new Product() {Name="상추",CategoryID=003 },
                new Product() {Name="사과",CategoryID=004 },
                new Product() {Name="삼겹살",CategoryID=002 },
                new Product() {Name="아이스크림",CategoryID=006 },
            };
            var outerJoinQuery = from category in categories
                                 join product in products on category.ID equals product.CategoryID into prodGroup
                                 from item in prodGroup.DefaultIfEmpty(new Product() { Name = "해당없음", CategoryID = category.ID })
                                 select new
                                 {
                                     Name = item.Name,
                                     Category = item.CategoryID                                   
                                 };
            foreach (var item in outerJoinQuery)
                Console.WriteLine("{0} : 분류ID = {1}", item.Name, item.Category);
        }
    }
}
cs

결과)
콜라 : 분류ID = 1
등심 : 분류ID = 2
삼겹살 : 분류ID = 2
상추 : 분류ID = 3
사과 : 분류ID = 4
해당없음 : 분류ID = 5

링크 쿼리에 대해 더 알고 싶으신 분은 MSDN을 참고하시기 바랍니다.
LINQ의 다양한 용법에 대해서는 다시 알아볼 날이 있을 겁니다.

댓글 없음:

댓글 쓰기