LINQ에 대해서
LINQ (Language INtergrated Query)
- C#과 .NET 환경에서 사용할 수 있는 데이터 쿼리 언어
- 컬렉션이나 데이터 소스에 대해 SQL과 유사한 방식으로 데이터 조회, 필터링, 정렬, 변환 작업을 수행할 수 있다.
위 특징으로 인해 SQL문을 배운적이 있다면, LINQ의 사용법을 아주 쉽게 익힐 수 있을 것이다.
그러나 이 LINQ가 항상 좋은 것일까? 게임 개발자로써 LINQ를 어떻게 사용할지 알아보기 위해, LINQ의 장단점을 알아보자.
장점
- 간결해지는 코드. 가독성 향상
- 데이터를 처리/가공하는 로직을 간단하게 표현할 수 있다. 이러한 표현 방식은 자연어와 유사해 Human Error를 줄일 수 있을 것이고, 더욱이 높은 생산성도 기대할 수 있다.
- 이러한 점은 유지보수에도 도움이 된다.
단점
- 퍼포먼스
- LINQ는 내부적으로 iterator를 사용한다. 그 과정에서 추가적인 메모리 할당과 함수 호출이 발생한다.
- GC 증가
- LINQ의 일부 메서드 ToList(), Select()와 같은 메서드는 새로운 객체를 만들어낸다. 이러한 메서드의 호출이 빈번해질수록, GC의 부담은 커진다.
이러한 점들이 존재하기에, LINQ를 정확히 알고 적재적소에 사용할 수 있어야 한다.
대체로 성능이 중요한 기능에는 LINQ 대신 직접 작성한 명시적인 반복문이 더 나을 수 있다.
var
C# 3.0 부터 타입 추론 (type inference) 기능이 추가되면서 메서드의 지역 변수 선언을 타입에 관계없이
var
예약어로 쓸 수 있다.컴파일러에 의해 실제 타입으로 치환된다.
이렇게 객체를 선언할 때도
var
키워드를 활용해 코드를 간결하게 만들 수 있다.코드의 가독성은 무조건 좋아질지는 모르겠다. 개발자의 취향 차이가 아닐까 싶다.
함수의 반환 값을 받을 때는
var
를 사용하지 않는 것이 좀 더 좋겠다.Rider와 같이
var
로 명시된 변수에 그 실제 타입을 보여주는 기능이 있는 IDE가 있는 반면, 없는 경우도 있으니.
인스턴스 초기화
Public 으로 선언한 멤버 변수는 생성자 없이도 new 키워드와 그 값을 지정해주는 형태로 초기화가 가능하다. 먼저 예시1을 보자
예시1 (name, age 필드를 초기화하는 생성자를 가진 Person 클래스)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Person
{
private string _name;
private int _age;
public Person()
{
_name = string.Empty;
_age = 0;
}
public Person(string name)
{
_name = name;
}
public Person(int age)
{
_age = age;
}
public Person(string name, int age)
{
_name = name;
_age = age;
}
}
private void Start()
{
Person p1 = new Person();
Person p2 = new Person("John Doe");
Person p3 = new Person(21);
Person p4 = new Person("John Doe", 20);
}
이렇게 Person 클래스의 인스턴스를 생성함과 동시에 필드 초기화를 위해 다양한 버전의 생성자를 정의해야 한다. 코드를 쓸데없이 길게 만들고, 개발자에게 반복작업을 요구하는 방식이다.
예시2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Person
{
private string _name;
private int _age;
public string Name
{
get { return _name; }
set { _name = value; }
}
public int Age
{
get { return _age; }
set { _age = value; }
}
public int Height
{
get;
set;
}
}
private void Start()
{
Person p1 = new Person();
Person p2 = new Person() { Name = "John Doe"};
Person p3 = new Person() { Age = 20 };
Person p4 = new Person() { Name = "John Doe", Age = 20, Height = 170};
Debug.Log($"Name : {p1.Name} | Age : {p1.Age} | Height : {p1.Height}");
Debug.Log($"Name : {p2.Name} | Age : {p2.Age} | Height : {p2.Height}");
Debug.Log($"Name : {p3.Name} | Age : {p3.Age} | Height : {p3.Height}");
Debug.Log($"Name : {p4.Name} | Age : {p4.Age} | Height : {p4.Height}");
}
예시2의 방법으로 생성된 객체들의 필드에 담긴 값들
LINQ를 본격적으로 사용해보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public int Height { get; set; }
public void Result()
{
Debug.Log($"Name : {Name} | Age : {Age} | Height : {Height}");
}
}
private void Start()
{
List<Person> people = new List<Person>()
{
new Person {Name = "john", Age = 22, Height = 176},
new Person {Name = "tom", Age = 23, Height = 180},
new Person {Name = "tyler", Age = 24, Height = 188}
};
var result1 = from person in people select person;
foreach (var one in result1)
one.Result();
}
결과는 이렇게 나온다.
여기서 var result1 = from person in people select person;
이 한줄에 집중해보자.
from person in people
→ people 안에 있는 데이터에서select person
→ (조건을 충족하는) person 객체를 추출한다.
이 구문은 조건을 걸지 않았다.
1
2
3
var result2 = from person in people
where person.Age > 22
select person;
가령, 이렇게 조건을 건다면 나이가 22 이상인 Person 객체만 result2에 담길 것이다.
Rider에서 보면
이렇게 result1의 타입이 IEnumerable<Person>
이라는 것을 알 수 있다.
즉, 이 구문은 IEnumerable<Person>
타입의 결과값을 반환한다는 것을 알 수 있는데, 컬렉션에서 데이터를 선택하고 그 결과를 활용하는 방법 중 하나다.
그리고 이 한줄은 내부적으로 다음의 코드로 동작한다.
1
var result1 = people.Select((person) => person);
익명 형식(Anonymous Type)
컴파일러가 자동으로 정의한 클래스를 사용해 이름 없는 객체를 생성하는 기능
- 타입 이름이 없음: 컴파일러가 임시적으로 내부적으로 정의한 이름 없는 클래스.
- 읽기 전용 속성: 익명 형식의 속성은 읽기 전용이다. 수정 불가능.
- 타입 추론: 익명 형식을 변수에 할당할 때는 반드시
var
를 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public int Height { get; set; }
}
private void Start()
{
var person1 = new Person { Name = "Go", Age = 22, Height = 175 };
var person2 = new { Name = "Sun", Age = 23, Height = 180 };
}
person1
은 Person
이라는 클래스의 인스턴스이지만, person2
는 익명 형식에 해당하는 인스턴스다.
위 코드를 Rider에서 보면, 위와 같이 Name
, Age
, Height
라는 필드를 가진 익명 타입의 인스턴스라는 것을 볼 수 있다. 이렇게 익명 형식의 인스턴스는 타입 이름을 명시할 방법이 없기 때문에 반드시 var
를 사용해야 한다.
LINQ Query
기본 사용법
1
2
3
4
5
6
7
8
9
10
11
private int[] number = { 1, 2, 3, 4, 5 };
private void Start()
{
var result = from one in number select one;
foreach (var item in result)
{
Debug.Log(item);
}
}
- 모든 LINQ 쿼리식은 from 절로 시작한다. from ‘범위변수’ in ‘데이터원본’ 형식으로 사용한다.
위 코드의 실행 결과
where의 사용법
1
2
3
4
5
6
7
8
9
10
11
private int[] number = { 1, 2, 3, 4, 5 };
private void Start()
{
var result = from one in number where one > 2 select one;
foreach (var item in result)
{
Debug.Log(item);
}
}
- where : 필터 역할을 하는 연산자. from에서 뽑아낸 ‘범위 변수’의 조건을 정한다.
위 코드의 실행결과. 2보다 큰 값들만 결과에 포함되었다.
orderby의 사용법
1
2
3
4
5
6
7
8
9
10
11
private int[] number = { 1, 2, 3, 4, 5 };
private void Start()
{
var result = from one in number where one > 2 orderby one descending select one;
foreach (var item in result)
{
Debug.Log(item);
}
}
- orderby : 데이터 정렬을 수행하는 연산자. 단, 정렬할 대상이
IComparable
인터페이스를 상속받아야 한다.descending
내림차순 /ascending
오름차순
위 코드의 실행결과. 내림차순 정렬
여기서는 int
값들에 대해서 정렬을 수행했는데, 이는 int 타입 자체의 정의를 타고들어가면
이렇게 Int32 타입은 System.IComparable
인터페이스를 상속받고있음을 알 수 있다.
System.IComparable
인터페이스는 위와 같이 CompareTo
라는 함수로 객체 간의 비교를 수행하는 기능을 지원한다.
마지막으로 지금까지 사용했던
select의 사용법
1
2
3
4
5
6
7
8
9
10
11
private int[] number = { 1, 2, 3, 4, 5 };
private void Start()
{
var result = from one in number where one > 2 orderby one ascending select new {Result = one * one};
foreach (var item in result)
{
Debug.Log(item);
}
}
- select : 최종 결과를 수행하는 마침표 역할을 한다. 익명 타입으로 결과를 재가공 할 수 있다.
위 코드의 실행결과.
위 예시에서는 1, 2, 3, 4, 5 중 2보다 큰 값인 3, 4, 5를 오름차순으로 정렬한 후, 각각의 값들을 제곱하여 최종 결과가 9, 16, 25가 나온 모습을 볼 수 있다.
그룹화
group by into의 사용법1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private int[] number = { 1, 2, 3, 4, 5 };
private void Start()
{
var result = from one in number
group one by one > 2 into g
select g;
foreach (var item in result)
{
Debug.Log($"2보다 큰가?? : {item.Key}");
foreach (var one in item)
{
Debug.Log(one);
}
Debug.Log("=========================");
}
}
group A by B into C
: 데이터 그룹화 (그룹별 분류)A
: from 절에서 뽑아낸 범위 변수B
: 분류 기준C
: 그룹 변수
위 코드를 Rider에서 보면 다음과 같이 각 변수들의 타입을 보여준다.
그룹 변수 g
는 IGrouping<bool, int>
이라는 인터페이스를 상속받는 다는 것을 보여준다.
해당 인터페이스의 정의를 살펴보면
위와 같이 정의되어있음을 알 수 있다.
이를 통해, 위 코드의 동작을 이해할 수 있다.
- 1, 2, 3, 4, 5를 ‘2보다 큰가?’라는 조건에 대해 true인 경우와 false인 경우로 둘로 나누어 준다.
- 위 코드에서는 1, 2가 하나의 그룹으로, 3, 4, 5가 또다른 하나의 그룹으로 묶인다.
- 첫번째
foreach (var item in result)
반복문에서는item
이IGrouping<bool,int>
타입이다.- Key가 false인 그룹 {1, 2}가 있고, Key가 true인 그룹 {3, 4, 5}가 있다.
- 이 반복문은 여러 그룹에 대해 순회하는 반복문이다.
- 두번째
foreach (var one in item)
반복문에서는one
이int
타입이다.- 이 반복문은 하나의 그룹 내를 순회하는 반복문이다.
위 코드의 실행결과.
group by into의 사용법2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private List<string> words = new List<string>
{
"apple",
"banana",
"cherry",
"date",
"fig",
"grape",
"kiwi",
};
private void Start()
{
var groupedWords = from word in words
group word by word.Length into wordGroup
orderby wordGroup.Key
select wordGroup;
foreach (var group in groupedWords)
{
Debug.Log($"Length : {group.Key}");
foreach (var word in group)
{
Debug.Log($"{word}");
}
}
}
위 코드에 사용된 LINQ Query 를 말로 풀어 설명해보자면 다음과 같이 설명할 수 있겠다.
- words에 포함된 string 값들을 길이에 따라 그룹화한다. (Key가 그룹의 길이)
- 그룹들을 길이에 따라 오름차순으로 정렬한다.
- 이렇게 완성된 그룹들의 집합을 반환한다.
위 코드의 실행결과.
join (내부 조인) 의 사용법을 보여주는 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class MonsterName
{
public string Name { get; set; }
}
public class MonsterData
{
public string Name { get; set; }
public int Hp { get; set; }
public int Def { get; set; }
}
private void Start()
{
MonsterName[] monsterNames =
{
new MonsterName{Name = "monster1"},
new MonsterName{Name = "monster2"},
new MonsterName{Name = "monster3"},
new MonsterName{Name = "monster4"},
new MonsterName{Name = "monster5"},
};
MonsterData[] monsterDatas =
{
new MonsterData {Name = "monster1", Hp = 30, Def = 10},
new MonsterData {Name = "monster2", Hp = 20, Def = 0},
new MonsterData {Name = "monster3", Hp = 55, Def = 5},
new MonsterData {Name = "monster4", Hp = 70, Def = 0},
new MonsterData {Name = "", Hp = 90, Def = 15},
};
int damage = 20;
var monster = from one in monsterNames
join data in monsterDatas on one.Name equals data.Name
select new
{
Name = data.Name,
Hp = data.Hp - (damage - data.Def)
};
foreach (var one in monster)
Debug.Log($"Name : {one.Name} 남은 Hp : {one.Hp}");
}
- join (내부 조인) : 두 데이터의 원본을 연결한다.
from a in A
join b in B on a.XXXX equals b.YYYY
: A로부터 a, B로부터 b를 추출하여 equals 앞뒤의 필드가 동일하도록 데이터들을 연결한다.
위 코드의 실행결과.
필드의 값이 동일한 데이터끼리만 연결되기 때문에, monsterNames
의 마지막 데이터와 monsterDatas
의 마지막 데이터는 연결되지 못한다.
join (외부 조인) 의 사용법을 보여주는 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class MonsterName
{
public string Name { get; set; }
}
public class MonsterData
{
public string Name { get; set; }
public int Hp { get; set; }
public int Def { get; set; }
}
private void Start()
{
MonsterName[] monsterNames =
{
new MonsterName{Name = "monster1"},
new MonsterName{Name = "monster2"},
new MonsterName{Name = "monster3"},
new MonsterName{Name = "monster4"},
new MonsterName{Name = "monster5"},
};
MonsterData[] monsterDatas =
{
new MonsterData {Name = "monster1", Hp = 30, Def = 10},
new MonsterData {Name = "monster2", Hp = 20, Def = 0},
new MonsterData {Name = "monster3", Hp = 55, Def = 5},
new MonsterData {Name = "monster4", Hp = 70, Def = 0},
new MonsterData {Name = "", Hp = 90, Def = 15},
};
int damage = 20;
var monster = from one in monsterNames
join data in monsterDatas on one.Name equals data.Name into result
from joinData in result.DefaultIfEmpty(new MonsterData {Name = "없음"})
select new
{
Name = joinData.Name,
Hp = joinData.Hp - (damage - joinData.Def)
};
foreach (var one in monster)
Debug.Log($"Name : {one.Name} 남은 Hp : {one.Hp}");
}
이번에는 외부 조인이다. 이전의 내부 조인은 매칭되지 못하는 데이터는 누락시키는 반면, 외부 조인은 그러한 데이터도 모두 포함한다.
from a in A
join b in B on a.XXXX equals b.YYYY into ZZZZ
from c in ZZZZ.DefaultIfEmpty()
:
into
는 중간결과값을 저장한다. 그 다음의 query를 처리하기 위해.c
는ZZZZ
의 범위 변수.
위 코드의 실행결과.
monsterNames
의 마지막 데이터는 매칭되는 데이터를 찾지 못했지만, 누락되지 않고 result.DefaultIfEmpty(new MonsterData {Name = "없음"})
에 의해 만들어진 데이터와 매칭된다.
LINQ 메서드
조건에 맞는 요소들만 필터링하는 Where
1
2
3
4
5
6
7
8
9
10
11
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var everyNumber = numbers.Where(n => n % 2 == 0);
foreach (var number in everyNumber)
{
Debug.Log(number);
}
}
위 코드의 실행결과.
확장 메서드인 where의 정의는 System.Linq 안에 다음과 같이 정의되어있다.
1
2
public static System.Collections.Generic.IEnumerable<TSource> Where<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Func<TSource, bool> predicate) { throw null; }
public static System.Collections.Generic.IEnumerable<TSource> Where<TSource>(this System.Collections.Generic.IEnumerable<TSource> source, System.Func<TSource, int, bool> predicate) { throw null; }
위에서는 이 두 함수 중 첫번째 함수를 사용했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void Start()
{
var everyNumber = numbers.Where(WhereFunction);
foreach (var number in everyNumber)
{
Debug.Log(number);
}
}
bool WhereFunction(int n)
{
return n % 2 == 0;
}
람다 함수 대신 위와 같이 함수를 사용하는 것도 가능하다.
정렬에 사용되는 OrderBy
1
2
3
4
5
6
7
8
9
private int[] numbers = { 3, 2, 1, 5, 4 };
private void Start()
{
var everyNumber = numbers.OrderBy(n => n);
foreach (var number in everyNumber)
Debug.Log(number)
}
위 코드는 각 항목들을 오름차순 정렬하는 코드.
위 코드의 실행결과.
1
2
3
4
5
6
7
8
9
private int[] numbers = { 3, 2, 1, 5, 4 };
private void Start()
{
var everyNumber = numbers.OrderByDescending(n => n);
foreach (var number in everyNumber)
Debug.Log(number);
}
위와 같이 OrderByDescending
을 이용하면 다음과 같이 내림차순 정렬을 할 수 있다.
위 코드의 실행결과.
요소를 새로운 형태로 반환하는 Select
1
2
3
4
5
6
7
8
9
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var everyNumber = numbers.Select(n => n * n);
foreach (var number in everyNumber)
Debug.Log(number);
}
위 코드는 컬렉션의 각 요소들을 그것의 제곱으로 변환하는 코드.
위 코드의 실행결과.
특정 키를 기준으로 요소를 그룹화하는 GroupBy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Monster
{
public string Name { get; set; }
public int Hp { get; set; }
}
private void Start()
{
Monster[] monster =
{
new Monster{Name = "monster1", Hp = 10},
new Monster{Name = "monster2", Hp = 20},
new Monster{Name = "monster3", Hp = 30},
new Monster{Name = "monster4", Hp = 40},
new Monster{Name = "monster5", Hp = 50},
};
var grouped = monster.GroupBy(mob => mob.Hp < 25);
foreach (var item in grouped)
{
Debug.Log(item.Key);
foreach (var one in item)
Debug.Log(one.Name);
Debug.Log("==========================");
}
}
위 코드는 5가지 몬스터를 체력에 따라서 두 가지의 그룹으로 나누는 코드.
몬스터1, 2는 체력이 25보다 작기 때문에 Key가 True인 그룹으로,
몬스터 3, 4, 5는 체력이 25 이상이기 때문에 Key가 False인 그룹으로 나뉘어진다.
위 코드의 실행결과.
두 컬렉션을 합치는 Join
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Monster
{
public string MonsterName { get; set; }
public int Hp { get; set; }
}
class Item
{
public string MonsterName { get; set; }
public string ItemName { get; set; }
}
private void Start()
{
Monster[] monsters =
{
new Monster {MonsterName = "monster1", Hp = 10},
new Monster {MonsterName = "monster2", Hp = 20},
new Monster {MonsterName = "monster3", Hp = 30},
new Monster {MonsterName = "monster4", Hp = 40},
new Monster {MonsterName = "monster5", Hp = 50},
};
Item[] items =
{
new Item {MonsterName = "monster1", ItemName = "Potion"},
new Item {MonsterName = "monster2", ItemName = "Super Potion"},
new Item {MonsterName = "monster3", ItemName = "Max Potion"},
new Item {MonsterName = "monster4", ItemName = "Elixir"},
};
var query = monsters.Join(items,
monster => monster.MonsterName,
item => item.MonsterName,
(monster, item) => new {monster.MonsterName, item.ItemName});
var query2 = from mob in monsters
join item in items on mob.MonsterName equals item.MonsterName
select new { mob.MonsterName, item.ItemName };
foreach (var item in query)
{
Debug.Log($"MonsterName : {item.MonsterName}, DropItem : {item.ItemName}");
}
}
위 코드의 query
와 query2
는 같은 결과를 갖는다.
결과는 다음과 같이 두 데이터 컬렉션인 monsters
와 items
를 join한 결과다.
위 코드의 실행결과.
1
2
3
4
var query = monsters.Join(items,
monster => monster.MonsterName,
item => item.MonsterName,
(monster, item) => new {monster.MonsterName, item.ItemName});
이 코드는 처음본다면 이해하기 조금 어려울 수도 있지만, 하나씩 살펴보면 앞서 보았던 join과 동일하다는 것을 알 수 있다.
첫번째 매개변수 items
로 monsters
컬렉션을 기준으로 items
와 조인한다.
두번째, 세번째 매개변수로 기준이 되는 두 컬렉션에서 MonsterName
속성을 키로 사용한다.
네번째 매개변수로 각각의 컬렉션에서 매칭된 데이터쌍의 MonsterName
속성과 ItemName
을 사용해 익명 객체를 만든다. 이 익명객체들이 결과에 담겨 반환된다.
외부조인은 다음과 같이 할 수 있겠다.
외부조인에 사용되는 GroupJoin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var leftJoinQuery = monsters
.GroupJoin(
items,
monster => monster.MonsterName,
item => item.MonsterName,
(monster, itemGroup) => new {
MonsterName = monster.MonsterName,
ItemName = itemGroup.FirstOrDefault()?.ItemName // itemGroup에서 첫 번째 아이템을 선택, 없으면 null
}
);
foreach (var result in leftJoinQuery)
{
Debug.Log($"MonsterName : {result.MonsterName}, DropItem : {result.ItemName ?? "No Item"}");
}
- 이는
monsters
와items
를 left outer join한 것인데, 만약에 right outer join을 하고자 한다면 두 컬렉션을 반대로 사용하면 된다. GroupJoin
은 그룹화된 데이터를 반환한다.itemGroup
은 해당monster
와 연결된 모든 아이템들의 컬렉션이다.itemGroup.FirstOrDefault()
로 첫 번째 아이템을 선택하며, 연결된 아이템이 없을 경우null
을 반환한다.여기서
FirstOrDefault()
함수는 컬렉션 중 조건에 맞는 첫번째 요소를 가져오되, 조건에 맞는 요소가 없으면 기본값을 반환하는 함수다.정의는 다음과 같다.
1 2
TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source); TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
위 예제에서는 첫번째 시그니처를 사용한 것이다. 두번째 정의에서는
predicate
로 조건을 지정하는데, 해당 매개변수를 받지 않는 첫번째 정의는 그냥 컬렉션에서 첫번째 요소를 가져온다. 물론, 메서드가 비어있다면 해당 타입의 기본값을 받는다.int
면0
,bool
이면false
와 같이.
위 코드의 실행결과.
데이터 집계
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var total = numbers.Sum();
var average = numbers.Average();
var min = numbers.Min();
var max = numbers.Max();
Debug.Log($"total {total}");
Debug.Log($"average {average}");
Debug.Log($"min {min}");
Debug.Log($"max {max}");
}
위 코드의 실행결과.
첫번째와 마지막 데이터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var first = numbers.First();
var firstEven = numbers.FirstOrDefault(n => n % 2 == 0);
var last = numbers.Last();
var lastEven = numbers.LastOrDefault(n => n % 2 == 0);
Debug.Log($"first {first}");
Debug.Log($"firstEven {firstEven}");
Debug.Log($"last {last}");
Debug.Log($"lastEven {lastEven}");
}
이번에는 조건을 사용한 FirstOrDefault
함수다.
1, 2, 3, 4, 5
중에 짝수인 첫번째 숫자인 2
가 반환되고,
1, 2, 3, 4, 5
중에 짝수인 가장 마지막 숫자인 4
가 반환된다.
위 코드의 실행결과.
Any와 All
1
2
3
4
5
6
7
8
9
10
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var hasEven = numbers.Any(n => n % 2 == 0);
var allPositive = numbers.All(n => n > 0);
Debug.Log($"hasEven {hasEven}");
Debug.Log($"AllPositive {allPositive}");
}
Any
는 조건에 맞는 요소가 하나 이상 있는지 확인하며, All
은 모든 요소가 조건에 맞는지 확인한다.
반대로 All
은 하나라도 조건에 맞지 않는 요소가 있으면 false
를 반환하지만, Any
는 조건에 맞지않는 요소가 있어도 조건에 부합하는 요소가 하나라도 있다면 true
를 반환한다. 모든 요소가 조건에 맞지 않다면 Any
도 false
를 반환할 것이다.
위 코드의 실행결과.
특정 요소의 포함 여부를 확인하는 Contains
1
2
3
4
5
6
7
8
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var containsTen = numbers.Contains(10);
Debug.Log($"containsTen {containsTen}");
}
위 코드의 실행결과.
중복된 요소를 제거하는 Distinct
1
2
3
4
5
6
7
8
9
10
11
private int[] numbers = { 1, 2, 2, 4, 5 };
private void Start()
{
var distinct = numbers.Distinct();
foreach (var item in distinct)
{
Debug.Log($"{item}");
}
}
위 코드의 실행결과.
Take와 Skip
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var firstThree = numbers.Take(3);
var skippedThree = numbers.Skip(3);
Debug.Log("firstThree");
foreach (var item in firstThree)
{
Debug.Log(item);
}
Debug.Log("skippedThree");
foreach (var item in skippedThree)
{
Debug.Log(item);
}
}
Take
는 컬렉션의 앞에서부터 지정된 만큼의 요소를 반환하고, Skip
은 컬렉션의 앞에서부터 지정된만큼의 요소를 건너뛰고 나머지를 반환한다.
위 코드의 실행결과.
요소들을 누적하여 단일 결과를 만드는 Aggregate
1
2
3
4
5
6
7
8
9
10
private int[] numbers = { 1, 2, 3, 4, 5 };
private void Start()
{
var product = numbers.Aggregate((acc, n) => acc * n);
Debug.Log($"product {product}");
var product2 = numbers.Aggregate((acc, n) => acc + n);
Debug.Log($"product2 {product2}");
}
product
는 컬렉션의 모든 값들을 곱한 값이다. product2
는 컬렉션의 모든 값들을 더한 값이다.
위 코드의 실행결과.
컬렉션의 각 요소를 단일 컬렉션으로 만드는 SelectMany
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void Start()
{
var studentCourses = new List<List<string>>
{
new List<string> {"Math", "Science"},
new List<string> {"History", "Math"},
new List<string> {"Science", "Art"},
};
var allCourses = studentCourses.SelectMany(courseList => courseList);
foreach (var course in allCourses)
Debug.Log(course);
}
위 예제에서는 SelectMany
가 string을 담는 이중 리스트를 모두 펴서 하나의 리스트로 만든다.
다시말해, SelectMany
메서드는 다차원 데이터를 단일 컬렉션으로 평탄화한다.
위 코드의 실행결과.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void Start()
{
var dictionary = new List<Dictionary<string, string>>
{
new Dictionary<string, string> {{"Apple", "사과"}, {"Banana", "바나나"}},
new Dictionary<string, string> {{"Car", "고양이"}, {"Dog", "개"}}
};
var allTranslations = dictionary.SelectMany(item => item);
foreach (var translation in allTranslations)
{
Debug.Log($"{translation}");
}
}
위 코드의 실행결과.
응용 - 메서드 체이닝
지금까지 배운 LINQ 메서드들은 모두 그 결과로 또 다른 컬렉션을 반환한다. LINQ 메서드는 대부분의 컬렉션에 대해 사용할 수 있기에, 다음과 같이 여러 메서드를 연속해서 사용할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Character
{
public string Name { get; set; }
public int Level { get; set; }
public string Class { get; set; }
}
private void Start()
{
var characters = new List<Character>
{
new Character { Name = "Warrior", Level = 10, Class = "Tank" },
new Character { Name = "Archer", Level = 15, Class = "DPS" },
new Character { Name = "Mage", Level = 8, Class = "DPS" },
new Character { Name = "Paladin", Level = 12, Class = "Tank" },
new Character { Name = "Thief", Level = 6, Class = "DPS" }
};
var highLevelDPS = characters
.Where(c => c.Class == "DPS")
.Where(c => c.Level >= 10)
.OrderByDescending(c => c.Level)
.Select(c => new { c.Name, c.Level });
foreach (var character in highLevelDPS)
Debug.Log($"Name: {character.Name}, Level: {character.Level}");
}
위 코드에 사용된 LINQ 메서드들의 동작을 줄글로 풀어 설명하면
“5개의 캐릭터중에서 DPS클래스만, 레벨이 10 이상인 캐릭터만 추출한 뒤, 레벨을 기준으로 내림차순 정렬한 후에 해당 캐릭터의 이름과 레벨만을 반환한다.”
와 같다.
위 코드의 실행결과.
레퍼런스
지금까지 LINQ에 대해 알아보았지만, LINQ의 모든 것에 대해 다루지도 못했거니와 LINQ의 모든 메서드에 대해 다루지 못했다. 그만큼 방대한 정보가 있지만 블로그에서 모두 다루는 것은 무리가 있기에 모든 정보에 대해 접근할 수 있는 다음의 페이지를 참고하면 좋을 것이다.
C#의 LINQ(Language-Integrated Query) - C#
그리고 다음은 LINQ 자체에 있어서 배우고 이해할 수 있게 해준 영상이다.
항상 양질의 영상을 올려주시는 채널장분께 감사의 인사를 드린다.