(C#) 27. 다차원 배열 (multi-array)
타일 맵을 2차원 배열로 만들어 콘솔에 그렸다. GetLength(0)과 (1)이 각각 무엇인지, [y, x] 순서를 헷갈리면 어떻게 되는지, 콘솔 색을 되돌리지 않으면 생기는 문제를 정리했다.
격자를 표현하려면
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[][]는 길이가 다를 수 있는 배열의 배열이다- 맵 데이터와 범위 검사를 한 클래스에 묶으면 쓰는 쪽이 단순해진다
