[C#] 29. Linq 함수식을 사용하는 방법


Study/C#  2021. 9. 15. 15:41

안녕하세요. 명월입니다.


이 글은 C#의 Linq 함수식을 사용하는 방법에 대한 글입니다.


이전에 Linq 식에 대한 간략한 설명과 Linq 쿼리식에 대해서 설명한 적이 있습니다.

링크 - [C#] 27. 리스트(List)와 딕셔너리(Dictionary), 그리고 Linq식 사용법

링크 - [C#] 28. Linq 쿼리식을 사용하는 방법


Linq 식에 대해서는 프로그램 내의 객체(Object)를 효과적으로 필터, 검색해서 데이터를 분류하는 문법입니다.

쿼리식의 경우는 SQL 쿼리처럼 작성을 해서 데이터를 분류하는 방법입니다.

쿼리식의 장점은 SQL 쿼리식이 익숙한 분에게는 이러한 문법이 쉽게 접근할 수 있지만, 조금 프로그램 문법과는 이질감이 있고 중간의 디버그 상태를 확인 함에 어려움이 있어서 사용하기를 권장하지는 않습니다.


그래서 Linq 식에는 프로그램 함수와 같은 Linq 함수식이 있습니다.

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

namespace Example
{
  // 예제 클래스
  class Node
  {
    // 값을 생성자로만 입력한다.
    public Node(int data)
    {
      // 프로퍼티 Data에 값을 입력
      this.Data = data;
    }
    // Data 프로퍼티
    public int Data
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 선언(리스트의 객체는 Node 클래스)
      var list = new List<Node>();
      // i가 0부터 9까지
      for (int i = 0; i < 10; i++)
      {
        // 리스트에 데이터를 삽입
        list.Add(new Node(i));
      }
      // Node 클래스의 Data 값이 5 초과된 인스턴스를 List<int> 형식으로 분류한다.
      var filterList = list.Where(x => x.Data > 5).Select(x => x.Data);
      // 필터된 리스트를 반복문으로 추출
      foreach (var node in filterList)
      {
        // 콘솔 출력
        Console.WriteLine(node);
      }

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위의 소스에서는 쿼리식의 from node in list where node.Data > 5 select node.Data;를 list.Where(x => x.Data > 5).Select(x => x.Data);로 표현했습니다.

여기서 Where 함수와 Select 함수의 안에서는 람다식을 사용해서 필터와 검색 조건을 생성했습니다.

그리고 Where 함수의 반환 값과 Select 함수의 반환값은 IEnumerable이 되기 때문에 마치 함수를 연결하는 것처럼 체인 패턴으로 생성이 가능합니다.


즉, 위처럼 체인 형식이 아닌 한 줄마다 함수를 호출하는 것도 가능합니다. (참고, Select의 경우는 Return 값에 따른 제네이레이터 타입이 바뀌기 때문에 주의)


기본적은 Linq 함수식의 경우는 Where과 Select를 가장 많이 사용합니다만, 사양에 따라서 Joinr과 정렬, 그룹별 분리도 가능합니다.

First, FirstOrDefault, Single, SingleOrDefault, Last, LastOrDefault

위 함수는 리스트의 결과를 한 개의 결과를 받을 경우 사용하게 됩니다. 최종 반환 값은 List가 아닌 List의 제네레이터 타입에 의한 결과를 반환합니다.

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

namespace Example
{
  // 예제 클래스
  class Node
  {
    // 값을 생성자로만 입력한다.
    public Node(int data)
    {
      // 프로퍼티 Data에 값을 입력
      this.Data = data;
    }
    // Data 프로퍼티
    public int Data
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 선언(리스트의 객체는 Node 클래스)
      var list = new List<Node>();
      // i가 0부터 9까지
      for (int i = 0; i < 10; i++)
      {
        // 리스트에 데이터를 삽입
        list.Add(new Node(i));
      }
      // list에서 6이상의 데이터를 분류
      var where = list.Where(x => x.Data > 5);
      // list에서 가장 첫번째 데이터 추출
      Node first = where.First();
      // list에서 가장 마지막 데이터 추출
      Node last = where.Last();
      // 콘솔 출력
      Console.WriteLine("First - " + first.Data);
      Console.WriteLine("Last - " + last.Data);
      // 위 6이상의 검색 절에서 7과 일치하는 데이터 분류
      where = where.Where(x => x.Data == 7);
      // list에서 데이터라 하나라면 추출(2개 이상이면 Exception)
      Node single = where.Single();
      // 콘솔 출력
      Console.WriteLine("Single - " + single.Data);
      // 위 6이상의 검색 절 & 7과 일치하는 데이터에서 10과 일치하는 데이터 분류.(반드시 분류되는 데이터는 없다.)
      where = where.Where(x => x.Data == 10);
      // list에서 데이터가 하나라면 추출, 없으면 null를 리턴
      Node default1 = where.SingleOrDefault();
      // 콘솔 출력
      Console.WriteLine("Default - " + (default1 == null ? "Null" : "Not null"));

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

First, Last의 경우는 리스트에서 첫번째 값과 마지막 값을 반환하는 함수식입니다. 그러나 데이터가 없으면 Exception을 발생합니다.

그러나 여기에 orDefault가 붙게 되면, 즉, FirstOrDefault, LastOrDefault이면 검색된 데이터가 없으면 null를 리턴합니다. 에러가 발생하지 않습니다.

Single의 함수는 리스트의 결과가 하나일 경우, 리턴을 하는 함수식입니다. 만약 결과 값이 두개 이상이라면 에러를 발생하게 됩니다.

SingleOrDefault의 함수도 만약 결과가 없으면 null를 리턴하게 됩니다.

OrderBy, OrderByDescending

데이터를 정렬하는 함수입니다.

OrderBy의 경우는 오림차순, OrderByDescending의 경우는 내림차순의 정렬을 하게 됩니다.

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

namespace Example
{
  // 예제 클래스
  class Node
  {
    // 값을 생성자로만 입력한다.
    public Node(int data)
    {
      // 프로퍼티 Data에 값을 입력
      this.Data = data;
    }
    // Data 프로퍼티
    public int Data
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 선언(리스트의 객체는 Node 클래스)
      var list = new List<Node>();
      // i가 0부터 9까지
      for (int i = 0; i < 10; i++)
      {
        // 리스트에 데이터를 삽입
        list.Add(new Node(i));
      }
      // list에서 6이상의 데이터를 분류
      var where = list.Where(x => x.Data > 5);
      // 내림차순으로 정렬
      where = where.OrderByDescending(x => x.Data);
      // 필터된 리스트를 반복문으로 추출
      foreach (var node in where)
      {
        // 콘솔 출력
        Console.WriteLine(node.Data);
      }

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

Select, SelectMany

Select는 데이터를 치환하는 것이고, SelectMany는 두개의 리스트를 합치는 역할을 합니다.

여기서 합친다는 것은 리스트의 Join을 이야기 하는 것이 아니고 A의 리스트가 2개가 있고 B의 리스트가 두개가 있으면 총 6개의 리스트를 만든다는 뜻입니다.

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

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 선언
      var list1 = new List<String>();
      var list2 = new List<int>();
      // list1은 문자형
      list1.Add("1");
      list1.Add("2");
      // list2은 정수형
      list2.Add(3);
      list2.Add(4);
      list2.Add(5);

      // list1를 Select로 정수형으로 치환하고 SelectMany로 두개의 리스트를 합친다.
      var ret = list1.Select(x => Convert.ToInt32(x)).SelectMany(x => list2, (x, y) => x + " * " + y + " = " + (x * y));
      foreach (var i in ret)
      {
        // 콘솔 출력
        Console.WriteLine(i);
      }

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

Join

Join은 두개의 리스트를 합치는 함수입니다.

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

namespace Example
{
  // 예제 클래스
  class Node
  {
    // 값을 생성자로만 입력한다.
    public Node(int key, string value)
    {
      // 프로퍼티에 값을 입력
      this.Key = key;
      this.Value = value;
    }
    // Key 프로퍼티
    public int Key
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
    // Value 프로퍼티
    public string Value
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 선언(리스트의 객체는 Node 클래스)
      var list1 = new List<Node>();
      var list2 = new List<Node>();
      // i가 0부터 9까지
      for (int i = 0; i < 10; i++)
      {
        // 리스트에 데이터를 삽입
        list1.Add(new Node(i, "List1:" + i));
        list2.Add(new Node(i, "List2:" + i));
      }
      // Join 함수의 파라미터는 합칠 리스트, list1의 대한 비교값, list2의 대한 비교값, Join된 결과 리턴값
      var ret = list1.Join(list2.Where(x => x.Key > 5), x => x.Key, y => y.Key, (x, y) => x.Value + " " + y.Value);
      // 결과 출력
      foreach (var item in ret)
      {
        Console.WriteLine(item);
      }

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

Group

리스트를 Group 별로 나누는 함수입니다.

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

namespace Example
{
  // 예제 클래스
  class Node
  {
    // 값을 생성자로만 입력한다.
    public Node(int key, string value)
    {
      // 프로퍼티에 값을 입력
      this.Key = key;
      this.Value = value;
    }
    // Key 프로퍼티
    public int Key
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
    // Value 프로퍼티
    public string Value
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
    // 클래스의 String을 리턴
    public override string ToString()
    {
      return "Key - " + Key + " Value - " + Value;
    }
  }
  // Group 된 클래스
  class Group
  {
    // 값을 생성자로만 입력한다.
    public Group(int key, IEnumerable<Node> value)
    {
      // 프로퍼티에 값을 입력
      this.Key = key;
      this.Value = value;
    }
    // Key 프로퍼티
    public int Key
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
    // Value 프로퍼티
    public IEnumerable<Node> Value
    {
      // 입력은 생성자로만 받는다.
      get; private set;
    }
    // 클래스의 String을 리턴
    public override string ToString()
    {
      // 그룹으로 된 value 데이트의 String 값을 모음
      var sb = new StringBuilder();
      foreach (var item in Value)
      {
        sb.AppendLine(item.ToString());
      }
      return sb.ToString();
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 선언(리스트의 객체는 Node 클래스)
      var list = new List<Node>();
      // i가 0부터 9까지
      for (int i = 0; i < 10; i++)
      {
        // 리스트에 데이터를 삽입
        list.Add(new Node(i, "List:" + i));
      }
      // 짝수, 홀수 별로 그룹을 나누고 그 키로 Node를 재정렬한다.
      var filerList = list.GroupBy(x => x.Key % 2, x => x).Select(x => new Group(x.Key, x.ToList()));
      // filerList의 키 순서대로 반복문
      foreach (var item in filerList)
      {
        // 콘솔 출력
        Console.WriteLine("Group Key - " + item.Key);
        Console.WriteLine(item.ToString());
      }

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

Sum, Average, Max, Min

Linq 함수식에는 쿼리식에서 없는 함수가 존재하는 데, 그것이 Sum, Average, Max, Min입니다. Sum은 리스트의 결과 합, Average는 평균, Max는 큰 값, Min은 작은 값을 나타냅니다.

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

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 리스트 선언
      var list = new List<int>();
      // i가 0부터 9까지
      for (int i = 0; i < 10; i++)
      {
        // 리스트에 데이터를 삽입
        list.Add(i);
      }
      // 리스트 중 가장 큰 값구하기
      var max = list.Max(x => x);
      // 리스트 중 가장 작은 값구하기
      var min = list.Min(x => x);
      // 리스트의 합을 구하기
      var sum = list.Sum(x => x);
      // 리스트의 평균 값을 구하기
      var avg = list.Average(x => x);
      // 콘솔 출력
      Console.WriteLine(max);
      Console.WriteLine(min);
      Console.WriteLine(sum);
      Console.WriteLine(avg);

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

Linq 함수식은 쿼리식에 비해서 일단 소스 가독성이 좋습니다.

그리고 모든 함수식은 IEnumerable 인터페이스로 리턴을 하기 때문에 체인 패턴식으로 한 스탭에 여러 함수를 호출할 수도 있습니다.

C#의 대표적인 ORM인 Entity 프레임워크도 Linq의 함수식을 상속받아서 사용하기 때문에 여러 사양에서도 손쉽게 사용할 수 있는 장점이 있습니다.


그러나 Linq 식이라는 것은 역시 함수 자체의 알고리즘으로는 매우 효율적이나 사용 방법이나 사양에 따라 성능을 저하시킬 수 있으니 사용함에 따라 주의를 해야합니다.

예를 들면, 중복 필터라던가 하나의 조건식으로 처리할 것을 여러 Where과 Select를 나누는 것은 좋지 않습니다.


여기까지 C#의 Linq 함수식을 사용하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.