Visual Basic · 기본
Visual Basic과 .NET 입문
컬렉션과 제네릭 - List(Of T)·Dictionary
List(Of T)·Dictionary(Of K, V)·HashSet, 컬렉션 초기화, 순회 중 수정 오류
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서 비품의 공통 기능을 상속과 인터페이스로 표현했다. 비품 하나를 표현할 수 있게 되면, 여러 비품을 모아 관리하는 방법이 필요해진다. 사무실에 새 노트북이 들어오고 오래된 프로젝터가 빠지는 동안에도 목록을 유지해야 하기 때문이다. 이 장에서는 개수가 바뀌는 목록, 이름으로 찾는 수량표, 중복 없는 신청자 명단을 각각 알맞은 컬렉션으로 만든다.
컬렉션(collection)은 여러 값을 모아 두고 추가, 검색, 삭제 등의 작업을 제공하는 자료 구조다. 배열도 여러 값을 담지만, 여기서는 항목을 넣고 빼는 일을 더 편하게 표현하는 형식을 사용한다. 예제는 컬렉션의 동작에 집중하도록 비품 이름을 문자열로 표현한다. 앞 장에서 만든 비품 클래스도 같은 방식으로 컬렉션에 담을 수 있다.
- 제네릭의 형식 인수가 무엇인지 설명하고 List(Of String)을 선언한다.
- List(Of T), Dictionary(Of K, V), HashSet(Of T)을 작업 목적에 맞게 고른다.
- 컬렉션 초기화 구문으로 시작 데이터를 한곳에 모은다.
- 키가 없는 조회와 중복 추가를 예상하고 결과를 확인한다.
- 순회 중 목록을 수정할 때 생기는 오류를 이해하고 뒤에서부터 삭제한다.
문제 상황
작은 사무실에서 비품 대여 기록을 종이로 관리하고 있다고 하자. 노트북 두 대와 프로젝터 한 대가 대여 중이고, 반납 담당자는 오늘 들어온 노트북 두 대의 기록을 지워야 한다. 비품별로 남아 있는 수량도 확인해야 하며, 다음 대여를 기다리는 신청자 명단에는 같은 사람이 두 번 들어가면 안 된다.
모든 정보를 하나의 배열로 처리하면 각 작업의 뜻이 흐려진다. 대여 기록은 같은 비품 이름이 여러 번 나와도 된다. 노트북 두 대를 빌렸다면 “노트북” 기록도 두 개다. 반면 수량표에서 “노트북”이라는 이름은 하나여야 하며, 그 이름에 대응하는 수량만 바뀐다. 신청자 명단은 같은 이름을 다시 받아도 인원이 늘어나지 않아야 한다.
따라서 세 가지 질문을 나누어 생각한다. “몇 번째 기록인가”가 중요하면 목록을, “이 이름의 수량은 얼마인가”가 중요하면 사전을, “이미 등록되어 있는가”가 중요하면 집합을 사용한다. 같은 문자열을 담더라도 허용하는 중복과 찾는 방식이 다르다.
이 장의 프로그램은 시작 데이터를 만들고 목록을 출력한 뒤, 노트북 반납을 처리하고 결과를 보여 준다. 사용자 입력을 기다리지 않으므로 실행할 때마다 같은 결과를 얻는다. 실제 대여 관리에서는 비품 이름 대신 각 기기의 고유 번호를 사용해야 하지만, 여기서는 컬렉션의 차이를 관찰하기 위해 이름과 수량만 사용한다.
제네릭과 컬렉션 초기화
제네릭(generic)은 담을 값의 형식을 지정하여 사용할 수 있는 형식을 말한다. List(Of T)의 T는 특정한 값이 아니라 형식을 넣을 자리다. 문자열 목록은 List(Of String), 정수 목록은 List(Of Integer)로 선언한다. 앞 장의 비품 클래스 이름이 Equipment라면 List(Of Equipment)도 만들 수 있다.
Of String을 지정한 목록에 정수를 넣으려 하면 Option Strict On 환경에서는 해당 형식에 맞지 않는 추가가 컴파일 오류가 된다. 저장할 형식을 처음부터 정해 두면, 꺼낸 값을 사용할 때도 그 형식을 알 수 있다. 여러 형식을 아무렇게나 담고 나중에 구별하는 방식보다 코드의 의도가 분명하다.
Dim names As New List(Of String)()
names.Add("노트북")
names.Add("프로젝터")
Dim quantities As New List(Of Integer) From {2, 1}
New는 컬렉션 객체를 만든다. 빈 목록을 만든 뒤 Add로 값을 넣어도 되고, From 뒤의 중괄호에 시작 항목을 적어도 된다. 이 From은 컬렉션에 초기 항목을 추가하는 구문이다. 다음 장에서 다룰 쿼리 식의 From과는 사용되는 문맥이 다르다. 여기서는 데이터를 검색하거나 걸러 내는 식을 쓰지 않는다.
선언만 하고 객체를 만들지 않은 변수는 사용할 목록을 갖고 있지 않다. Dim names As List(Of String)만 적었다면 names.Add를 바로 호출할 수 없다. 처음에는 선언과 생성을 한 줄에 모은 Dim names As New List(Of String)() 형태를 사용하면 흐름을 읽기 쉽다.
목록과 집합은 각 항목을 하나씩 적지만, 사전의 초기 항목은 키와 값의 쌍이다. 다음 코드는 비품 이름을 키로, 대여 가능한 수량을 값으로 저장한다. 바깥 중괄호는 전체 항목을 둘러싸며, 안쪽 중괄호 하나는 이름과 수량 한 쌍을 둘러싼다.
Dim available As New Dictionary(Of String, Integer) From {
{"노트북", 2},
{"프로젝터", 1}
}
컬렉션 초기화는 기존 내용 전체를 대체하는 명령이 아니다. 새 객체를 만드는 과정에서 항목을 추가하는 표현이다. 사전의 초기 항목에 같은 키를 두 번 쓰면 추가가 실패한다. 초기화 구문이 짧더라도 해당 컬렉션의 중복 규칙은 그대로 적용된다.
| 형식 | 형식 인수의 뜻 | 초기 항목 예 |
|---|---|---|
| List(Of String) | 각 항목이 문자열이다 | From {"노트북", "프로젝터"} |
| Dictionary(Of String, Integer) | 키는 문자열, 값은 정수다 | From {{"노트북", 2}} |
| HashSet(Of String) | 중복을 판정할 항목이 문자열이다 | From {"민지", "준호"} |
목록, 사전, 집합을 고르는 기준
List(Of T)는 순서 있는 목록이다
List(Of T)는 항목의 순서를 유지하며 같은 값을 여러 번 담을 수 있다. 인덱스(index)는 목록에서 항목의 위치를 나타내는 정수다. 배열과 마찬가지로 첫 항목의 인덱스는 0이다. 항목이 세 개라면 유효한 인덱스는 0, 1, 2이며, Count는 3이다.
Add는 끝에 항목을 추가한다. Insert는 지정한 위치에 넣고 뒤 항목을 밀어낸다. Remove는 같은 값 중 처음 발견한 항목 하나를 지우며, 실제로 지웠는지를 Boolean으로 돌려준다. RemoveAt은 지정한 위치의 항목을 지운다. 같은 노트북 기록이 여러 개 있을 때 Remove("노트북") 한 번으로 모든 기록이 사라지는 것은 아니다.
Contains는 해당 값이 목록에 있는지 확인한다. 위치가 필요하면 IndexOf를 사용한다. IndexOf는 찾지 못했을 때 -1을 반환하므로, 그 결과를 곧바로 인덱스로 사용하면 안 된다. 처음에는 값의 존재만 필요할 때 Contains를 쓰고, 위치가 필요한 작업에서만 인덱스를 다루면 된다.
List의 크기는 항목을 추가하거나 삭제할 때 달라진다. 내부 저장 공간을 얼마나 확보했는지를 나타내는 Capacity도 있지만, 실제 항목 수를 알려면 Count를 사용한다. 빈 목록의 Count는 0이며, 첫 항목에 접근할 수 있는지는 Count가 0보다 큰지로 판단한다.
Dictionary(Of K, V)는 키로 값을 찾는다
Dictionary(Of K, V)는 키(key)와 값(value)을 한 쌍으로 저장하는 사전이다. K와 V는 설명을 위한 형식 자리 이름이며, 실제 선언에서는 Dictionary(Of String, Integer)처럼 구체적인 형식을 넣는다. 키는 항목을 구별하는 표식이고, 값은 그 표식에 연결된 데이터다.
available("노트북")은 목록의 몇 번째 항목을 읽는 표현이 아니다. “노트북”이라는 키에 대응하는 값을 읽는다. 키는 중복될 수 없지만 값은 중복될 수 있다. 노트북과 프로젝터의 수량이 모두 1이어도 서로 다른 키이므로 저장할 수 있다.
Add("노트북", 2)는 새 쌍을 추가한다. 이미 같은 키가 있으면 오류가 발생한다. 반면 available("노트북") = 3은 기존 키의 값을 바꾸며, 키가 없다면 새 쌍을 추가한다. 중복 등록을 막을 작업인지, 값을 갱신할 작업인지에 따라 표현을 고른다.
없는 키를 괄호로 조회하면 KeyNotFoundException이라는 실행 오류가 발생한다. 예외는 실행 중 정상 진행을 막는 상황을 알리는 방식이며, 처리 문법은 파일 입출력을 다룰 때 함께 배운다. 여기서는 오류가 난 뒤 처리하기보다 TryGetValue로 조회 성공 여부를 확인한다.
Dim quantity As Integer
If available.TryGetValue("회의용 마이크", quantity) Then
Console.WriteLine(quantity)
Else
Console.WriteLine("등록되지 않은 비품이다.")
End If
TryGetValue의 반환값은 키를 찾았는지를 알려 준다. 찾았다면 두 번째 인수인 quantity에 값이 들어간다. 찾지 못했을 때는 그 형식의 기본값이 들어가므로, 수량 0만 보고 실패를 판단하면 안 된다. 실제로 수량이 0인 비품도 등록되어 있을 수 있기 때문이다.
HashSet(Of T)은 중복 없는 집합이다
HashSet(Of T)은 같은 항목을 한 번만 저장하는 집합이다. 대기 신청자 명단처럼 “이미 등록된 사람인가”를 자주 확인하는 작업에 어울린다. Add는 새 항목을 넣었으면 True, 같은 항목이 이미 있어서 넣지 않았으면 False를 반환한다. 사전의 Add와 달리 같은 항목을 다시 추가했다는 이유만으로 오류를 내지는 않는다.
집합에는 목록처럼 몇 번째 항목을 읽는 인덱스가 없다. Contains로 포함 여부를 확인하고, Remove로 특정 항목을 지운다. 중복을 없애는 것이 목적이지, 접수 순서를 보관하는 것이 목적은 아니다. 접수 순서를 보관해야 한다면 List를 함께 쓰거나 데이터를 구성하는 방식을 다시 정해야 한다.
이 예제의 문자열 컬렉션은 기본 비교 방식을 사용한다. 따라서 “민지”와 “민지 ”는 끝의 공백 때문에 서로 다른 값이다. 실제 입력을 받는 프로그램에서는 공백을 정리할지, 이름 대신 사원 번호를 사용할지 결정해야 한다. 집합을 썼다는 이유만으로 서로 다른 문자열이 같은 사람으로 인식되지는 않는다.
사전과 집합의 열거 순서를 출력 규칙으로 삼지 않는다. 완성 코드에서는 사전 값을 별도의 비품 목록 순서로 조회하고, 집합은 List로 복사한 뒤 정렬한다. 이렇게 출력 순서를 직접 정하면 내부 저장 방식에 기대지 않고 결과를 재현할 수 있다.
| 컬렉션 | 중복 규칙 | 접근 방법 | 예제 용도 |
|---|---|---|---|
| List(Of T) | 같은 항목을 여러 번 저장한다 | 인덱스 또는 순회 | 대여 기록 |
| Dictionary(Of K, V) | 키는 중복되지 않는다 | 키로 조회 | 비품별 남은 수량 |
| HashSet(Of T) | 같은 항목을 한 번 저장한다 | 포함 여부 확인 | 중복 없는 신청자 |
순회하면서 삭제할 때의 규칙
순회는 컬렉션의 항목을 차례로 살펴보는 일이다. For Each는 인덱스를 직접 계산하지 않고 항목을 읽을 수 있어 목록을 출력할 때 편리하다. 하지만 List를 For Each로 읽는 도중에 같은 List의 항목을 추가하거나 삭제하면, 다음 항목으로 진행할 때 InvalidOperationException이 발생할 수 있다. 순회를 시작했을 때의 목록 구조가 바뀌었기 때문이다.
이때 사용하는 열거자(enumerator)는 순회 위치를 관리하는 객체다. List의 열거자는 목록이 구조적으로 바뀌었는지를 확인한다. 반복 변수에 들어온 문자열을 읽는 일과 원래 목록에서 항목을 제거하는 일은 서로 다르다. 삭제가 한 번 실행되었다고 해서 남은 순회도 정상적으로 끝난다고 생각하면 안 된다.
인덱스로 앞에서부터 삭제하면 다른 문제가 생긴다. 0번을 지운 순간 기존 1번 항목은 0번으로 이동한다. 그런데 반복문이 다음 인덱스인 1로 진행하면 이동한 항목을 건너뛴다. 처음 계산한 마지막 인덱스가 더 이상 존재하지 않아 범위를 벗어날 수도 있다.
이번 프로그램에서는 마지막 항목부터 첫 항목으로 이동하면서 삭제한다. 뒤쪽 항목을 지워도 아직 검사하지 않은 앞쪽 항목의 인덱스는 달라지지 않는다. 각 기록을 한 번씩 살펴보고, 반납 대상이면 RemoveAt으로 지운다. 삭제한 횟수를 따로 세어 사전의 대여 가능 수량에 더한다.
다른 방법으로는 먼저 삭제할 항목을 따로 모은 뒤 순회가 끝난 다음 삭제하는 방식이 있다. 선택 기준이 복잡하거나 삭제 목록을 사용자에게 보여 줘야 할 때 유용하다. 이 장에서는 한 목록에서 지정한 이름을 모두 지우는 작업이므로 역방향 인덱스 순회를 사용한다.
컬렉션마다 수정 중 순회의 세부 규칙이 모두 같지는 않다. 처음에는 조회하는 반복문과 추가·삭제하는 작업을 분리하고, 동시에 해야 한다면 사용하는 형식의 동작을 확인하는 습관을 들인다. 특히 여기서 설명한 List의 오류를 모든 컬렉션의 모든 수정에 그대로 적용하지 않는다.
완성 코드
macOS나 Linux에서 .NET 10 SDK가 설치된 터미널을 열고 아래 명령으로 프로젝트를 만든다. 생성된 Program.vb 전체를 다음 코드로 바꾼다. 기존 파일 내용에 덧붙이지 않는다. 필요한 형식은 System.Collections.Generic에서 가져오며, 별도의 패키지는 필요하지 않다.
dotnet new console -lang VB -n OfficeCollections
cd OfficeCollections
수량표의 값은 현재 대여 가능한 수량이다. 대여 기록의 항목 수와 더하면 해당 종류의 전체 보유 수량이 된다. 시작 상태에서 노트북은 대여 가능 2대와 대여 중 2대로 총 4대다. 이번 실행에서는 대여 중인 노트북 두 대가 모두 반납된다고 가정한다.
Option Strict On
Imports System
Imports System.Collections.Generic
Module Program
Sub Main()
Dim equipmentOrder As New List(Of String) From {
"노트북", "프로젝터", "모니터"
}
Dim available As New Dictionary(Of String, Integer) From {
{"노트북", 2},
{"프로젝터", 1},
{"모니터", 3}
}
Dim loans As New List(Of String) From {
"노트북", "프로젝터", "노트북"
}
Dim waiting As New HashSet(Of String) From {
"민지", "준호"
}
Dim added As Boolean = waiting.Add("민지")
Console.WriteLine("=== 신청자 등록 ===")
If added Then
Console.WriteLine("민지를 새로 등록했다.")
Else
Console.WriteLine("민지는 이미 등록되어 있다.")
End If
Console.WriteLine($"신청자 수: {waiting.Count}")
Dim waitingOrder As New List(Of String)(waiting)
waitingOrder.Sort(StringComparer.Ordinal)
For Each name As String In waitingOrder
Console.WriteLine($"신청자: {name}")
Next
Console.WriteLine()
Console.WriteLine("=== 반납 전 대여 기록 ===")
For i As Integer = 0 To loans.Count - 1
Console.WriteLine($"{i + 1}. {loans(i)}")
Next
Dim returned As Integer = 0
For i As Integer = loans.Count - 1 To 0 Step -1
If loans(i) = "노트북" Then
loans.RemoveAt(i)
returned += 1
End If
Next
available("노트북") += returned
Console.WriteLine()
Console.WriteLine($"노트북 반납 수: {returned}")
Console.WriteLine($"남은 대여 기록 수: {loans.Count}")
For Each equipment As String In loans
Console.WriteLine($"대여 중: {equipment}")
Next
Console.WriteLine()
Console.WriteLine("=== 대여 가능한 수량 ===")
For Each equipment As String In equipmentOrder
Console.WriteLine($"{equipment}: {available(equipment)}")
Next
Dim quantity As Integer
If available.TryGetValue("회의용 마이크", quantity) Then
Console.WriteLine($"회의용 마이크: {quantity}")
Else
Console.WriteLine("회의용 마이크: 등록되지 않은 비품")
End If
End Sub
End Module
줄별 해설
Option Strict On은 의도하지 않은 형식 변환을 제한한다. 두 Imports 줄은 Console과 제네릭 컬렉션의 형식을 짧은 이름으로 사용할 수 있게 한다. Module Program 안의 Sub Main이 실행 시작점이며, 여기서는 모든 작업을 이 프로시저 안에서 끝낸다.
equipmentOrder 선언은 수량표를 보여 줄 순서를 정한다. 이름 세 개를 원하는 순서로 넣었으므로 노트북, 프로젝터, 모니터 순서로 출력한다. 사전 자체를 순회해 출력 순서를 얻지 않는 이유가 여기에 있다.
available 선언은 비품 이름과 대여 가능 수량을 연결한다. String과 Integer라는 두 형식 인수가 각각 키와 값에 적용된다. 세 쌍의 키가 서로 다르므로 초기화 과정에서 중복 키 오류가 발생하지 않는다.
loans 선언은 대여 중인 기록 세 개를 만든다. 첫 번째와 세 번째 항목이 모두 노트북이지만 List는 이를 별개의 항목으로 보관한다. 이 중복은 잘못된 등록이 아니라 노트북 두 대가 대여 중이라는 예제의 표현이다.
waiting 선언과 Add 호출은 중복 등록을 확인한다. 초기 집합에 민지가 있으므로 waiting.Add("민지")의 결과는 False다. If는 그 결과에 따라 이미 등록되었다는 문장을 출력한다. Count는 중복 추가 후에도 2다.
waitingOrder 생성은 집합의 항목을 새 List에 복사한다. 집합은 그대로 유지되고, 복사한 목록만 정렬한다. Sort에 전달한 StringComparer.Ordinal은 문화권별 정렬 규칙 대신 문자열의 문자 코드에 따른 비교를 사용한다. 이 예제의 두 이름은 민지, 준호 순서가 된다.
첫 번째 For Each는 정렬된 신청자 목록을 읽기만 한다. name은 이번 차례의 문자열이다. 반복문 안에서 waitingOrder를 추가하거나 삭제하지 않으므로 목록 구조가 바뀌지 않는다.
반납 전 기록을 출력하는 For는 인덱스 0부터 Count - 1까지 이동한다. 화면의 번호는 사람이 읽기 편하도록 i + 1을 사용한다. 화면 번호 1과 목록 인덱스 0을 구별해야 한다.
returned와 역방향 For는 반납할 노트북 기록을 찾는다. 시작 인덱스는 2다. 2번 노트북을 지우고, 1번 프로젝터는 남기고, 0번 노트북을 지운다. RemoveAt이 실행될 때마다 returned가 하나씩 증가하므로 최종값은 2다. 목록이 비어 있었다면 시작값이 -1이므로 이 반복문의 본문은 실행되지 않는다.
available("노트북") += returned는 노트북의 대여 가능 수량을 2에서 4로 바꾼다. 이 키는 위에서 등록했으므로 조회할 수 있다. 대여 기록 삭제와 수량 갱신을 함께 수행하여 두 정보가 같은 반납 상황을 나타내게 한다.
반납 후 For Each는 남은 프로젝터 기록 하나를 출력한다. 이어지는 수량 출력은 equipmentOrder에서 이름을 읽고 available에서 수량을 찾는다. 두 컬렉션을 연결하는 기준은 인덱스가 아니라 같은 비품 이름이다.
마지막 TryGetValue는 등록하지 않은 회의용 마이크를 조회한다. 반환값이 False이므로 Else의 안내 문장을 출력한다. 없는 키를 괄호로 직접 읽지 않아 프로그램이 끝까지 실행된다.
실행 결과
프로젝트 폴더에서 다음 명령을 실행한다. 첫 실행 때 SDK가 표시하는 안내가 있다면 프로그램 출력과 구별한다. 아래는 완성 프로그램이 표준 출력에 쓰는 내용이다.
dotnet run
=== 신청자 등록 ===
민지는 이미 등록되어 있다.
신청자 수: 2
신청자: 민지
신청자: 준호
=== 반납 전 대여 기록 ===
1. 노트북
2. 프로젝터
3. 노트북
노트북 반납 수: 2
남은 대여 기록 수: 1
대여 중: 프로젝터
=== 대여 가능한 수량 ===
노트북: 4
프로젝터: 1
모니터: 3
회의용 마이크: 등록되지 않은 비품
노트북 기록 두 개가 없어지고 프로젝터만 남는다. 대여 가능한 노트북 수량은 반납한 두 대만큼 증가한다. 신청자 수는 중복 등록을 시도하기 전과 같으며, 등록되지 않은 비품 조회도 안내 문장을 출력한 뒤 끝난다.
실무에서 자주 틀리는 것
For Each 안에서 같은 목록을 삭제한다
다음 코드는 노트북 기록을 찾자마자 원래 목록을 변경한다. List의 순회가 계속되면서 수정이 감지되면 실행 오류가 발생한다. Remove가 값을 받는다는 점만 보고 안전하다고 판단해서는 안 된다.
' 틀린 코드
For Each equipment As String In loans
If equipment = "노트북" Then
loans.Remove(equipment)
End If
Next
같은 목록에서 일치하는 항목을 모두 지우려면 마지막 인덱스부터 검사한다. 실제 수량표도 관리한다면 완성 코드처럼 삭제한 개수를 세고 수량을 갱신해야 한다.
' 고친 코드
For i As Integer = loans.Count - 1 To 0 Step -1
If loans(i) = "노트북" Then
loans.RemoveAt(i)
End If
Next
이미 있는 키에 Add로 값을 갱신한다
사전에 노트북 키가 있는 상태에서 Add를 다시 호출하면 같은 키의 새 항목을 만들려 하기 때문에 오류가 발생한다. 수량을 바꾸려는 목적이라면 키에 값을 대입한다.
' 틀린 코드
Dim available As New Dictionary(Of String, Integer) From {
{"노트북", 2}
}
available.Add("노트북", 4)
' 고친 코드
Dim available As New Dictionary(Of String, Integer) From {
{"노트북", 2}
}
available("노트북") = 4
새 비품 등록과 기존 비품 수량 변경은 서로 다른 작업이다. 등록에서는 중복 키를 확인하고, 변경에서는 기존 키가 있어야 하는지 정책을 정한다. 괄호 대입은 없는 키도 추가하므로, 기존 항목만 바꿔야 한다면 ContainsKey로 먼저 확인한다.
없는 키를 직접 읽는다
수량표에 없다는 사실과 수량이 0이라는 사실은 다르다. 등록 여부를 모르면서 바로 읽으면 조회 자체가 실패한다.
' 틀린 코드
Dim available As New Dictionary(Of String, Integer) From {
{"노트북", 2}
}
Console.WriteLine(available("회의용 마이크"))
' 고친 코드
Dim available As New Dictionary(Of String, Integer) From {
{"노트북", 2}
}
Dim quantity As Integer
If available.TryGetValue("회의용 마이크", quantity) Then
Console.WriteLine(quantity)
Else
Console.WriteLine("등록되지 않은 비품이다.")
End If
성공 여부를 반환값으로 판단해야 한다. 조회에 실패한 quantity의 기본값 0을 출력하면, 등록하지 않은 비품을 재고가 없는 비품으로 잘못 안내하게 된다.
집합을 인덱스로 읽는다
HashSet에는 목록의 위치를 나타내는 인덱스 접근이 없다. 다음 코드는 첫 신청자를 읽는 코드가 되지 않으며 컴파일되지 않는다.
' 틀린 코드
Dim waiting As New HashSet(Of String) From {"준호", "민지"}
Console.WriteLine(waiting(0))
단순히 모든 항목을 읽으려면 For Each를 사용한다. 일정한 순서로 보여 줘야 한다면 목록으로 복사하고 정렬한다.
' 고친 코드
Dim waiting As New HashSet(Of String) From {"준호", "민지"}
Dim names As New List(Of String)(waiting)
names.Sort(StringComparer.Ordinal)
For Each name As String In names
Console.WriteLine(name)
Next
정렬은 표시 순서를 정하는 작업이다. 이 코드만으로 먼저 신청한 사람을 알아낼 수는 없다. 신청 시각이나 접수 순서가 필요하다면 해당 정보를 별도로 저장해야 한다.
한눈에 보기
| 목적 | 표현 | 확인할 점 |
|---|---|---|
| 목록 끝에 추가 | loans.Add("노트북") | 같은 값도 추가된다 |
| 목록 수 확인 | loans.Count | 마지막 인덱스는 Count - 1이다 |
| 목록의 위치로 삭제 | loans.RemoveAt(i) | 삭제 후 뒤 항목의 위치가 바뀐다 |
| 사전 값 갱신 | available("노트북") = 4 | 없는 키라면 새 항목이 된다 |
| 안전한 사전 조회 | available.TryGetValue(name, quantity) | 반환값으로 성공 여부를 판단한다 |
| 집합에 추가 | waiting.Add("민지") | 중복이면 False를 반환한다 |
| 포함 여부 확인 | waiting.Contains("민지") | 문자열의 공백도 비교에 영향을 준다 |
| 일정한 표시 순서 | names.Sort(StringComparer.Ordinal) | 집합을 목록으로 복사한 뒤 정렬한다 |
목록에서는 위치와 중복, 사전에서는 키의 존재, 집합에서는 포함 여부를 먼저 생각한다. 컬렉션을 고를 때 이름이나 짧은 코드만 보지 말고, 데이터가 지켜야 할 규칙을 기준으로 삼는다. 같은 자료를 서로 다른 목적으로 보여 줄 때는 완성 코드처럼 조회용 컬렉션과 표시 순서를 함께 사용할 수도 있다.
API의 세부 동작을 더 확인하려면 List 공식 참조, Dictionary 공식 참조, HashSet 공식 참조를 참고할 수 있다. 다음 장에서는 이처럼 모아 둔 데이터에서 조건에 맞는 항목을 선택하는 LINQ 쿼리 식을 다룬다.
연습 문제
- 완성 코드의 대여 기록에 “모니터”를 마지막 항목으로 추가한다. 노트북 반납 처리는 그대로 둔다. 반납 후 남는 기록의 개수와 순서, 모니터의 대여 가능 수량을 구한다. 시작 수량표는 변경하지 않는다.
- 수량표에 등록되어 있는 “프로젝터”를 TryGetValue로 조회한다. 성공하면 “프로젝터 대여 가능: 1”을 출력하는 코드를 작성한다. 등록되지 않았을 때의 안내도 포함한다.
- “민지”, “준호”로 초기화한 집합에 “준호”와 “서연”을 차례로 추가한다. 각 Add의 반환값과 최종 Count를 구하고, 이름을 일정한 순서로 출력하는 코드를 작성한다.
- 대여 기록이 “노트북”, “노트북”, “프로젝터”일 때 앞에서부터 삭제하면 왜 문제가 되는지 설명한다. 뒤에서부터 모든 노트북 기록을 삭제하고, 삭제한 수를 출력하는 코드를 작성한다.
정답과 해설
1. 남은 기록과 수량
대여 기록 초기화를 다음처럼 바꾼다.
Dim loans As New List(Of String) From {
"노트북", "프로젝터", "노트북", "모니터"
}
노트북 두 항목만 삭제되므로 남은 기록은 2개다. 순서는 프로젝터, 모니터다. 모니터는 반납하지 않았으므로 대여 가능 수량은 시작값인 3이다. 대여 중인 모니터 한 대까지 포함하면 이 문제의 시작 상태에서는 모니터를 총 4대 보유한 것으로 해석한다.
2. 등록된 비품 조회
Dim quantity As Integer
If available.TryGetValue("프로젝터", quantity) Then
Console.WriteLine($"프로젝터 대여 가능: {quantity}")
Else
Console.WriteLine("프로젝터는 등록되지 않았다.")
End If
프로젝터 키가 있으므로 성공 분기가 실행된다. 값 1이 quantity에 들어가고 지정한 문장이 출력된다. 수량이 나중에 0으로 바뀌어도 키가 남아 있다면 조회는 성공한다.
3. 중복 추가와 새 신청자 추가
Dim waiting As New HashSet(Of String) From {"민지", "준호"}
Dim firstAdded As Boolean = waiting.Add("준호")
Dim secondAdded As Boolean = waiting.Add("서연")
If firstAdded Then
Console.WriteLine("준호 추가: 성공")
Else
Console.WriteLine("준호 추가: 중복")
End If
If secondAdded Then
Console.WriteLine("서연 추가: 성공")
Else
Console.WriteLine("서연 추가: 중복")
End If
Console.WriteLine($"신청자 수: {waiting.Count}")
Dim names As New List(Of String)(waiting)
names.Sort(StringComparer.Ordinal)
For Each name As String In names
Console.WriteLine(name)
Next
첫 반환값은 False, 두 번째 반환값은 True다. 준호는 이미 있으므로 항목 수가 늘지 않고, 서연은 새 이름이므로 최종 Count는 3이다. 정렬한 이름은 민지, 서연, 준호 순서로 출력된다.
4. 위치 이동을 고려한 삭제
Dim loans As New List(Of String) From {
"노트북", "노트북", "프로젝터"
}
Dim removed As Integer = 0
For i As Integer = loans.Count - 1 To 0 Step -1
If loans(i) = "노트북" Then
loans.RemoveAt(i)
removed += 1
End If
Next
Console.WriteLine($"삭제한 기록 수: {removed}")
For Each equipment As String In loans
Console.WriteLine(equipment)
Next
앞에서 0번 노트북을 지우면 두 번째 노트북이 0번으로 이동한다. 반복 인덱스를 1로 올리면 그 노트북을 건너뛴다. 뒤에서 시작하는 코드는 프로젝터를 남기고 두 노트북을 모두 지운다. “삭제한 기록 수: 2”와 “프로젝터”가 차례로 출력된다. 이 코드는 삭제 원리만 확인하며, 완성 앱의 반납 처리에 넣을 때는 삭제 수만큼 대여 가능 수량도 늘려야 한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.