포스트

(C#) 27. 다차원 배열 (multi-array)

타일 맵을 2차원 배열로 만들어 콘솔에 그렸다. GetLength(0)과 (1)이 각각 무엇인지, [y, x] 순서를 헷갈리면 어떻게 되는지, 콘솔 색을 되돌리지 않으면 생기는 문제를 정리했다.

(C#) 27. 다차원 배열 (multi-array)

격자를 표현하려면

26편의 배열은 한 줄이다. 게임 맵처럼 가로세로가 있는 건 축이 두 개 필요하다.

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
using System;

namespace multiArray
{
    class Program
    {
        class Map
        {
            int[,] tiles =
            {
                { 1, 1, 1, 1, 1 },
                { 1, 0, 0, 0, 1 },
                { 1, 0, 0, 0, 1 },
                { 1, 0, 0, 0, 1 },
                { 1, 1, 1, 1, 1 }
            };

            public void Render()
            {
                for (int y = 0; y < tiles.GetLength(0); y++) 
                {
                    for (int x = 0; x < tiles.GetLength(1); x++) 
                    {
                        if (tiles[y, x] == 1)
                            Console.ForegroundColor = ConsoleColor.Red;
                        else
                            Console.ForegroundColor = ConsoleColor.Green;

                        Console.Write('\u25cf');
                    }
                    Console.WriteLine();
                }
            }
        }

        static void Main(string[] args)
        {
            Map map = new Map();
            map.Render();
        }
    }
}

실행 결과

소스에 적힌 숫자 배치가 화면에 그대로 나온다. 1이 벽(빨강), 0이 바닥(초록)이다. 코드를 보면서 맵 모양을 알 수 있는 게 이 방식의 장점이다.

Length 가 아니라 GetLength

1차원에서는 scores.Length를 썼는데 여기서는 다르다.

1
2
3
tiles.GetLength(0)   // 5. 행 개수
tiles.GetLength(1)   // 5. 열 개수
tiles.Length         // 25. 전체 칸 수

Length전체 원소 개수다. 루프 조건에 그냥 쓰면 25번 돌아서 범위를 벗어난다.

GetLength(0)이 첫 번째 차원, 즉 [y, x]y 쪽이다. { }로 감싼 줄의 개수가 여기 해당한다.

정사각형 맵이라 둘 다 5여서 바꿔 써도 티가 안 난다. 가로세로가 다른 맵으로 바꾸는 순간 깨진다. 처음부터 맞춰 쓰는 게 낫다.

[y, x] 순서

1
tiles[y, x]

첫 번째가 행(y), 두 번째가 열(x)이다. 초기화 블록의 생김새와 맞는다.

1
2
{ 1, 1, 1, 1, 1 },   // y = 0
{ 1, 0, 0, 0, 1 },   // y = 1

한 줄이 하나의 y이고 그 안의 원소가 x다.

수학에서 좌표를 (x, y)로 쓰는 데 익숙하면 계속 헷갈린다. 실제로 tiles[x, y]로 잘못 써도 정사각형 맵에서는 컴파일도 되고 실행도 된다. 맵이 상하로 뒤집혀 나올 뿐이라 대칭인 맵에서는 눈치채지 못한다.

가로세로가 다른 맵을 한 번 만들어보면 바로 드러난다. IndexOutOfRangeException이 나거나 모양이 완전히 깨진다.

바깥 루프가 y, 안쪽이 x인 것도 이유가 있다. 콘솔은 한 줄씩 그리니 y가 바깥이어야 한다. 13편에서 본 “바깥 루프가 무엇으로 묶을지 정한다”가 그대로 적용된다.

콘솔 색을 되돌리지 않으면

1
Console.ForegroundColor = ConsoleColor.Red;

이건 콘솔 전체의 상태를 바꾼다. Render가 끝나도 마지막에 설정한 색이 남는다. 그 뒤에 찍는 모든 출력이 초록색으로 나오고, 프로그램이 끝난 뒤 터미널까지 색이 남는 경우도 있다.

1
2
3
4
5
public void Render()
{
    // ...
    Console.ResetColor();     // 원래대로
}

13편setw 이야기와 같은 부류다. 전역 상태를 바꿨으면 되돌려야 한다. 예외가 날 수 있는 코드면 try/finally에 넣는 게 확실하다.

‘\u25cf’ 가 안 보일 수 있다

'\u25cf'는 검은 원(●) 문자를 유니코드 코드 포인트로 쓴 것이다. 소스 파일 인코딩에 안 휘둘려서 이 표기가 안전하다.

콘솔 폰트가 이 글자를 갖고 있어야 보인다. 없으면 네모나 물음표로 나온다. 윈도우 기본 콘솔에서 폰트를 바꾸면 깨지는 경우가 있었다.

한글이 섞이면 C++의 setw 편에서 본 것과 같은 문제가 생긴다. 한글이나 전각 문자는 폭이 2칸이라 격자가 어긋난다. 맵을 그릴 때는 반각 문자만 쓰는 게 안전했다.

다차원 배열과 가변 배열

C#에는 두 가지가 있다. 생김새가 비슷한데 다르다.

1
2
int[,] rect = new int[3, 5];      // 다차원 배열. 직사각형
int[][] jag = new int[3][];       // 가변 배열. 배열의 배열

가변 배열은 줄마다 길이가 달라도 된다.

1
2
jag[0] = new int[5];
jag[1] = new int[2];    // 길이가 달라도 된다

접근 방법도 다르다. rect[y, x]jag[y][x]다.

메모리 배치가 다르다는 게 중요하다. int[,]는 25개가 한 덩어리로 붙어 있고, int[][]는 배열 5개가 각자 다른 곳에 흩어져 있고 그 주소를 모은 배열이 따로 있다.

직사각형 격자라면 int[,]가 자연스럽다. 소스 모양이 맵 모양과 같아서 읽기 좋고, 메모리도 한 덩어리다.

다만 성능만 놓고 보면 의외의 결과가 있다. C# 런타임이 1차원 배열 접근은 아주 잘 최적화하는데 다차원 배열은 그만큼은 아니라서, 접근이 아주 많은 코드에서는 int[][]가 더 빠른 경우가 있다. 맵 렌더링 정도로는 차이가 안 나서 신경 쓸 필요는 없었다.

클래스 안에 필드로 둔 것

1
2
3
4
5
class Map
{
    int[,] tiles = { ... };
    public void Render() { }
}

맵 데이터와 그리는 기능이 한 클래스에 묶여 있다. 17편에서 본 객체의 형태다.

tiles에 접근 지정자가 없어서 private이다(21편에서 본 기본값). 밖에서는 맵 내부를 모르고 Render()만 부른다. 나중에 저장 방식을 바꿔도 쓰는 쪽은 안 바뀐다.

여기에 벽 판정 같은 걸 더하면 맵다워진다.

1
2
3
4
5
6
public bool CanGo(int y, int x)
{
    if (y < 0 || y >= tiles.GetLength(0)) return false;
    if (x < 0 || x >= tiles.GetLength(1)) return false;
    return tiles[y, x] == 0;
}

범위 검사를 맵 안에 두면 밖에서 매번 안 해도 된다. 이 구조가 40편 이후 미로 생성과 길찾기에서 계속 쓰인다.

정리하면

  • Length는 전체 개수다. 차원별 길이는 GetLength(0), GetLength(1)
  • [y, x] 순서다. 정사각형 맵에서는 뒤집어 써도 안 걸리니 직사각형으로 한 번 확인해본다
  • 콘솔 색은 전역 상태다. 끝나면 ResetColor로 되돌린다
  • 유니코드 문자는 폰트에 따라 안 보일 수 있고, 전각 문자는 폭이 2라 격자가 어긋난다
  • int[,]는 직사각형 한 덩어리, int[][]는 길이가 다를 수 있는 배열의 배열이다
  • 맵 데이터와 범위 검사를 한 클래스에 묶으면 쓰는 쪽이 단순해진다
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.