`select` 절
select 절 (C# Reference)
이 글에서는 쿼리 식에서 select 절이 하는 일을 하나씩 짚어볼게요. 먼저 큰 그림을 보면, 쿼리 식에 쓰이는 select 절은 그 쿼리가 실행될 때 어떤 타입의 값을 만들어 내는지를 정해줘요. 그 결과는 지금까지 나열된 모든 절들과 select 절 자체 안의 식들을 평가해서 나오는 값이에요. 그리고 쿼리 식은 반드시 select 절이나 group 절로 끝나야 해요. 이 두 가지로 끝나지 않는 쿼리 식은 올바른 쿼리 식이 아니랍니다.
가장 단순한 select 절이 쿼리 식 안에서 어떻게 쓰이는지 다음 예시로 확인해 볼게요.
namespace SelectClause;
class SelectSample1
{
static void Main()
{
//Create the data source
List<int> Scores = [97, 92, 81, 60];
// Create the query.
IEnumerable<int> queryHighScores =
from score in Scores
where score > 80
select score;
// Execute the query.
foreach (int i in queryHighScores)
{
Console.Write(i + " ");
}
}
}
//Output: 97 92 81
select 절이 만들어 내는 시퀀스의 타입이 쿼리 변수 queryHighScores의 타입을 정해요. 가장 단순한 경우에는 select 절이 그냥 범위 변수(그러니까 score 같은 걸 말하죠)만 지정해요. 이렇게 하면 반환되는 시퀀스가 데이터 소스와 같은 타입의 요소를 담게 돼요. 자세한 내용은 LINQ 쿼리 연산의 타입 관계 문서를 참고해 주세요. 그런데 select 절은 단순히 넘겨주는 것만 하는 게 아니에요. 소스 데이터를 새로운 타입으로 변환(프로젝션(projection))하는 강력한 도구이기도 해요. 이쪽을 더 알고 싶으면 데이터 변환 with LINQ (C#)를 읽어 보시면 좋아요.
select 절이 취할 수 있는 여러 가지 형태를 다음 예시에서 한꺼번에 보여 드릴게요. 각 쿼리에서 select 절과 쿼리 변수(studentQuery1, studentQuery2 같은 것들)의 타입이 어떻게 연결되는지 주목해서 보시면 좋아요.
namespace SelectClause;
class SelectSample2
{
// Define some classes
public class Student
{
public required string First { get; init; }
public required string Last { get; init; }
public required int ID { get; init; }
public required List<int> Scores;
public ContactInfo? GetContactInfo(SelectSample2 app, int id)
{
ContactInfo? cInfo =
(from ci in app.contactList
where ci.ID == id
select ci)
.FirstOrDefault();
return cInfo;
}
public override string ToString() => $"{First} {Last}:{ID}";
}
public class ContactInfo
{
public required int ID { get; init; }
public required string Email { get; init; }
public required string Phone { get; init; }
public override string ToString() => $"{Email},{Phone}";
}
public class ScoreInfo
{
public double Average { get; init; }
public int ID { get; init; }
}
// The primary data source
List<Student> students =
[
new Student {First="Svetlana", Last="Omelchenko", ID=111, Scores= new List<int>() {97, 92, 81, 60}},
new Student {First="Claire", Last="O'Donnell", ID=112, Scores= new List<int>() {75, 84, 91, 39}},
new Student {First="Sven", Last="Mortensen", ID=113, Scores= new List<int>() {88, 94, 65, 91}},
new Student {First="Cesar", Last="Garcia", ID=114, Scores= new List<int>() {97, 89, 85, 82}},
];
// Separate data source for contact info.
List<ContactInfo> contactList =
[
new ContactInfo {ID=111, Email="[email protected]", Phone="206-555-0108"},
new ContactInfo {ID=112, Email="[email protected]", Phone="206-555-0298"},
new ContactInfo {ID=113, Email="[email protected]", Phone="206-555-1130"},
new ContactInfo {ID=114, Email="[email protected]", Phone="206-555-0521"}
];
static void Main(string[] args)
{
SelectSample2 app = new SelectSample2();
// Produce a filtered sequence of unmodified Students.
IEnumerable<Student> studentQuery1 =
from student in app.students
where student.ID > 111
select student;
Console.WriteLine("Query1: select range_variable");
foreach (Student s in studentQuery1)
{
Console.WriteLine(s.ToString());
}
// Produce a filtered sequence of elements that contain
// only one property of each Student.
IEnumerable<String> studentQuery2 =
from student in app.students
where student.ID > 111
select student.Last;
Console.WriteLine("\r\n studentQuery2: select range_variable.Property");
foreach (string s in studentQuery2)
{
Console.WriteLine(s);
}
// Produce a filtered sequence of objects created by
// a method call on each Student.
IEnumerable<ContactInfo> studentQuery3 =
from student in app.students
where student.ID > 111
select student.GetContactInfo(app, student.ID);
Console.WriteLine("\r\n studentQuery3: select range_variable.Method");
foreach (ContactInfo ci in studentQuery3)
{
Console.WriteLine(ci.ToString());
}
// Produce a filtered sequence of ints from
// the internal array inside each Student.
IEnumerable<int> studentQuery4 =
from student in app.students
where student.ID > 111
select student.Scores[0];
Console.WriteLine("\r\n studentQuery4: select range_variable[index]");
foreach (int i in studentQuery4)
{
Console.WriteLine($"First score = {i}");
}
// Produce a filtered sequence of doubles
// that are the result of an expression.
IEnumerable<double> studentQuery5 =
from student in app.students
where student.ID > 111
select student.Scores[0] * 1.1;
Console.WriteLine("\r\n studentQuery5: select expression");
foreach (double d in studentQuery5)
{
Console.WriteLine($"Adjusted first score = {d}");
}
// Produce a filtered sequence of doubles that
// are the result of a method call.
IEnumerable<double> studentQuery6 =
from student in app.students
where student.ID > 111
select student.Scores.Average();
Console.WriteLine("\r\n studentQuery6: select expression2");
foreach (double d in studentQuery6)
{
Console.WriteLine($"Average = {d}");
}
// Produce a filtered sequence of anonymous types
// that contain only two properties from each Student.
var studentQuery7 =
from student in app.students
where student.ID > 111
select new { student.First, student.Last };
Console.WriteLine("\r\n studentQuery7: select new anonymous type");
foreach (var item in studentQuery7)
{
Console.WriteLine("{0}, {1}", item.Last, item.First);
}
// Produce a filtered sequence of named objects that contain
// a method return value and a property from each Student.
// Use named types if you need to pass the query variable
// across a method boundary.
IEnumerable<ScoreInfo> studentQuery8 =
from student in app.students
where student.ID > 111
select new ScoreInfo
{
Average = student.Scores.Average(),
ID = student.ID
};
Console.WriteLine("\r\n studentQuery8: select new named type");
foreach (ScoreInfo si in studentQuery8)
{
Console.WriteLine("ID = {0}, Average = {1}", si.ID, si.Average);
}
// Produce a filtered sequence of students who appear on a contact list
// and whose average is greater than 85.
IEnumerable<ContactInfo> studentQuery9 =
from student in app.students
where student.Scores.Average() > 85
join ci in app.contactList on student.ID equals ci.ID
select ci;
Console.WriteLine("\r\n studentQuery9: select result of join clause");
foreach (ContactInfo ci in studentQuery9)
{
Console.WriteLine("ID = {0}, Email = {1}", ci.ID, ci.Email);
}
}
}
/* Output
Query1: select range_variable
Claire O'Donnell:112
Sven Mortensen:113
Cesar Garcia:114
studentQuery2: select range_variable.Property
O'Donnell
Mortensen
Garcia
studentQuery3: select range_variable.Method
[email protected],206-555-0298
[email protected],206-555-1130
[email protected],206-555-0521
studentQuery4: select range_variable[index]
First score = 75
First score = 88
First score = 97
studentQuery5: select expression
Adjusted first score = 82.5
Adjusted first score = 96.8
Adjusted first score = 106.7
studentQuery6: select expression2
Average = 72.25
Average = 84.5
Average = 88.25
studentQuery7: select new anonymous type
O'Donnell, Claire
Mortensen, Sven
Garcia, Cesar
studentQuery8: select new named type
ID = 112, Average = 72.25
ID = 113, Average = 84.5
ID = 114, Average = 88.25
studentQuery9: select result of join clause
ID = 114, Email = [email protected]
*/
앞 예시의 studentQuery8처럼, 반환되는 시퀀스의 요소가 소스 요소의 속성 중 일부만 담게 하고 싶을 때가 있어요. 반환되는 시퀀스를 최대한 작게 유지하면 메모리 요구량이 줄고 쿼리 실행 속도도 빨라져요. 이 목표는 select 절에서 익명 타입을 만들고, 개체 이니셜라이저(object initializer)로 소스 요소에서 필요한 속성만 골라 초기화하면 이룰 수 있어요. 실제로 어떻게 하는지 예시가 궁금하다면 개체 및 컬렉션 이니셜라이저 문서를 확인해 보세요.
마지막으로, 컴파일 시점에 select 절은 표준 쿼리 연산자 <xref:System.Linq.Enumerable.Select*>를 호출하는 메서드 호출로 번역돼요. 그래서 select 절이 실제로는 LINQ의 Select 연산자를 표현하는 문법 설탕(syntactic sugar)이라고 생각하면 이해가 쉬워요.