포스트

(C#) 26. 배열 (array)

같은 타입 여러 개를 묶는 배열과 순회 방법. max를 0으로 초기화해서 생기는 버그, 빈 배열에서 평균을 구할 때 터지는 문제, foreach와 for를 언제 나눠 쓰는지 정리했다.

(C#) 26. 배열 (array)

변수 이름을 다섯 개 만들 수는 없다

점수 다섯 개를 다루려고 score1부터 score5까지 만들면, 평균을 구하는 코드가 다섯 줄이 되고 개수가 바뀌면 전부 고쳐야 한다.

같은 타입 여러 개를 하나로 묶어 번호로 접근하는 게 배열이다.

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using System;

namespace array
{

    class Program
    {
        static int GetHighestScore(int[] scores)
        {
            int max = 0;
            foreach (int score in scores)
            {
                if (max < score) max = score;
            }
            return max;
        }

        static int GetAverageScore(int[] scores)
        {
            int sum = 0;
            foreach(int score in scores)
            {
                sum += score;
            }
            return (int)sum / scores.Length;
        }

        static int GetIndexOf(int[] scores, int value)
        {
            for (int i = 0; i < scores.Length; i++)
            {
                if (value == scores[i]) return i;
            }
            return -1;
        }

        static void Main(string[] args)
        {
            // 베열
            //int[] scores = new int[5] { 10, 20, 30, 40, 50 } ;
            //int[] scores = new int[] { 10, 20, 30, 40, 50 } ;
            int[] scores = { 10, 30, 40, 20, 50 } ;
            
            int maxValue = GetHighestScore(scores);
            int avgValue = GetAverageScore(scores);
            int index = GetIndexOf(scores, 30);

            Console.WriteLine(maxValue);
            Console.WriteLine(avgValue);
            Console.WriteLine(index);

            // 0 1 2 3 4
            //scores[0] = 10;
            //scores[1] = 20;
            //scores[2] = 30;
            //scores[3] = 40;
            //scores[4] = 50;

            //for (int i = 0; i < scores.Length; i++) 
            //{
            //    Console.WriteLine(scores[i]);
            //}

            //foreach(int score in scores)
            //{
            //    Console.WriteLine(score);
            //}
        }
    }
}

실행 결과

선언하는 세 가지 방법

주석에 나란히 있는 셋은 전부 같다.

1
2
3
int[] scores = new int[5] { 10, 20, 30, 40, 50 };   // 크기까지 명시
int[] scores = new int[] { 10, 20, 30, 40, 50 };    // 크기는 추론
int[] scores = { 10, 20, 30, 40, 50 };              // 선언과 동시에만 가능

세 번째는 짧지만 선언과 동시에 초기화할 때만 된다.

1
2
3
int[] scores;
scores = { 10, 20 };          // 컴파일 에러
scores = new int[] { 10, 20 };  // 이렇게 써야 한다

값 없이 크기만 잡으면 전부 0으로 채워진다. string 배열이면 null로 채워진다.

1
int[] scores = new int[5];    // 0, 0, 0, 0, 0

max 를 0으로 시작하면 안 된다

GetHighestScore에 문제가 있다.

1
2
3
4
int max = 0;
foreach (int score in scores)
    if (max < score) max = score;
return max;

점수가 전부 음수면 어떤 값도 0보다 크지 않아서 0이 그대로 반환된다. 배열에 없는 값이 최댓값으로 나온다.

1
2
int[] temps = { -5, -3, -10 };
GetHighestScore(temps);    // 0. 배열에 없는 값이다

점수라서 음수가 없을 거라 가정한 것인데, 함수 이름과 매개변수만 봐서는 그 가정이 안 보인다. 온도나 좌표에 그대로 갖다 쓰면 조용히 틀린다.

첫 원소로 시작해야 한다.

1
2
3
4
5
6
7
8
9
10
static int GetHighestScore(int[] scores)
{
    if (scores == null || scores.Length == 0)
        throw new ArgumentException("빈 배열", nameof(scores));

    int max = scores[0];
    for (int i = 1; i < scores.Length; i++)
        if (max < scores[i]) max = scores[i];
    return max;
}

int.MinValue로 시작하는 방법도 있는데, 빈 배열일 때 그 값이 그대로 나오는 건 여전하다. 빈 배열은 최댓값 자체가 없으니 예외를 던지는 게 맞았다.

빈 배열이면 평균에서 터진다

1
return (int)sum / scores.Length;

scores.Length가 0이면 DivideByZeroException이다. 9편15편에서 계속 나온 “루프가 한 번도 안 도는 경우”가 여기서는 예외로 나타난다.

(int) 캐스팅도 의미가 없다. sum이 이미 int라 아무것도 안 한다. 정수 나눗셈이라 소수점이 버려진다는 점이 오히려 중요하다.

1
2
int[] scores = { 10, 30, 40, 20, 51 };
// 합 151, 개수 5 -> 30.2 인데 30 이 나온다

소수점이 필요하면 어느 한쪽을 실수로 만들어야 한다.

1
2
3
4
5
6
7
static double GetAverageScore(int[] scores)
{
    if (scores.Length == 0) return 0;
    int sum = 0;
    foreach (int score in scores) sum += score;
    return (double)sum / scores.Length;
}

(double)sum에 붙이는 게 중요하다. (double)(sum / scores.Length)라고 쓰면 정수 나눗셈이 먼저 끝나고 그 결과를 실수로 바꾸는 것이라 소용없다.

foreach 와 for

1
2
foreach (int score in scores) { }
for (int i = 0; i < scores.Length; i++) { }

foreach는 인덱스를 안 다루니 범위 실수가 아예 없다. GetIndexOf처럼 몇 번째인지가 필요하면 for를 쓴다.

foreach의 변수는 읽기 전용이다.

1
2
foreach (int score in scores)
    score = 0;      // 컴파일 에러

값을 바꾸려면 for로 인덱스를 통해 접근해야 한다. 값 형식이라 score를 바꿔봐야 복사본만 바뀌기도 한다.

1
2
for (int i = 0; i < scores.Length; i++)
    scores[i] = 0;

배열은 순회 중에 원소 값을 바꿔도 되지만, List는 순회 중 추가나 삭제를 하면 예외가 난다. 그건 28편에서 나온다.

배열은 참조 타입이다

1
2
3
4
static void Reset(int[] arr)
{
    arr[0] = 0;         // 원본이 바뀐다
}

17편class와 같다. 배열 변수에는 주소가 들어 있어서, 함수에 넘겨도 같은 배열을 가리킨다. ref 없이도 내용 수정이 반영된다.

크기는 만들 때 정해지고 나중에 못 바꾼다. 늘려야 하면 새로 만들어 복사하거나 28편List를 쓴다.

1
Array.Resize(ref scores, 10);    // 실제로는 새 배열을 만들어 복사한다

직접 짤 필요가 없는 것들

세 함수 다 표준 라이브러리에 있다.

1
2
3
4
5
using System.Linq;

int max = scores.Max();
double avg = scores.Average();
int index = Array.IndexOf(scores, 30);

Max는 빈 배열에서 예외를 던지고, Averagedouble을 돌려준다. 위에서 고친 내용이 이미 반영되어 있는 셈이다.

직접 짜본 게 의미가 없는 건 아니다. 순회하면서 상태를 갱신하는 패턴은 계속 나오고, 44편 이후 자료구조를 직접 만들 때 필요하다. 다만 실제로 쓸 때는 이미 있는 걸 쓰는 게 맞다.

정리하면

  • 크기만 잡으면 int는 0, 참조 타입은 null로 채워진다
  • { ... } 짧은 초기화는 선언과 동시에만 된다
  • 최댓값을 0으로 시작하면 전부 음수인 배열에서 틀린다. 첫 원소로 시작한다
  • 빈 배열은 최댓값도 평균도 정의되지 않는다. 예외를 던지거나 명시적으로 처리한다
  • 정수끼리 나누면 소수점이 버려진다. 나누기 전에 한쪽을 실수로 만든다
  • 인덱스가 필요하면 for, 아니면 foreach. foreach 변수는 읽기 전용이다
  • 배열은 참조 타입이라 함수에 넘겨도 내용 수정이 반영된다
  • Max, Average, Array.IndexOf가 이미 있다
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.