여러 값을 하나의 이름으로 다루기
지금까지는 age, name, score처럼 값 하나를 변수 하나에 담았다. 하지만 실제 프로그램은 보통 값 하나만 다루지 않는다. 학생 여러 명의 점수, 쇼핑몰 장바구니의 상품 목록, 회원 이름 목록, 메뉴 이름과 가격처럼 여러 데이터를 함께 다룬다.
이때 사용하는 대표적인 도구가 배열과 컬렉션이다.
배열: 같은 자료형의 값을 정해진 개수만큼 담는 구조
List<T>: 값의 개수가 실행 중에 늘거나 줄 수 있는 목록Dictionary<TKey, TValue>: 키와 값을 연결해 빠르게 찾는 구조foreach: 배열과 컬렉션을 읽기 쉽게 순회하는 반복문
여기서 T는 아직 깊게 알 필요는 없다. "어떤 자료형이 들어갈 자리"라고 생각하면 된다. 예를 들어 List<string>은 문자열 목록이고, List<int>는 정수 목록이다.
AI 생성 이미지
배열 선언과 인덱스 접근
배열은 같은 자료형의 값을 여러 개 담는다. 배열을 만들 때는 자료형 뒤에 []를 붙인다.
string[] names = { "민수", "지아", "현우" };
Console.WriteLine(names[0]);
Console.WriteLine(names[1]);
Console.WriteLine(names[2]);실행 결과:
민수
지아
현우배열의 위치 번호를 인덱스라고 한다. C#의 인덱스는 1이 아니라 0부터 시작한다.
names[0] → 첫 번째 값
names[1] → 두 번째 값
names[2] → 세 번째 값배열에 몇 개의 값이 들어 있는지는 Length로 확인한다.
int[] scores = { 90, 75, 88, 100 };
Console.WriteLine(scores.Length);실행 결과:
4배열은 크기가 정해져 있다. 위 배열은 정수 4개를 담는 배열이다. 실행 중에 다섯 번째 점수를 바로 추가할 수는 없다. 물론 더 큰 배열을 새로 만들어 값을 옮길 수는 있지만, 단순히 목록을 계속 늘리고 줄이는 용도라면 뒤에서 볼 List<T>가 더 편하다.
반복문으로 배열 순회하기
배열은 반복문과 함께 쓸 때 힘을 발휘한다. 점수 4개의 합계를 구한다고 해 보자.
int[] scores = { 90, 75, 88, 100 };
int sum = 0;
for (int i = 0; i < scores.Length; i++)
{
sum += scores[i];
}
double average = (double)sum / scores.Length;
Console.WriteLine($"합계: {sum}");
Console.WriteLine($"평균: {average:F1}");실행 결과:
합계: 353
평균: 88.3여기서 중요한 부분은 조건식이다.
i < scores.Length배열의 마지막 인덱스는 Length가 아니라 Length - 1이다. 그래서 i <= scores.Length라고 쓰면 범위를 벗어난다.
앞 장에서 배운 메서드와 연결하면 계산 코드를 더 깔끔하게 분리할 수 있다.
int[] scores = { 90, 75, 88, 100 };
Console.WriteLine($"평균: {Average(scores):F1}");
static double Average(int[] values)
{
int sum = 0;
foreach (int value in values)
{
sum += value;
}
return (double)sum / values.Length;
}실행 결과:
평균: 88.3배열 자체를 메서드의 매개변수로 넘길 수 있다. 그러면 여러 값을 하나의 묶음으로 전달할 수 있다. 실제 프로그램에서는 빈 배열이 들어올 가능성도 생각해야 하지만, 여기서는 값이 들어 있는 배열을 다룬다고 가정하자.
foreach로 더 읽기 쉽게 순회하기
for 문은 인덱스가 필요할 때 좋다. 하지만 "처음부터 끝까지 값을 하나씩 꺼내기만" 한다면 foreach가 더 읽기 쉽다.
string[] names = { "민수", "지아", "현우" };
foreach (string name in names)
{
Console.WriteLine($"안녕하세요, {name}님!");
}실행 결과:
안녕하세요, 민수님!
안녕하세요, 지아님!
안녕하세요, 현우님!foreach는 컬렉션에서 값을 하나씩 꺼내 변수에 담아 준다.
foreach (자료형 변수명 in 컬렉션)
{
// 변수명을 사용해 현재 값을 처리
}인덱스가 필요 없다면 foreach를 먼저 떠올리자. 범위 초과 실수를 줄일 수 있고, 코드의 의도도 분명해진다.
string은 문자 목록처럼 보이지만 불변이다
string은 문자열이다. 문자열은 여러 문자로 이루어져 있으므로 배열처럼 인덱스로 접근할 수 있다.
string word = "CSharp";
Console.WriteLine(word[0]);
Console.WriteLine(word[1]);
Console.WriteLine(word.Length);실행 결과:
C
S
6문자 하나씩 반복할 수도 있다.
string word = "CSharp";
foreach (char ch in word)
{
Console.WriteLine(ch);
}실행 결과:
C
S
h
a
r
p하지만 string은 배열과 다르다. 문자열은 불변 객체다. 불변이라는 말은 한 번 만들어진 문자열의 내용이 바뀌지 않는다는 뜻이다.
예를 들어 다음 코드는 가능하지 않다.
string word = "CSharp";
// word[0] = 'B'; // 오류문자열을 바꾸는 것처럼 보이는 메서드도 실제로는 새 문자열을 만든다.
string word = "CSharp";
string upper = word.ToUpper();
Console.WriteLine(word);
Console.WriteLine(upper);실행 결과:
CSharp
CSHARPToUpper()는 원래 문자열을 직접 바꾸지 않는다. 대문자로 바뀐 새 문자열을 만들어 돌려준다. 그래서 결과를 사용하려면 변수에 다시 담아야 한다.
word = word.ToUpper();문자열이 불변이면 안전하다는 장점이 있다. 여러 곳에서 같은 문자열을 사용해도 어느 한 곳에서 몰래 내용이 바뀌지 않는다. 대신 반복문 안에서 문자열을 계속 이어 붙이는 코드는 비효율적일 수 있다. 많은 문자열 조합은 나중에 StringBuilder 같은 도구로 다룰 수 있다.
List<T>로 늘어나는 목록 관리하기
배열은 크기가 고정되어 있다. 반면 List<T>는 값을 추가하거나 제거할 수 있는 목록이다.
List<string> todos = new();
todos.Add("C# 복습하기");
todos.Add("연습 문제 풀기");
todos.Add("커밋하기");
Console.WriteLine($"할 일 개수: {todos.Count}");
foreach (string todo in todos)
{
Console.WriteLine($"- {todo}");
}실행 결과:
할 일 개수: 3
- C# 복습하기
- 연습 문제 풀기
- 커밋하기배열은 개수를 Length로 확인하지만, List<T>는 Count를 사용한다.
Console.WriteLine(todos.Count);List<T>도 인덱스로 접근할 수 있다.
Console.WriteLine(todos[0]);값을 제거할 수도 있다.
todos.Remove("연습 문제 풀기");
foreach (string todo in todos)
{
Console.WriteLine(todo);
}실행 결과:
C# 복습하기
커밋하기Remove는 일치하는 값을 찾아 제거한다. 같은 값이 여러 번 들어 있다면 앞에서부터 처음 만난 값 하나가 제거된다. 값의 개수가 정해져 있고 잘 바뀌지 않으면 배열을 써도 된다. 실행 중에 계속 추가되거나 제거되는 목록이라면 List<T>가 더 자연스럽다.
Dictionary<TKey, TValue>로 빠르게 찾기
배열과 리스트는 순서대로 값을 담는다. 하지만 어떤 경우에는 "이름으로 가격 찾기", "아이디로 사용자 찾기"처럼 특정 키로 값을 찾고 싶다. 이때 Dictionary<TKey, TValue>를 사용한다.
Dictionary<string, int> prices = new();
prices["커피"] = 3000;
prices["라떼"] = 4000;
prices["주스"] = 3500;
Console.WriteLine(prices["라떼"]);실행 결과:
4000Dictionary<string, int>는 키가 string, 값이 int인 사전이다. 여기서는 메뉴 이름으로 가격을 찾는다.
딕셔너리에서 같은 키에 값을 다시 넣으면 기존 값이 새 값으로 바뀐다.
prices["커피"] = 3200;위 코드는 "커피"의 가격을 3000에서 3200으로 바꾼다.
없는 키를 바로 꺼내면 오류가 발생한다. 그래서 사용자 입력처럼 확실하지 않은 키는 TryGetValue로 확인하는 편이 안전하다.
Dictionary<string, int> prices = new()
{
["커피"] = 3000,
["라떼"] = 4000,
["주스"] = 3500
};
string menu = "차";
if (prices.TryGetValue(menu, out int price))
{
Console.WriteLine($"{menu} 가격은 {price}원입니다.");
}
else
{
Console.WriteLine($"{menu}는 메뉴에 없습니다.");
}실행 결과:
차는 메뉴에 없습니다.딕셔너리도 foreach로 순회할 수 있다.
foreach (var item in prices)
{
Console.WriteLine($"{item.Key}: {item.Value}원");
}실행 결과 예:
커피: 3000원
라떼: 4000원
주스: 3500원여기서 item.Key는 키, item.Value는 값이다. var는 오른쪽 값의 자료형을 컴파일러가 추론하게 한다. 딕셔너리 항목의 정확한 타입 이름은 길기 때문에 이런 곳에서는 var가 코드를 읽기 쉽게 만든다.
다만 딕셔너리는 "순서대로 처리하기 위한 목록"이라기보다 "키로 값을 찾기 위한 구조"에 가깝다. 따라서 딕셔너리를 순회할 때는 특정 출력 순서에 지나치게 의존하지 않는 편이 좋다. 순서가 중요한 목록이라면 배열이나 List<T>가 더 어울린다.
흔한 실수
첫째, 배열이나 리스트의 마지막 인덱스를 착각하기 쉽다.
int[] numbers = { 10, 20, 30 };
// Console.WriteLine(numbers[3]); // 오류값은 3개지만 마지막 인덱스는 2다.
둘째, 배열에는 Length, 리스트와 딕셔너리에는 Count를 사용한다.
int[] scores = { 90, 80 };
List<string> names = new() { "민수", "지아" };
Dictionary<string, int> prices = new()
{
["커피"] = 3000,
["라떼"] = 4000
};
Console.WriteLine(scores.Length);
Console.WriteLine(names.Count);
Console.WriteLine(prices.Count);셋째, 문자열 메서드가 원본을 바꾼다고 생각하면 안 된다.
string text = "hello";
text.ToUpper();
Console.WriteLine(text);실행 결과:
hello대문자 결과를 쓰려면 text = text.ToUpper();처럼 다시 저장해야 한다.
넷째, 딕셔너리에서 없는 키를 바로 읽으면 오류가 난다. 확실하지 않은 키는 ContainsKey나 TryGetValue로 확인하자. 값을 바로 사용할 목적이라면 보통 TryGetValue가 더 자연스럽다.
다섯째, foreach로 순회하는 중에 같은 컬렉션의 구조를 바꾸면 문제가 생길 수 있다. 예를 들어 리스트를 돌면서 동시에 항목을 삭제하는 방식은 피하자. 삭제가 필요하면 삭제할 항목을 따로 모아 두거나, 인덱스를 사용하는 for 문으로 조심해서 처리한다.
작은 연습 문제
int[] scores = { 70, 95, 82, 100, 60 };배열을 만들고foreach로 합계와 평균을 출력해 보자.문자열 하나를 변수에 담고,
foreach로 문자 수를 세어 보자. 공백도 문자로 세어진다는 점을 확인해 보자.List<string>으로 장보기 목록을 만들자. 항목 3개를 추가한 뒤 하나를 제거하고 남은 목록을 출력해 보자.Dictionary<string, int>로 과일 이름과 가격을 저장하자. 사용자가 입력한 과일이 있으면 가격을 출력하고, 없으면 "등록되지 않은 과일입니다."라고 출력해 보자.
이번 장의 핵심은 여러 값을 하나의 구조로 묶는 것이다. 배열은 고정된 목록, List<T>는 변하는 목록, Dictionary<TKey, TValue>는 키로 찾는 목록에 적합하다. 여기에 foreach를 함께 사용하면 여러 데이터를 안전하고 읽기 쉽게 탐색할 수 있다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.