C#/LINQ

[C#]LINQ 누적기 함수 - Aggregate 메서드

DevStory 2022. 8. 3.

Aggregate 메서드

C#의 Linq는 집계 함수를 제공합니다. 그중 Aggregate() 메서드는 누적 연산을 수행합니다.


메서드 오버로드

System.Linq의 Enumerable 클래스에 정의된 Aggregate() 메서드를 확인할 수 있으며, 세 가지 오버로드된 버전이 존재합니다.

public static TSource Aggregate<TSource>(
  this IEnumerable<TSource> source, 
  Func<TSource, TSource, TSource> func);

public static TAccumulate Aggregate<TSource, TAccumulate>(
  this IEnumerable<TSource> source, 
  TAccumulate seed, 
  Func<TAccumulate, TSource, TAccumulate> func);

public static TResult Aggregate<TSource, TAccumulate, TResult>(
  this IEnumerable<TSource> source, 
  TAccumulate seed, 
  Func<TAccumulate, TSource, TAccumulate> func, 
  Func<TAccumulate, TResult> resultSelector);

Aggregate() 메서드 동작 과정을 글로 표현하기 어려우므로 아래 예제들을 통해 Aggregate() 메서드의 동작 과정을 살펴봅시다.


예제 1. string 타입의 List

다음 예제는 string 타입의 List에서 Aggregate() 메서드를 호출합니다.

class Program
{
  static void Main(string[] args)
  {
    List<string> strList = new List<string>()
    {
      "Java", "C#", "React", "Svelte"
    };

    string linqMethodResult = strList.Aggregate((s1, s2) => {
        Console.WriteLine("s1: " + s1);
        Console.WriteLine("s2: " + s2 + "\n");
        return s1 + " / " + s2;
    });

    Console.WriteLine("메서드 구문 결과: " + linqMethodResult);
  }
}

[실행 결과]

s1: Java
s2: C#

s1: Java / C#
s2: React

s1: Java / C# / React
s2: Svelte

메서드 구문 결과: Java / C# / React / Svelte

동작 과정

Aggregate() 메서드에는 다음 람다식이 작성되어 있습니다.

string linqMethodResult = strList.Aggregate((s1, s2) => {
    Console.WriteLine("s1: " + s1);
    Console.WriteLine("s2: " + s2 + "\n");
    return s1 + " / " + s2;
});

여기서 s1은 컬렉션을 순회하면서 람다식에서 반환되는 값이 누적됩니다. Aggregate() 함수는 문자열 " / "로 구분된 문자열을 반환하며, 다음 내용은 Aggregate() 함수 동작 과정을 단계별로 설명합니다.

 

1단계: 첫 번째 루프에서 s1은 strList의 첫 번째 요소인 "Java"이며, s2는 다음 요소(두 번째 요소)인 "C#"입니다. 람다식에서 반환된 값은 s1에 누적됩니다.

2단계: 1단계 람다식에서 반환된 값이 s1에 누적된 것을 확인할 수 있으며, s2는 세 번째 요소인 "React"입니다. 람다식에서 반환된 값은 s1에 누적됩니다.

3단계: 2단계 람다식에서 반환된 값이 s1에 누적된 것을 확인할 수 있으며, s2는 네 번째 요소인 "Svelte"입니다. 람다식에서 반환된 값은 s1에 누적됩니다.

4단계: s2가 가리키는 요소가 없으므로 Aggregate() 메서드는 누적된 결과인 s1을 반환합니다.


예제 2. int 타입의 List

다음 예제는 int 타입의 List에서 Aggregate() 메서드를 사용하여 모든 요소를 곱한 결과를 구합니다.

class Program
{
  static void Main(string[] args)
  {
    List<int> intList = new List<int>()
    {
      5, 10, 20
    };

    int linqMethodResult = intList.Aggregate((num1, num2) => {
        Console.WriteLine("num1: " + num1);
        Console.WriteLine("num2: " + num2 + "\n");
        return num1 * num2;
    });

    Console.WriteLine("메서드 구문 결과: " + linqMethodResult);
  }
}

 [실행 결과]

num1: 5
num2: 10

num1: 50
num2: 20

메서드 구문 결과: 1000

동작 과정

1단계: 5 * 10 결과를 num1에 할당합니다.

2단계: 1단계의 결과인 50에 20을 곱한 결과를 num1에 할당합니다.

3단계: Aggregate() 메서드는 2단계의 결과인 1000을 반환합니다.


예제 3. seed 매개변수가 있는 Aggregate 메서드

Aggregate() 메서드의 두 번째 오버로드된 버전은 seed 매개변수가 존재한다는 것입니다. 쉽게 이야기하자면, 처음 루프에서 누적되는 값이 데이터 집합의 첫 번째 요소가 아닌 seed 값을 가리킵니다.

 

다음 예제는 seed의 값을 3으로 설정하고 int 타입의 List에서 Aggregate() 메서드를 사용하여 모든 요소를 곱한 결과를 구합니다.

class Program
{
  static void Main(string[] args)
  {
    List<int> intList = new List<int>()
    {
      5, 10, 20
    };

    int linqMethodResult = intList.Aggregate(3, (num1, num2) => {
        Console.WriteLine("num1: " + num1);
        Console.WriteLine("num2: " + num2 + "\n");
        return num1 * num2;
    });

    Console.WriteLine("메서드 구문 결과: " + linqMethodResult);
  }
}

[실행 결과]

num1: 3
num2: 5

num1: 15
num2: 10

num1: 150
num2: 20

메서드 구문 결과: 3000

동작 과정

1단계: num1은 intList의 첫 번째 요소가 아닌 seed값 3입니다. num2는 두 번째 요소가 아닌 첫 번째 요소 5입니다. 3 * 5 결과를 num1에 할당합니다.

2단계: num1은 1단계의 결과인 15이며, num2는 두 번째 요소인 10입니다. 15 * 10 결과를 num1에 할당합니다.

3단계: num1은 2단계의 결과인 150이며, num2는 세 번째 요소인 20입니다. 150 * 20 결과를 num1에 할당합니다.

4단계: Aggregate() 메서드는 3단계의 결과인 3000을 반환합니다.


예제 4. 사용자 정의 클래스 - seed값

다음 예제는 사용자 정의 클래스인 Person 타입의 List에서 Aggregate() 메서드를 사용하여 모든 사람들의 이름을 하나의 문자열로 연결합니다.

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
  public int Money { get; set; }
}

class Program
{
  static void Main(string[] args)
  {
    List<Person> personA = new List<Person>
    {
      new Person{Name ="Bob",  Age = 20, Money = 30000},
      new Person{Name ="Nick", Age = 30, Money = 50000},
      new Person{Name ="Tom",  Age = 40, Money = 80000}
    };

    // 1. 질의 구문(Query Syntax)
    string linqQueryResult = (from   person in personA
                              select person)
                              .Aggregate<Person, string>("Name: ", (person1, person2) => { 
                                  return person1 + person2.Name + " / "; 
                              });

    // 2. 메서드 구문(Method Syntax)
    string linqMethodResult = personA
        .Aggregate<Person, string>("Name: ", (person1, person2) => { 
            return person1 + person2.Name + " / ";
        });

    // 맨 마지막 문자열 " / "을 제거하는 작업
    int queryResultLastIndex = linqQueryResult.LastIndexOf("/");
    linqQueryResult = linqQueryResult.Remove(queryResultLastIndex);

    int methodResultLastIndex = linqMethodResult.LastIndexOf("/");
    linqMethodResult = linqMethodResult.Remove(methodResultLastIndex);

    Console.WriteLine("질의 구문");
    Console.WriteLine("질의 구문 결과: " + linqQueryResult);

    Console.WriteLine("\n메서드 구문");
    Console.WriteLine("메서드 구문 결과: " + linqMethodResult);
  }
}

[실행 결과]

질의 구문
질의 구문 결과: Name: Bob / Nick / Tom

메서드 구문
메서드 구문 결과: Name: Bob / Nick / Tom

위 예제는 seed를 가지는 Aggregate() 메서드를 사용했으며, 꺾새(<>) 내부에 클래스 타입과 반환 타입을 명시했다는 점을 유의해주세요.


예제 5. resultSelector 매개변수가 있는 Aggregate 메서드

resultSelector은 Aggregate() 메서드의 세 번째 오버로드된 버전에 존재하며, 최종 누적 결과에 대해 처리하고자 하는 로직을 작성할 수 있습니다.

 

다음 예제는 예제 4. 사용자 정의 클래스 - seed값에서 마지막 문자열 " / "을 제거하는 로직을 resultSelector을 사용하여 구현합니다.

 

Func 대리자 꺾새(<>) 내부에 resultSelector의 타입이 추가된 점을 유의해주세요. 

public class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
  public int Money { get; set; }
}

class Program
{
  static void Main(string[] args)
  {
    List<Person> personA = new List<Person>
    {
      new Person{Name ="Bob",  Age = 20, Money = 30000},
      new Person{Name ="Nick", Age = 30, Money = 50000},
      new Person{Name ="Tom",  Age = 40, Money = 80000}
    };

    // 1. 질의 구문(Query Syntax)
    string linqQueryResult = (from person in personA
                              select person)
                              .Aggregate<Person, string, string>(
                                "Name: ", // seed값
                                (person1, person2) => { // 누적기 함수
                                    return person1 + person2.Name + " / ";
                                },
                                (person1) => { // 누적된 최종 결과에 대해 처리하고자하는 로직
                                    int LastIndex = person1.LastIndexOf("/");
                                    return person1.Remove(LastIndex);
                                });

    // 2. 메서드 구문(Method Syntax)
    string linqMethodResult = personA
        .Aggregate<Person, string, string>(
            "Name: ", // seed값
            (person1, person2) => { // 누적기 함수 
                return person1 + person2.Name + " / ";
            },
            (person1) => { // 누적된 최종 결과에 대해 처리하고자하는 로직
                int LastIndex = person1.LastIndexOf("/");
                return person1.Remove(LastIndex);
            });

    Console.WriteLine("질의 구문");
    Console.WriteLine("질의 구문 결과: " + linqQueryResult);

    Console.WriteLine("\n메서드 구문");
    Console.WriteLine("메서드 구문 결과: " + linqMethodResult);
  }
}

[실행 결과]

질의 구문
질의 구문 결과: Name: Bob / Nick / Tom

메서드 구문
메서드 구문 결과: Name: Bob / Nick / Tom
반응형

댓글