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


Study/C#  2021. 9. 13. 17:58

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


이 글은 C#의 리스트(List)와 딕셔너리(Dictionary), 그리고 Linq식 사용법에 대한 글입니다.


예전 글에서 제가 배열과 객체 지향 프로그래밍(OOP)에 대해서 설명한 적이 있습니다.

link - [C#] 07. 배열과 리스트

link - [C#] 19. 객체 지향(OOP) 프로그래밍의 4대 원칙(캡슐화, 추상화, 상속, 다형성)


객체 지향에 대해서 간략하게 다시 설명하면 프로그램을 개발할 때 모든 것을 객체(Object)적으로 생각하여 개발을 하는 것을 의미합니다.

즉, 객체라는 것은 프로그램에서 클래스(Class)의 형태로 관리를 하는 것이고 또 이 객체를 효과적으로 관리하기 위해서는 리스트(List)와 딕셔너리(Dictionary)를 자주 사용합니다.


리스트는 자료 구조 알고리즘에서는 연결 리스트 알고리즘이고 딕셔너리(Dictionary)는 맵 알고리즘입니다.

연결 리스트의 알고리즘은 처음부터 끝까지의 데이터를 포인터로 연결한 것이고 딕셔너리는 키와 값으로 연결된 자료구조입니다.

using System;
using System.Collections.Generic;

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));
      }
      // 리스트의 5번째 Node 인스턴스를 취득
      var removeNode = list[5];
      // 리스트에서 제거
      list.Remove(removeNode);
      
      // 리스트의 2번째에 Data가 100인 Node 인스턴스를 삽입
      list.Insert(2, new Node(100));
      
      // list의 값을 순서대로 출력
      foreach (var node in list)
      {
        // 콘솔 출력
        Console.WriteLine(node.Data);
      }
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

리스트에는 0인 값부터 9까지의 Dat를 가진 Node 인스턴스를 순서대로 가지고 있습니다.

리스트는 분명하게 순서가 정해져 있고 5번째의 Node 인스턴스를 가져와서 리스트에서 제거를 했습니다.

그리고 다시 2번째의 리스트에 100의 Data를 가진 Node 인스턴스를 삽입합니다.


이것이 리스트의 특성입니다.


딕셔너리(Dictionary)의 경우는 처음과 끝의 순서가 정해져 있는 것이 아니고 Key가 정해져 있어서 Key로 데이터를 취득해옵니다.

using System;
using System.Collections.Generic;

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 dic = new Dictionary<string, Node>();
      // i가 0부터 9까지
      for (int i = 0; i < 10; i++)
      {
        // 딕셔너리에 카는 "Key" + i의 값으로 데이터를 삽입
        dic.Add("Key" + i, new Node(i));
      }
      // 딕셔너리에서 키가 Key2인 값을 제거
      dic.Remove("Key2");
      // 딕셔너리에 key100가인 데이터를 입력
      dic.Add("key100", new Node(100));
      // 딕셔너리의 Key의 취득해서 개수만큼 반복
      foreach (var key in dic.Keys)
      {
        // 딕셔너리의 Key의 값으로 데이터를 취득
        var node = dic[key];
        // 콘솔 출력
        Console.WriteLine("Key = " + key + ", value = " + node.Data);
      }
      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

딕셔너리에 Key를 통해서 데이터를 입력하고 제거, 출력을 했습니다.

리스트와 다르다는 것은 데이터가 순서대로 있는 것이 아니고 key라는 데이터를 통해서 데이터가 관리되는 것을 확인할 수 있습니다.

참고로 Key의 값은 리스트의 형식으로 순서대로 저장되어 있는 것이 아니라서 Keys로 키의 리스트(?)를 가져올 때 순서대로 가져오지 않는 것을 확인 할 수 있습니다.


C#은 객체 지향 언어(OOP)로써 데이터를 관리하기 위해 리스트와 딕셔너리를 정말 많이 사용합니다.

리스트에서 위 예제처럼 입력, 출력을 할 수 있지만 특정 데이터를 검색하기 위해서는 우리는 반복문을 사용해야 합니다.

using System;
using System.Collections.Generic;

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));
      }
      // 짝수의 값만 다른 리스트로 저장한다.
      var evenList = new List<Node>();
      // list의 값을 순서대로 출력
      foreach (var node in list)
      {
        // node 인스턴스의 Data가 짝수의 경우
        if (node.Data % 2 == 0)
        {
          // 리스트에 저장
          evenList.Add(node);
        }
      }
      // evenList의 값을 순서대로 출력
      foreach (var node in evenList)
      {
        // 콘솔 출력
        Console.WriteLine(node.Data);
      }

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

list의 Data값이 짝수인 데이터를 검색하여 evenList의 리스트에 인스턴스를 넣었습니다.

그리고 evenList를 반복문으로 출력하니 결과는 0포함된 짝수만 출력이 되는 것을 확인할 수 있습니다.


위 예제는 잘못된 소스는 아닙니다만, C#에서는 좀 더 효율적으로 필터, 추출할 수 있는 방법이 있습니다.

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));
      }

      // 짝수의 값만 다른 리스트로 저장한다.
      var evenList = from node in list where node.Data % 2 == 0 select node;
      // evenList의 값을 순서대로 출력
      foreach (var node in evenList)
      {
        // 콘솔 출력
        Console.WriteLine(node.Data);
      }

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

위 데이터 베이스의 쿼리같은 프로그램 식이 있습니다.

from node in list는 list의 각 아이템의 값을 node로 치환하고, where의 조건식으로 node.Data가 짝수로 필터하고 select로 출력하여 새로운 리스트를 생성했습니다.

그리고 foreach로 출력을 해니 결과는 같은 값을 출력합니다.


위와 같은 식을 C#에서는 Linq 쿼리식이라고 합니다.

링크 쿼리식은 위와 같이 필터(where)의 역할도 있지만 정렬과 집합의 식도 있습니다.

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를 재정렬한다.
      var filerList = from node in list orderby node.Data descending group node by node.Data % 2 into g select (key: g.Key, value: g);
      // filerList의 키 순서대로 반복문
      foreach (var item in filerList)
      {
        // 각 키안에 리스트를 출력
        foreach (var value in item.value)
        {
          // 콘솔 출력
          Console.WriteLine("Key : " + item.key + " Value : " + value.Data);
        }
        // 개행
        Console.WriteLine();
      }

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

위 예제에서는 orderby로 리스트를 내림차순(descending)으로 재정렬하고 group by로 1과 0으로 재정렬하고 select로 딕셔너리 형태로 데이터를 만듭니다.

간단하게 Dictionary<int, List<Node>> 형태로 재구성되는 것입니다.


프로그램 설계에 따라 데이터 베이스 쿼리식같이 만들 수도 있지만, 프로그램 함수식으로 작성할 수도 있습니다.

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));
      }
      // 짝수의 값만 다른 리스트로 저장한다.
      var evenList = list.Where(x => x.Data % 2 == 0);
      // evenList의 값을 순서대로 출력
      foreach (var node in evenList)
      {
        // 콘솔 출력
        Console.WriteLine(node.Data);
      }
      // 개행
      Console.WriteLine();

      // 짝수, 홀수 별로 그룹을 나누고 그 키로 Node를 재정렬한다.
      var filerList = list.OrderByDescending(x => x.Data).GroupBy(x => x.Data % 2).Select(x => (key: x.Key, value: x));
      // filerList의 키 순서대로 반복문
      foreach (var item in filerList)
      {
        // 각 키안에 리스트를 출력
        foreach (var value in item.value)
        {
          // 콘솔 출력
          Console.WriteLine("Key : " + item.key + " Value : " + value.Data);
        }
        // 개행
        Console.WriteLine();
      }

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

프로그램의 함수처럼 리스트의 필터를 만들 수도 있습니다.

결과는 역시 위 쿼리식과 같은 결과가 나옵니다.


C#에서는 객체를 리스트(List)와 딕셔너리(Dictionary)로 객체(Object)를 관리하고 Linq의 쿼리식과 함수식을 통해서 데이터를 필터, 분류하여 사용합니다.

Linq 식에는 대표적으로 가장 자주 사용하는 건 where과 select입니다만, 상황에 따라서 두 리스트간의 Join, 중복 데이터 제거(Distinct), 합집합(Union) 등의 기능이 있습니다.

조금 더 자세한 건 다음 글에서 설명하겠습니다.


여기까지 C#의 리스트(List)와 딕셔너리(Dictionary), 그리고 Linq식 사용법에 대한 글이었습니다.


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