[ 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);
});
}
}
}
'Programming > C#' 카테고리의 다른 글
C# 5.0 변경점 - 호출자 정보, 비동기 호출 (1) | 2019.04.24 |
---|---|
C# 4.0 변경점 - 선택적 매개변수/명명된 인자, dynamic 예약어 (0) | 2019.04.15 |
C# 3.0 변경점(2) - 람다 식 (0) | 2019.04.04 |
C# 3.0 변경점(1) - var, 자동 구현 속성, 객체/컬렉션 초기화, 익명 타입, 확장 메서드 (0) | 2019.04.03 |
C# 2.0 변경점(2) - yield return/break, partial class, Nullable, 익명 메서드, 정적 클래스 (0) | 2019.03.31 |