Programming/C#

C# 3.0 변경점(3) - LINQ(Language Integrated Query)

lee308812 2019. 4. 9. 20:34

[ LINQ ]

- 데이터의 선택/열거 작업을 일관된 방법으로 다루기 위해 기존 문법을 확장시킨 것

- 전형적인 사용 예는 컬렉션을 대상으로 쿼리를 수행하는 것이다. 

using System;
using System.Linq;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }

        public override string ToString()
        {
            return string.Format("{0}: {1} in {2}", Name, Age, Address);
        }
    }

    class MainLanguage
    {
        public string Name { get; set; }
        public string Language { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Person> people = new List<Person>
            {
                new Person{ Name = "Tom", Age = 63, Address = "Korea"},
                new Person{ Name = "Winnie", Age = 40, Address = "Tibet"},
                new Person{ Name = "Adners", Age = 47, Address = "Sudan"},
                new Person{ Name = "Hans", Age = 25, Address = "Tibet"},
                new Person{ Name = "Eureka", Age = 32, Address = "Sudan"},
                new Person{ Name = "Hawk", Age = 15, Address = "Korea"}
            };

            List<MainLanguage> languages = new List<MainLanguage>
            {
                new MainLanguage { Name = "Anders", Language = "Delphi"},
                new MainLanguage { Name = "Anders", Language = "C#"},
                new MainLanguage { Name = "Tom", Language = "Borland C++"},
                new MainLanguage { Name = "Hans", Language = "Visual C++"},
                new MainLanguage { Name = "Winnie", Language = "R"}
            };

            // yield return이 IEnumerable<T>를 반환하므로, 
            // 아래 all은 IEnumerable<Person>임을 유추할 수 있다.

            var all = from person in people   // foreach(var person in people)
                      select person;            // yield return person;

            foreach (var item in all)
                Console.WriteLine(item);

            // people 목록을 사람 이름만 담긴 string type 목록으로 바꾸기
            // var allName = people.Select( (elem) => elem.Name ); 와 동일하다.
            var allName = from person in people
                          select person.Name; 

            foreach (var item in allName)
                Console.WriteLine(item);

            // 익명 타입을 select에 사용하는 것도 가능하다.
            var dateList = from person in people
                           select new { Name = person.Name, Year = DateTime.Now.AddYears(-person.Age).Year };

            foreach (var item in dateList)
                Console.WriteLine(string.Format("{0} - {1}", item.Name, item.Year));

            // 확장 메서드로의 표현도 동일하게 적용된다.
            var dateList2 = people.Select(
                (elem) => new { Name = elem.Name, Year = DateTime.Now.AddYears(-elem.Age).Year }
                );
        }
    }
}

- LINQ 쿼리도 "간편 표기법"에 지나지 않는다. 위의 LINQ 쿼리는 C# 컴파일러에 의해 빌드 시에 원래의 확장 메서드를 사용하는 코드로 변경되어 컴파일되기 때문이다. 아래 3가지 코드는 완전히 동일한 역할을 한다.

LINQ 표현

from person in people

select person;

확장 메서드 표현 people.Select( (elem) => elem );
일반 메서드 표현

IEnumerable<Person> SelectFunc(List<Person> people)

{

        foreach(var item in people)

        {

                yield return item;

        }

}

- LINQ는 C# 3.0에 추가된 새로운 문법을 기반으로 표현된다. var 예약어, 객체 초기화, 익명 타입, 람다 식, 확장 메서드를 넣을 수 밖에 없었던 것이다.

 

[ where, orderby, group by , join ]

using System;
using System.Linq;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }

        public override string ToString()
        {
            return string.Format("{0}: {1} in {2}", Name, Age, Address);
        }
    }

    class MainLanguage
    {
        public string Name { get; set; }
        public string Language { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Person> people = new List<Person>
            {
                new Person{ Name = "Tom", Age = 63, Address = "Korea"},
                new Person{ Name = "Winnie", Age = 40, Address = "Tibet"},
                new Person{ Name = "Anders", Age = 47, Address = "Sudan"},
                new Person{ Name = "Hans", Age = 25, Address = "Tibet"},
                new Person{ Name = "Eureka", Age = 32, Address = "Sudan"},
                new Person{ Name = "Hawk", Age = 15, Address = "Korea"}
            };

            List<MainLanguage> languages = new List<MainLanguage>
            {
                new MainLanguage { Name = "Anders", Language = "Delphi"},
                new MainLanguage { Name = "Anders", Language = "C#"},
                new MainLanguage { Name = "Tom", Language = "Borland C++"},
                new MainLanguage { Name = "Hans", Language = "Visual C++"},
                new MainLanguage { Name = "Winnie", Language = "R"}
            };

            var ageOver30 = from person in people
                            where person.Age > 30 
                            select person;

            foreach (var item in ageOver30)
                Console.WriteLine(item);

            Console.WriteLine("============================");

            // 반환 형식이 bool이면 어떤 코드든 사용가능
            var endWithS = from person in people
                           where person.Name.EndsWith("s")
                           select person;

            foreach (var item in endWithS)
                Console.WriteLine(item);

            Console.WriteLine("============================");

            // orderby에 올 수 있는 값은 IComparable 인터페이스가
            // 구현된 타입이기만 하면 된다.
            var ageSort = from person in people
                          orderby person.Age
                          select person;

            foreach (var item in ageSort)
                Console.WriteLine(item);

            Console.WriteLine("============================");

            // 내림차순
            var ageSortDescending = from person in people
                                    orderby person.Age descending
                                    select person;

            foreach (var item in ageSortDescending)
                Console.WriteLine(item);

            Console.WriteLine("============================");

            // group by example 1
            var addrGroup = from person in people
                            group person by person.Address;
            
            foreach(var itemGroup in addrGroup) // group by로 묶여진 그룹을 나열하고
            {
                Console.WriteLine(string.Format("[{0}]", itemGroup.Key));
                foreach(var item in itemGroup)
                {
                    Console.WriteLine(item);
                }
                Console.WriteLine();
            }

            Console.WriteLine("============================");

            // group by example 2
            var nameAgeList = from person in people
                              group new { Name = person.Name, Age = person.Age } by person.Address;

            foreach (var itemGroup in nameAgeList)
            {
                Console.WriteLine(string.Format("[{0}]", itemGroup.Key));
                foreach(var item in itemGroup)
                {
                    Console.WriteLine(item);
                }
                Console.WriteLine();
            }

            Console.WriteLine("============================");

            // Join - 내부 조인(Inner Join)
            // on ~ equals ~ 조건을 만족하는 모든 레코드를 찾는다. (여러개라도)
            // on ~ equals ~ 조건을 만족하는 레코드가 없다면 제외한다.
            var nameToLangList = from person in people
                                 join language in languages on person.Name equals language.Name
                                 select new { Name = person.Name, Age = person.Age, Language = language.Language };

            foreach (var item in nameToLangList)
                Console.WriteLine(item);

            Console.WriteLine("============================");

            // 해당 레코드를 누락시키지 않고 포함시키는 것을 외부 조인이라 한다.
            // 별도로 외부 조인에 해당하는 구문은 없으며, 그에 준하는 방법이 있음.
            // join으로 엮이는 컬렉션을 한번 더 후처리하는 방법을 사용한다.

            // lang이라는 임시 컬렉션에 보관 후, 
            // 개별 요소에 대해 IEnumerable<T> 타입의 DefaultIfEmpty 확장 메서드를 호출한다.
            // DefaultIfEmpty는 값이 비어있으면 인자로 전달된 값을 사용한다.
            var nameToAllLangList = from person in people
                                    join language in languages on person.Name equals language.Name into lang 
                                    from language in lang.DefaultIfEmpty(new MainLanguage())
                                    select new { Name = person.Name, Age = person.Age, Language = language.Language };

            foreach (var item in nameToAllLangList)
                Console.WriteLine(item);

        }
    }
}

- 사실상 컬렉션에 대해 LINQ 질의를 수행하는 것은 IEnumerable<T>의 확장 메서드를 호출하는 것과 별반 다르지 않다.

 

LINQ IEnumerable<T> 확장 메서드
select Select
where Where
orderby [ascending] OrderBy
orderby [descending] OrderByDescending
group .. by GroupBy
join ... in ... on ... equals Join
join ... in ... on ... equals ... into GroupJoin

 

[ 표준 쿼리 연산자 ]

- LINQ 쿼리의 대상은 IEnumerable<T> 타입이거나 그것을 상속한 객체여야 한다. 그와 같은 객체에 LINQ 쿼리를 사용하면 C# 컴파일러는 내부적으로 IEnumerable<T> 확장 메서드로 변경해 소스코드를 빌드한다.

 

- 이 때문에 IEnumerable<T>에 정의된 확장 메서드는 표준 쿼리 연산자(standard query operators)라고 하며, OrderBy, ThenBy, Reverse, Distinct 등이 있다.

using System;
using System.Linq;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> list = new List<int> { 1, 1, 1, 2, 2, 2, 3, 3, 3, 1, 4 };

            list = list.Distinct().ToList(); // 중복 제거

            list.ForEach((elem) => { Console.WriteLine(elem); });

        }
    }
}

- 물론 LINQ 쿼리에 대응하지 않는 표준 연산자는 어차피 IEnumerable<T> 타입을 대상으로 동작하기 때문에 LINQ 쿼리의 결과와 함께 쓸 수 있다.

using System;
using System.Linq;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }

        public override string ToString()
        {
            return string.Format("{0}: {1} in {2}", Name, Age, Address);
        }
    }

    class MainLanguage
    {
        public string Name { get; set; }
        public string Language { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Person> people = new List<Person>
            {
                new Person{ Name = "Tom", Age = 63, Address = "Korea"},
                new Person{ Name = "Winnie", Age = 40, Address = "Tibet"},
                new Person{ Name = "Anders", Age = 47, Address = "Sudan"},
                new Person{ Name = "Hans", Age = 25, Address = "Tibet"},
                new Person{ Name = "Eureka", Age = 32, Address = "Sudan"},
                new Person{ Name = "Hawk", Age = 15, Address = "Korea"}
            };

            var all = from person in people
                      where person.Address == "Korea"
                      select person;

            var oldestAge = all.Max((elem) => elem.Age);
            Console.WriteLine(oldestAge);

            var oldestAge2 = people.Where((elem) => elem.Address == "Korea").Max((elem) => elem.Age);
            Console.WriteLine(oldestAge2);
        }

        static bool IsEqual(string arg1, string arg2)
        {
            Console.WriteLine("Executed");
            return arg1 == arg2;
        }
    }
}

 

- 표준 쿼리 연산자 가운데 IEnumerable<T>, IOrderedEnumerable<TElement>를 반환하는 메서드를 제외한 다른 모든 것들은 LINQ 식이 평가되면서 곧바로 실행된다. 따라서 LINQ라고 해도 모두 지연된 연산(lazy evaluation) 방식으로 동작하는 것이 아니다.

using System;
using System.Linq;
using System.Collections.Generic;

namespace ConsoleApp1
{
    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Address { get; set; }

        public override string ToString()
        {
            return string.Format("{0}: {1} in {2}", Name, Age, Address);
        }
    }

    class MainLanguage
    {
        public string Name { get; set; }
        public string Language { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            List<Person> people = new List<Person>
            {
                new Person{ Name = "Tom", Age = 63, Address = "Korea"},
                new Person{ Name = "Winnie", Age = 40, Address = "Tibet"},
                new Person{ Name = "Anders", Age = 47, Address = "Sudan"},
                new Person{ Name = "Hans", Age = 25, Address = "Tibet"},
                new Person{ Name = "Eureka", Age = 32, Address = "Sudan"},
                new Person{ Name = "Hawk", Age = 15, Address = "Korea"}
            };

            // LINQ 쿼리가 바로 실행됨
            Console.WriteLine("ToList() excuted");

            var inKorea = (from person in people
                           where IsEqual(person.Address, "Korea")
                           select person).ToList();
            Console.ReadLine();

            Console.WriteLine("IEnumerable<T> Where/Select evaluated");
            // IEnumerable<T>를 반환하므로 LINQ 쿼리가 평가만 되고 실행되지 않음

            var inKorea2 = from person in people
                            where IsEqual(person.Address, "Korea")
                            select person;
            // IsEqual 메서드가 실행되지 않는다.

            Console.ReadLine();

            // Take 확장 메서드 역시 IEnumerable<T>를 반환하므로 "Executed"문자열이 출력되지 않음.
            Console.WriteLine("IEnumerable<T> Take evaluated");
            var firstPeople = inKorea2.Take(1);

            Console.ReadLine();

            // 열거를 시작했을 때 LINQ 쿼리가 실제로 실행된다.
            foreach (var item in firstPeople)
                Console.WriteLine(item);

            Console.WriteLine("Single() Run");
            // 단일 값을 반환하는 Single 메서드의 호출은 곧바로 LINQ 쿼리가 실행되게 만듦.
            Console.WriteLine(firstPeople.Single()); // Single = 유일 요소 반환. 하나가 아니면 오류 Throw
        }

        static bool IsEqual(string arg1, string arg2)
        {
            Console.WriteLine("Executed");
            return arg1 == arg2;
        }
    }
}

[ 일관된 데이터 조회 ]

- LINQ 쿼리는 IEnumerable<T> 타입과 그것을 상속받은 타입을 대상으로 동작한다. 닷넷 응용 프로그램에서 IEnumerable<T> 대상이 되는 타입은 배열과 List<T>, Dictionary<TKey, TValue> 등의 컬렉션이다.

- 그 밖의 데이터 원본에 대해서도 IEnumerable<T>를 상속받아 정의하기만 한다면 마찬가지로 LINQ 쿼리를 수행할 수 있다.

 

- LINQ to XML : XML 자료형에 LINQ 쿼리를 수행할 수 있도록 "System.Xml.Linq" 네임스페이스 아래에 IEnumerable<T>와의 연동이 가능한 XElement, XAttribute, XDocument 등의 타입을 만들어뒀다. 

- SQL to XML : "System.Data.Linq" 네임스페이스 아래에 구현

 

- LINQ가 의미 있는 이유는 갖가지 다양한 데이터 원본에 대한 접근법을 단일화 했다는 것이다. 해당 데이터 원본마다 "LINQ 제공자(provider)"만 구현한다면 데이터를 조회할 때 LINQ 쿼리를 일관되게 사용할 수 있다.

using System;
using System.Linq;
using System.Xml.Linq;
using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            string txt = @"
<people>
    <person name='anders' age='47' />
    <person name='winnie' age='13' />
</people>";

            StringReader sr = new StringReader(txt);

            var xml = XElement.Load(sr); // XElement Type

            var query = from person in xml.Elements("person")
                        select person;

            Array.ForEach(query.ToArray(), (item) =>
            {
                Console.WriteLine(item.Attribute("name").Value + ": " + item.Attribute("age").Value);
            });
        }
    }
}