제네릭 클래스 만들기
이번 강의는 유용한 제네릭 클래스 및 제네릭 인터페이스 등을 사용해본 후 제네릭 클래스를 직접 만들어 보겠습니다. 제네릭 클래스는 형식 매개변수 T
에 지정한 형식으로 클래스와 멤버의 성질을 결정합니다.
> // 제네릭 클래스: 형식 매개 변수 T에 지정한 형식으로 클래스와 멤버의 성질이 결정
사용자 정의 클래스를 매개 변수로 사용하는 제네릭 클래스
이미 우리는 많은 양의 컬렉션 클래스와 제네릭 리스트를 사용해 왔습니다. 컬렉션을 다룰 때 필요한 데이터 형식만을 사용하기에 성능 향상을 가져다 주는 기법을 제네릭이라 합니다. 제네릭은 C# 2.0부터 등장한 특징 중 하나입니다. 제네릭은 매개 변수화 된 형식을 만드는 데 사용이 됩니다. 제네릭에 전달하는 매개 변수를 형식 매개 변수(Type Parameter)라고 합니다. 형식 매개 변수에 int
, string
과 같은 기본 형식이 아닌 사용자 정의 클래스를 사용하는 방법을 정리해보겠습니다.
컬렉션 이니셜라이저로 제네릭 리스트 초기화하기
컬렉션 이니셜라이저(Collection Initializer)는 컬렉션의 값을 초기화하는 쉬운 방법을 제공합니다.
특정 클래스 형식의 리스트를 한번에 초기화하는 컬렉션 이니셜라이저를 사용해보겠습니다.
코드: CollectionInitializers.cs
> //[?] 컬렉션 이니셜라이저를 사용하여 개체 여러 개를 초기화
> class Person
. {
. public string Name { get; set; }
. }
>
> List<Person> people = new List<Person>
. {
. new Person { Name = "홍길동" },
. new Person { Name = "백두산" },
. new Person { Name = "임꺽정" }
. };
>
> foreach (var person in people)
. {
. Console.WriteLine(person.Name);
. }
홍길동
백두산
임꺽정
List<T>
형태의 컬렉션 개체를 선언과 동시에 특정 개체 값으로 초기화할 수 있습니다. 이러한 내용을 컬렉션 이니셜라이저라고 합니다.
제네릭 클래스에 사용자 정의 클래스 사용하기
List<T>
형태의 T
에 사용자 정의 클래스를 매개 변수로 사용할 수 있습니다. 이렇게 만들어진 제네릭 개체는 개체 이니셜라이저와 마찬가지로 컬렉션도 이니셜라이저로 컬렉션 개체 생성할 때 바로 특정 요소로 초기화할 수 있습니다.
또 다른 컬렉션 이니셜라이저 예제를 살펴보겠습니다. 다음 코드의 [1]
번 코드 영역이 컬렉션 이니셜라이저입니다.
코드: ListOfCategory.cs
> // 모델 클래스: Category, CategoryModel, CategoryViewModel, CategoryDto, ...
. class Category
. {
. public int CategoryId { get; set; }
. public string CategoryName { get; set; }
. }
>
> //[1] 컬렉션 이니셜라이저를 사용하여 카테고리 리스트 만들기
> var categories = new List<Category>()
. {
. new Category() { CategoryId = 1, CategoryName = "좋은 책" },
. new Category() { CategoryId = 2, CategoryName = "좋은 강의" },
. new Category() { CategoryId = 3, CategoryName = "좋은 컴퓨터" }
. };
>
> //[2] foreach 문으로 컬렉션 데이터를 출력
> foreach (var category in categories)
. {
. Console.WriteLine($"{category.CategoryId} - {category.CategoryName}");
. }
1 - 좋은 책
2 - 좋은 강의
3 - 좋은 컴퓨터
위 코드는 카테고리를 다루는 모델 클래스인 Category
클래스의 인스턴스 여러 개를 담을 수 있는 List<T>
형태의 categories
컬렉션을 만들고 출력해보는 간단한 예제입니다.
제네릭 개체를 초기화하는 3가지 방법 정리
단순한 int
, string
이 아닌 사용자 정의된 클래스를 List<T>
제네릭 리스트 클래스의 T
에 전달하여 구조화해서 사용할 수 있습니다.
특정 클래스 형태의 리스트 클래스를 사용하여 12개월의 일사량을 출력하는 방법을 예제로 살펴보겠습니다.
코드: ListNote.cs
> /// <summary>
. /// 연간 일사량
. /// </summary>
. public class Insolation
. {
. /// <summary>
. /// 월(1월부터 12월까지)
. /// </summary>
. public int Month { get; set; }
. /// <summary>
. /// 일사량 값
. /// </summary>
. public float Value { get; set; }
. }
>
> //[1] 개체 형식의 리스트 생성: 컬렉션 이니셜라이저로 값 초기화
> List<Insolation> insolations = new List<Insolation>()
. {
. new Insolation { Month = 1, Value = 0.3f },
. new Insolation { Month = 2, Value = 0.6f },
. new Insolation { Month = 3, Value = 0.9f },
. new Insolation { Month = 4, Value = 1.2f }
. };
>
> //[2] Add() 메서드로 리스트에 값 추가: 개체 이니셜라이저로 값 초기화
. insolations.Add(new Insolation() { Month = 5, Value = 1.5f });
. insolations.Add(new Insolation() { Month = 6, Value = 1.8f });
. insolations.Add(new Insolation() { Month = 7, Value = 1.6f });
. insolations.Add(new Insolation() { Month = 8, Value = 1.5f });
>
> //[3] AddRange() 메서드로 리스트에 값들 추가
> var tempInsolations = new List<Insolation>()
. {
. new Insolation { Month = 9, Value = 1.2f },
. new Insolation { Month = 10, Value = 0.9f },
. new Insolation { Month = 11, Value = 0.6f },
. new Insolation { Month = 12, Value = 0.1f }
. };
. insolations.AddRange(tempInsolations);
>
> //[4] 리스트 출력
> Console.WriteLine("연간 일사량");
연간 일사량
> foreach (var insolation in insolations)
. {
. Console.WriteLine($"{insolation.Month:00} - {insolation.Value}");
. }
01 - 0.3
02 - 0.6
03 - 0.9
04 - 1.2
05 - 1.5
06 - 1.8
07 - 1.6
08 - 1.5
09 - 1.2
10 - 0.9
11 - 0.6
12 - 0.1
[1]
번 주석 코드처럼 List<T>
형태에 List<Insolation>
형태로 사용자 정의 클래스를 넣고 개체를 생성할 수 있습니다. 리스트를 선언과 동시에 초기화할 때에는 컬렉션 이니셜라이저를 사용하여 한번에 데이터를 여러 개 줄 수 있습니다.
이미 기본값으로 초기화된 리스트에 추가로 데이터를 입력할 때에는 [2]
번 코드처럼 Add()
메서드에 개체를 개체 이니셜라이저로 줄 수 있습니다. 또한, [3]
번 코드처럼 AddRange()
메서드로 여러개의 데이터 리스트를 한꺼번에 줄 수도 있습니다.
리스트의 값을 출력할 때에는 foreach
문을 사용하여 반복해서 사용하면 됩니다.
LINQ를 사용하여 사용자 정의 제네릭 개체 데이터 다루기
이번에는 LINQ를 사용하여 특정 클래스 형태의 컬렉션 데이터를 다루는 여러 가지 사용법을 알아보겠습니다. 다음 내용을 C# 인터렉티브에서 단계별로 실행해보세요.
코드: LinqCollectionDemo.cs
> // LINQ(Language INtegrate Query): 마치 SQL문을 사용하든 프로그램에서 사용하는 쿼리문
> class Car
. {
. public string Make { get; set; }
. public string Model { get; set; }
. public int Year { get; set; }
. }
> class NewType
. {
. public string Maker { get; set; }
. }
> // 컬렉션 이니셜라이저를 사용하여 데이터 담기
> List<Car> cars = new List<Car>() {
. new Car() { Make = "Camper", Model = "Camper1", Year = 2015 },
. new Car() { Make = "Camper", Model = "Camper3", Year = 2016 },
. new Car() { Make = "SUV", Model = "AAA", Year = 2017 },
. new Car() { Make = "SUV", Model = "BBB", Year = 2018 },
. new Car() { Make = "SUV", Model = "CCC", Year = 2019 },
. new Car() { Make = "SUV", Model = "DDD", Year = 2020 }
. };
> // LINQ 사용해서 Camper만 출력 : select * from cars where make = 'Camper'
. var campers = from car in cars
. where car.Make == "Camper"
. select car;
> campers
Enumerable.WhereListIterator<Submission#4.Car> { Submission#4.Car { Make="Camper", Model="Camper1", Year=2015 }, Submission#4.Car { Make="Camper", Model="Camper3", Year=2016 } }
> // 2015년도 이후로 출시된 자동차
. var newCars = from car in cars
. where car.Year >= 2015
. select car;
> newCars
Enumerable.WhereListIterator<Submission#4.Car> { Submission#4.Car { Make="Camper", Model="Camper1", Year=2015 }, Submission#4.Car { Make="Camper", Model="Camper3", Year=2016 }, Submission#4.Car { Make="SUV", Model="AAA", Year=2017 }, Submission#4.Car { Make="SUV", Model="BBB", Year=2018 }, Submission#4.Car { Make="SUV", Model="CCC", Year=2019 }, Submission#4.Car { Make="SUV", Model="DDD", Year=2020 } }
> // 가장 최근에 출시된 자동차부터 정렬
. var orderedCars = from car in cars
. orderby car.Year descending
. select car;
> orderedCars
OrderedEnumerable<Submission#4.Car, int> { Submission#4.Car { Make="SUV", Model="DDD", Year=2020 }, Submission#4.Car { Make="SUV", Model="CCC", Year=2019 }, Submission#4.Car { Make="SUV", Model="BBB", Year=2018 }, Submission#4.Car { Make="SUV", Model="AAA", Year=2017 }, Submission#4.Car { Make="Camper", Model="Camper3", Year=2016 }, Submission#4.Car { Make="Camper", Model="Camper1", Year=2015 } }
> // LINQ 식을 통해서 새로운 개체 형식으로 반환
. var newObjects = from car in cars
. orderby car.Year ascending
. select new NewType { Maker = car.Make };
> newObjects
Enumerable.WhereSelectEnumerableIterator<Submission#4.Car, Submission#5.NewType> { Submission#5.NewType { Maker="Camper" }, Submission#5.NewType { Maker="Camper" }, Submission#5.NewType { Maker="SUV" }, Submission#5.NewType { Maker="SUV" }, Submission#5.NewType { Maker="SUV" }, Submission#5.NewType { Maker="SUV" } }
List<T>
의 T
에 사용자 지정 클래스 설정하기
이번에는 List<T>
의 T
자리에 특정 클래스 형식을 지정하는 예제를 살펴보겠습니다. 다음 내용을 C# Interactive의 소스 코드 편집 창에 입력한 뒤 실행해보세요. 프로젝트 기반 소스는 ListOfObject.cs 파일에 있습니다.
(1) 전화번호의 지역과 국번을 담을 수 있는 AreaCode
클래스를 작성합니다.
> //[1] 지역과 국번 저장 클래스 선언
> public class AreaCode
. {
. public string Number { get; set; }
. public string AreaName { get; set; }
. }
(2) List<T>
형식에 List<AreaCode>
를 지정하여 제네릭 리스트 개체인 areas
를 생성합니다.
> //[2] 제네릭 리스트 개체 생성
> List<AreaCode> areas = new List<AreaCode>();
(3) 개체를 따로 따로 속성과 개체 이니셜라이저를 사용하여 2개의 개체를 생성합니다.
> //[3] 컬렉션에 포함될 각각의 개체 생성
> //[3][1] 속성으로 개체 초기화
> AreaCode seoul = new AreaCode();
> seoul.Number = "02";
> seoul.AreaName = "서울";
>
> //[3][2] 개체 이니셜라이저로 개체 초기화
> AreaCode sejong = new AreaCode()
. {
. Number = "044",
. AreaName = "세종"
. };
(4) 컬렉션에 Add()
메서드로 AreaCode
개체를 등록합니다.
> //[4] 컬렉션에 개체 등록
> areas.Add(seoul);
> areas.Add(sejong);
(5) for
구문 또는 foreach
구문을 통해서 areas
컬렉션 개체의 내용을 출력합니다.
> //[5] 컬렉션의 값을 반복해서 속성을 사용해서 출력
> foreach (var area in areas)
. {
. Console.WriteLine($"번호: {area.Number}, 지역: {area.AreaName}");
. }
번호: 02, 지역: 서울
번호: 044, 지역: 세종
위 실행 코드처럼 List<int>
형식의 단순한 형태 대신에 List<AreaCode>
형태를 사용하여 배열보다 사용하기 편한 구조를 만들 수 있습니다. 아무튼, 필자는 배열보다는 리스트 특히 List<T>
형태의 컬렉션을 자주 사용하는 편입니다.
참고: 컬렉션 합치기 연습
2개의 컬렉션을 하나의 컬렉션으로 합치는 기능을 만들어보겠습니다. 다음 그림과 같이 First
컬렉션과 Second
컬렉션을 합쳐서 Merge
컬렉션에 포함하는 기능을 만들어보고자할 때 방법이 많이 있겠지만, List<T>
제네릭 클래스의 Add()
메서드를 사용하여 쉽게 구현이 가능합니다.
그림: 컬렉션 합치기
위 그림의 컬렉션 합치기 예제를 만들어 보겠습니다.
코드: CollectionMerge.cs
> //[?] 수작업으로 First, Second 컬렉션을 Merge 컬렉션에 포함하기
> class First
. {
. public string A { get; set; }
. public string B { get; set; }
. }
>
> class Second
. {
. public string B { get; set; }
. public string C { get; set; }
. }
>
> class Merge
. {
. public string A { get; set; }
. public string B { get; set; }
. public string C { get; set; }
. }
>
> var first = new List<First>()
. {
. new First() { A = "F1A", B = "F1B" },
. new First() { A = "F2A", B = "F2B" }
. };
>
> var second = new List<Second>()
. {
. new Second() { B = "S1B", C = "S1C" },
. new Second() { B = "S2B", C = "S2C" }
. };
> // Merge 컬렉션 생성
> var merge = new List<Merge>();
>
> // first 컬렉션 추가: for 문 사용
> for (int i = 0; i < first.Count; i++)
. {
. merge.Add(new Merge() { A = first[i].A, B = first[i].B });
. }
>
> // second 컬렉션 추가: foreach 문 사용
> foreach (var s in second)
. {
. merge.Add(new Merge() { B = s.B, C = s.C });
. }
>
> // 합쳐진 컬렉션 출력
> Console.WriteLine($"{"A ",5} {"B ",5} {"C ",5}");
A B C
> foreach (var m in merge)
. {
. Console.WriteLine($"{m.A,5} {m.B,5} {m.C,5}");
. }
F1A F1B
F2A F2B
S1B S1C
S2B S2C
실행 결과, 2개의 컬렉션이 하나의 제네릭 컬렉션에 포함되어 출력됨을 알 수 있습니다.
사전(Dictionary) 제네릭 클래스 소개
닷넷에서 제공되는 컬렉션 클래스는 리스트(List)와 사전(Dictionary)으로 구분할 수 있습니다. 리스트와 사전은 다음과 같이 비교할 수 있습니다.
TIP
사전 클래스는 발음 그대로 딕셔너리로 표현하기도 합니다. 강의에서는 사전과 딕셔너리를 함께 사용합니다.
표: 리스트와 사전 비교
항목 | 리스트 | 딕셔너리 |
---|---|---|
요소의 저장 방식 | 하나의 요소에 값이 저장됨 | 하나의 요소에 키와 값이 저장됨 |
요소 접근 방식 | 인덱스를 사용하여 요소에 접근 | 키를 사용하여 요소에 접근 |
중복 허용 여부 | 요소의 값 중복 허용 | 요소의 중복은 허용하나 키의 중복은 허용하지 않음 |
특징 | 반복이 빠름 | 특정 키에 대한 검색이 빠름 |
C#에서 가장 많이 사용되는 리스트 클래스는 List<T>
이고 가장 많이 사용되는 사전 클래스는 Dictionary<TKey, TValue>
입니다.
딕셔너리 클래스 종류
System.Collections.Generic
네임스페이스에는 Dictionary<TKey, TValue>
이외에 SortedList<TKey, TValue>
, SortedDictionary<TKey, TValue>
등 추가적인 딕셔너리 클래스를 제공합니다. 따로 구분할 필요는 없지만 다음 내용은 간단히 읽어보면 됩니다. 추가적인 상세한 정보는 Microsoft Learn(Docs) 사이트를 참고해 주세요.
Dictionary<TKey, TValue>
- 일반적 형태로 저장됨, 정렬되지 않음
SortedList<TKey, TValue>
- 키에 의해 정렬, 정렬된 데이터 출력시 빠름
SortedDictionary<TKey, TValue>
- 키에 의해 정렬, 정렬되지 않은 데이터 출력시 빠름
참고: ListDictionary
클래스 소개
닷넷에서 제공하는 컬렉션 관련 클래스 중에는 ListDictionary
라는 독특한 클래스도 있습니다. 중요하지 않은 클래스이니 코드로만 간단히 살펴보겠습니다.
다음 내용을 C# Interactive의 소스 코드 편집 창에 입력한 뒤 실행해보세요. 프로젝트 기반 소스는 ListDictionaryDemo.cs 파일에 있습니다.
(1) 네임스페이스를 추가합니다.
> using System.Collections.Specialized;
(2) Book
클래스를 하나 만듭니다.
> //[0] 데모용 Book 클래스
> class Book
. {
. public string Title { get; set; }
. public string Author { get; set; }
. public string ISBN { get; set; }
. }
(3) Book
클래스의 개체를 2개 생성합니다. 하나는 속성을 사용하고 다른 하나는 개체 이니셜라이저를 사용하였습니다.
> //[1] 개체의 인스턴스 생성 -> 속성값 초기화
> Book b1 = new Book();
> b1.Title = "ASP.NET"; b1.Author = "박용준"; b1.ISBN = "1234";
>
> //[2] Object Intializer(개체 초기화자)
> Book b2 = new Book() { Title = "C#", Author = "박용준", ISBN = "4321" };
(4) ListDictionary
개체에 문자열 키 값을 사용하여 Book
개체 2개를 저장합니다. ListDictionary
클래스는 키와 값을 저장할 때 object
형식을 사용합니다.
> //[3] ListDictionary 클래스에 개체 담기 : Key, Value 쌍으로 개체 값 저장
> ListDictionary ld = new ListDictionary();
> ld.Add("첫번째", b1);
> ld.Add("두번째", b2);
(5) ListDictionary
개체에 저장된 값을 키를 가지고 검색 후 Book
클래스 형식으로 변환 후 각각의 속성 값을 출력해 봅니다.
> //[4] object 형식으로 저장된 값을 Book으로 변환 후 출력
> ((Book)ld["첫번째"]).Title
"ASP.NET"
> Book b = (Book)ld["두번째"];
> Console.WriteLine("{0}, {1}, {2}", b.Title, b.Author, b.ISBN);
C#, 박용준, 4321
ListDictionary
클래스는 제네릭 클래스가 아니기에 거의 사용하지 않습니다. 하지만, 키와 값으로 저장되는 클래스 중 하나이기에 이번 예제에서 다루어 봤습니다.
Dictionary<T>
클래스로 키와 값을 쌍으로 관리하기
Dictionary<T>
클래스는 키(Key)와 값(Value)의 쌍으로 컬렉션을 관리하기 위한 클래스입니다.
이 클래스를 사용하면 많은 양의 표 형태의 데이터를 다루기 편한 기능을 제공합니다.
Dictionary<키, 값>
형태의 컬렉션을 만들고 Add(키, 값)
메서드 또는 [키]
인덱서 형태로 여러 데이터를 보관하고 사용할 수 있습니다.
> Dictionary<int, int> pairs = new Dictionary<int, int>();
> pairs.Add(1, 100);
> pairs.Add(2, 200);
> pairs
Dictionary<int, int>(2) { { 1, 100 }, { 2, 200 } }
키와 값의 쌍을 관리하는 컬렉션이지만, 키 값이 없는 것을 요청하면 다음과 같은 에러가 발생할 수 있으니 미리 사용하고자 하는 만큼의 키와 기본값으로 초기화해 놓고 사용하면 좋습니다.
> pairs[99]
지정한 키가 사전에 없습니다.
+ System.ThrowHelper.ThrowKeyNotFoundException()
+ Dictionary<TKey, TValue>.get_Item(TKey)
1월부터 12월과 같이 명확하게 1부터 12까지의 데이터를 다루는 형태에서 메모리 상에 데이터를 올려두고 사용할 때 Dictionary<T>
클래스가 유용하게 사용됩니다.
코드: DictionaryOfTypeNote.cs
> using System.Collections.Generic;
>
> //[1] 키와 값으로 이루어진 Dictionary<T> 개체 생성
> Dictionary<int, double> keyValuePairs = new Dictionary<int, double>();
>
> //[2] 1부터 12까지 기본값으로 초기화: 배열과 달리 1부터 12까지 지정 가능
> for (int i = 1; i <= 12; i++)
. {
. keyValuePairs.Add(i, 0.0);
. }
>
> //[3] 월별 전기 요금 사용량 관리
> keyValuePairs[1] = 10.01; // 1월에 10.01 kW 사용했다고 가정
> keyValuePairs[2] = 20.02; // 2월에 20.02 kW 사용했다고 가정
>
> //[4] 월별 사용량 출력
> for (int i = 1; i <= 3; i++)
. {
. Console.WriteLine($"{i}월 - {keyValuePairs[i]}kW 사용");
. }
1월 - 10.01kW 사용
2월 - 20.02kW 사용
3월 - 0kW 사용
이번 예제처럼 배열과 달리 0부터 시작하지 않아도 되기에 1부터 12까지의 명확한 데이터 처리에 사용하면 편리합니다.
Dictionary<T>
클래스에 문자열 키 사용하기
이번에는 Dictionary<T>
클래스에 문자열 키를 사용하는 예제를 살펴보겠습니다.
코드: DictionaryOfString.cs
> //[1] Dictionary<T> 클래스: 키(Key)와 값(Value)의 쌍으로 컬렉션을 관리
> Dictionary<string, string> nickNames = new Dictionary<string, string>();
> nickNames.Add("Taeyo", "태오");
> nickNames.Add("RedPlus", "레드플러스");
> nickNames.Add("Itist", "아이티스트");
> nickNames
Dictionary<string, string>(3) { { "Taeyo", "태오" }, { "RedPlus", "레드플러스" }, { "Itist", "아이티스트" } }
> //[2] ContainsKey() 메서드로 키 확인
> if (nickNames.ContainsKey("RedPlus"))
. {
. Console.WriteLine(nickNames["RedPlus"]);
. }
레드플러스
> nickNames["RedPlus"]
"레드플러스"
> nickNames["RedMinus"]
System.Collections.Generic.KeyNotFoundException: 지정한 키가 사전에 없습니다.
+ System.ThrowHelper.ThrowKeyNotFoundException()
+ Dictionary<TKey, TValue>.get_Item(TKey)
[1]
번 코드처럼 Dictionary<string, string>
형태로 개체를 생성하면 문자열 키와 문자열 값을 저장할 수 있는 구조가 만들어 집니다.
Dictionary<T>
클래스는 없는 키 값을 요청하면 에러가 발생하기에 [2]
번 코드와 같이 ContainsKey()
메서드로 키 값을 확인 후 사용할 수 있습니다.
제네릭 인터페이스
형식 매개 변수 T
를 사용하는 제네릭 인터페이스를 사용해보겠습니다.
ICollection<T>
인터페이스
제네릭 컬렉션 관련 클래스들의 부모 역할을 하는 인터페이스 중 하나인 ICollection<T>
인터페이스는 제네릭 컬렉션을 조작하는 메서드에 대한 정의를 제공합니다.
예를 들어 다음과 같은 메서드들을 제공합니다.
Count
: 요소 수를 반환합니다.Add(T)
:T
개체를 추가합니다.Clear()
: 항목을 모두 제거합니다.Contains(T)
: 특정 값이 들어 있는지 여부를 확인합니다.Remove(T)
: 맨 처음 발견되는 특정 개체를 제거합니다.
IEnumerable<T>
인터페이스
IEnumerable<T>
인터페이스는 컬렉션의 데이터를 읽기 전용으로 출력할 때 사용됩니다. 출력 전용이라면 IEnumerable<T>
인터페이스를 반환값으로 사용하는 걸 권장합니다. 당연한 얘기지만, 데이터 수정할 때에는 IEnumerable<T>
를 사용할 수 없습니다.
참고로, Entity Framework Core와 같은 ORM 사용시에는 IEnumerable<T>
대신에 IQueryable<T>
인터페이스를 사용하는 걸 권장합니다.
문자열 배열을 사용하는 3가지 방법
이번에는 문자열 배열을 선언해서 사용하는 3가지 방법을 소개합니다. 가장 기본이되는 string[]
과 List<T>
와 IEnumerable<T>
의 3가지 형태입니다.
코드: StringArray.cs
> //[?] 문자열 배열을 사용하는 3가지 방법
> //[1] 문자열 배열을 선언하는 기본적인 방법
> var a1 = new string[] { "Red", "Green", "Blue" };
>
> //[2] List<T> 개체를 생성 후 문자열 배열을 ToList() 메서드로 변환
> var a2 = new List<string>(); a2 = a1.ToList();
>
> //[3] IEnumerable<T> 개체 생성 후 문자열 배열을 바로 대입 가능
> IEnumerable<string> a3 = a1;
>
> //[4] IEnumerable<T> 개체를 ToList() 메서드로 List<T> 형태로 변환
> var a4 = a3.ToList();
>
> //[5] IEnumerable<T> 개체는 주로 foreach 문으로 반복 사용
> foreach (var arr in a3)
. {
. Console.WriteLine(arr);
. }
Red
Green
Blue
>
> //[6] string[], List<T> 개체는 for 문으로 반복 가능
> for (int i = 0; i < 3; i++)
. {
. Console.WriteLine($"{a1[i]}, {a2[i]}, {a4[i]}");
. }
Red, Red, Red
Green, Green, Green
Blue, Blue, Blue
문자열 배열, 즉 컬렉션 형태의 데이터는 프로그램을 작성하면서 제일 많이 사용하는 형태 중 하나입니다. 이때 string[]
, List<T>
, IEnumerable<T>
형태를 사용해서 배열을 만들고 사용할 수 있습니다. 학습자 입장에서는 이 3가지의 사용법에 포커스를 먼저 맞춘 후 이에 대한 세부적인 차이점은 차차 알아가는 방법을 권장해 드립니다.
제네릭 클래스 만들기
지금까지 우리는 많은 양의 제네릭 클래스를 사용해 왔습니다. 이제는 직접 제네릭 클래스를 만들어 보겠습니다. 제네릭 클래스는 클래스를 생성할 때 <T>
형태로 클래스와 클래스의 멤버의 성질을 결정할 수 있습니다.
제네릭 클래스 만들기
내장된 제네릭 클래스가 아닌 나만의 제네릭 클래스를 만들어 보겠습니다.
코드: GenericClass.cs
> //[?] 제네릭 클래스: T에 지정한 형식으로 클래스와 멤버의 성질이 결정
> //[1] 클래스<T> 형태로 제네릭 클래스 만들기
> public class Cup<T>
. {
. public T Content { get; set; }
. }
>
> //[A] T에 string 전달하여 문자열 저장하는 속성 생성
> Cup<string> text = new Cup<string>();
> text.Content = "문자열"; // string
>
> //[B] T에 int 전달하여 정수형 저장하는 속성 생성
> Cup<int> number = new Cup<int>();
> number.Content = 1234; // int
>
> Console.WriteLine($"{text.Content}, {number.Content}");
문자열, 1234
[1]
번 코드 영역에서 Cup<T>
형태로 형식 매개 변수를 갖는 제네릭 클래스를 만들고 [A]
와 [B]
코드 영역에서 필요한 형식을 전달하여 그 형식에 맞게 속성이 만들어져 사용하는 코드를 작성하였습니다.
제네릭 클래스의 형식 매개 변수로 속성의 형식을 변경하기
T
형식 매개 변수에 전달되는 값에 따라서 Multi<T>
클래스의 Data
속성의 형식이 변경되는 에제는 다음과 같습니다.
코드: GenericClassPractice.cs
> public class Multi<T>
. {
. public T Data { get; set; }
. }
>
> Multi<string> title = new Multi<string>();
> title.Data = "연봉";
>
> Multi<long> income = new Multi<long>();
> income.Data = 100_000_000;
>
> Console.WriteLine($"{title.Data}: {income.Data:#,###}");
연봉: 100,000,000
Multi
클래스에 전달되는 T
에 string
이 전달되면 Data
속성은 string
형식이되는 것이고 long
이 전달되면 Data
속성은 long
형식이 됩니다.
제네릭에 사용자 정의 형식 클래스 전달하기
제네릭 클래스에 기본 형식이 아닌 사용자 정의 형식 클래스를 지정하는 예제를 만들어보도록 하겠습니다.
코드: GenericNote.cs
> class Juice { }
> class Coffee { }
>
> //[1] Cup of T, Cup<T>
. class Cup<T>
. {
. public T Type { get; set; }
. }
>
> //[A] T 형식 매개 변수로 Juice 클래스 전송
> Cup<Juice> juice = new Cup<Juice>();
> juice.Type = new Juice();
> Console.WriteLine(juice.Type.ToString()); // GenericNote.Juice
Juice
>
> //[B] T 형식 매개 변수로 Coffee 클래스 전송
> var coffee = new Cup<Coffee> { Type = new Coffee() };
> Console.WriteLine(coffee.Type.ToString()); // GenericNote.Coffee
Coffee
프로젝트 기반으로 실행하면 다음과 같이 실행됩니다.
GenericNote.Juice
GenericNote.Coffee
클래스 이름 뒤에 <T>
형태를 붙이면 제네릭 클래스가 됩니다. 제네릭 클래스에 전달되는 T
와 같은 표현은 형식 매개 변수로 특정 형식을 매개 변수로 받아서 해당 클래스 형태로 메서드 등을 만들 수 있습니다.
[1]
번과 같이 Cup<T
> 형태로 만들어진 제네릭 클래스는 T
에 특정 클래스 형식을 담을 수 있는 융통성을 가집니다.
[A]
에서 Juice
클래스를 T
형식 매개 변수로 전송하면 juice
개체의 Type
속성은 Juice
클래스의 인스턴스가 됩니다. [B]
에서 Coffee
클래스를 형식 매개 변수로 전송하면 coffee
개체의 Type
속성은 Coffee
클래스의 인스턴스가 됩니다.
형식 매개 변수에 대한 제약조건
제네릭 클래스를 만들 때 형식 매개 변수에 제약 조건을 줄 수 있습니다. 클래스 선언할 때 where
키워드를 사용하여 T
가 구조체인지 클래스인지를 결정할 수 있습니다.
이번에는 형식 매개 변수에 대한 제약조건을 사용해보겠습니다.
다음 내용을 C# Interactive에서 단계별로 실행해보세요. 프로젝트 기반 소스는 TypeConstraint.cs 파일에 있습니다.
(0) IKs
인터페이스와 GoodCar
, BadCar
, OfficeCamper
클래스를 선언합니다.
> public interface IKs { }
> public class GoodCar { }
> public class BadCar { public BadCar(string message) { } }
> public class OfficeCamper : GoodCar, IKs { }
> public class CarValue<T> where T : struct { } // 값 형식만
> CarValue<int> c = new CarValue<int>(); // struct 성공
> CarValue<string> c = new CarValue<string>();
(1,18): error CS0453: 제네릭 형식 또는 메서드 'CarValue<T>'에서 'string' 형식을 'T' 매개 변수로 사용하려면 해당 형식이 null을 허용하지 않는 값 형식이어야 합니다.
(1,35): error CS0453: 제네릭 형식 또는 메서드 'CarValue<T>'에서 'string' 형식을 'T' 매개 변수로 사용하려면 해당 형식이 null을 허용하지 않는 값 형식이어야 합니다.
> public class CarReference<T> where T : class { } // 참조 형식만
> CarReference<string> cs = new CarReference<string>(); // class 성공
> CarReference<decimal> cs = new CarReference<decimal>();
(1,23): error CS0452: 제네릭 형식 또는 메서드 'CarReference<T>'에서 'decimal' 형식을 'T' 매개 변수로 사용하려면 해당 형식이 참조 형식이어야 합니다.
(1,45): error CS0452: 제네릭 형식 또는 메서드 'CarReference<T>'에서 'decimal' 형식을 'T' 매개 변수로 사용하려면 해당 형식이 참조 형식이어야 합니다.
> public class CarNew<T> where T : new() { } // Default 생성자
> CarNew<GoodCar> cn = new CarNew<GoodCar>(); // new() 성공
> CarNew<BadCar> bad = new CarNew<BadCar>();
(1,16): error CS0310: '제네릭 형식 또는 메서드 'CarNew<T>'에서 'T' 매개 변수로 사용하려면 'BadCar'이(가) 매개 변수가 없는 public 생성자를 사용하는 비추상 형식이어야 합니다.
(1,33): error CS0310: '제네릭 형식 또는 메서드 'CarNew<T>'에서 'T' 매개 변수로 사용하려면 'BadCar'이(가) 매개 변수가 없는 public 생성자를 사용하는 비추상 형식이어야 합니다.
> public class CarClass<T> where T : GoodCar { } // GoodCar에서 파생
> CarClass<OfficeCamper> cc = new CarClass<OfficeCamper>(); // 사용자 정의 타입
> CarClass<BadCar> badCar;
(1,18): error CS0311: 'BadCar' 형식은 제네릭 형식 또는 'CarClass<T>' 메서드에서 'T' 형식 매개 변수로 사용할 수 없습니다. 'BadCar'에서 'GoodCar'(으)로의 암시적 참조 변환이 없습니다.
> public class CarInterface<T> where T : IKs { } // IKs인터페이스
> CarInterface<IKs> h = new CarInterface<IKs>(); // 인터페이스 지정
> CarInterface<object> ie;
(1,22): error CS0311: 'object' 형식은 제네릭 형식 또는 'CarInterface<T>' 메서드에서 'T' 형식 매개 변수로 사용할 수 없습니다. 'object'에서 'IKs'(으)로의 암시적 참조 변환이 없습니다.
>
(1) where
와 struct
키워드로 형식 매개 변수 T
에 값 형식만을 받는 제네릭 클래스를 만들 수 있습니다. 참조 형식이 들어오면 아래와 같이 컴파일 에러가 발생합니다.
> public class CarValue<T> where T : struct { } // 값 형식만
> CarValue<int> c = new CarValue<int>(); // struct 성공
> CarValue<string> c = new CarValue<string>();
(1,18): error CS0453: The type 'string' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'CarValue<T>'
(1,35): error CS0453: The type 'string' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'CarValue<T>'
(2) class
제약 조건을 부여하면 참조 형식만 받는 제네릭 클래스를 생성할 수 있습니다. 마찬가지로 값 형식이 들어오면 컴파일 에러가 발생합니다.
public class CarReference<T> where T : class { } // 참조 형식만
> CarReference<string> cs = new CarReference<string>(); // class 성공
> CarReference<decimal> cs = new CarReference<decimal>();
(1,23): error CS0452: The type 'decimal' must be a reference type in order to use it as parameter 'T' in the generic type or method 'CarReference<T>'
(1,45): error CS0452: The type 'decimal' must be a reference type in order to use it as parameter 'T' in the generic type or method 'CarReference<T>'
(3) 제약 조건으로 제공하는 new()
형태는 기본 생성자가 반드시 있는 클래스임을 의미합니다. 기본 생성자가 없이 매개 변수가 있는 생성자를 T
에 전달하면 에러가 발생합니다.
> public class CarNew<T> where T : new() { } // Default 생성자
> public class GoodCar { }
> public class BadCar { public BadCar(string message) { } }
> CarNew<GoodCar> cn = new CarNew<GoodCar>(); // new() 성공
> CarNew<BadCar> bad = new CarNew<BadCar>();
(1,16): error CS0310: 'BadCar' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'CarNew<T>'
(1,33): error CS0310: 'BadCar' must be a non-abstract type with a public parameterless constructor in order to use it as parameter 'T' in the generic type or method 'CarNew<T>'
위 코드에서 BadCar 클래스는 매개 변수가 있는 생성자만 있고 기본 생성자가 없을 때에는 CarNew<T>에 전달되면 에러가 발생됩니다.
(4) 제네릭 제약 조건에 명확하게 특정 클래스 또는 인터페이스를 지정할 경우에는 해당 형식만을 사용할 수 있습니다.
다음 코드의 CarClass<T>
는 반드시 GoodCar
클래스를 상속한 클래스만이 올 수 있습니다. OfficeCamper
클래스는 GoodCar
클래스로부터 파생된 클래스이기에 CarClass<T>
에 전달이 가능합니다. BadCar
클래스를 T
에 전달하면 에러가 발생합니다.
> public class GoodCar { }
> public class CarClass<T> where T : GoodCar { } // GoodCar에서 파생
> public class OfficeCamper : GoodCar, IKs { }
> CarClass<OfficeCamper> cc = new CarClass<OfficeCamper>();
> CarClass<BadCar> badCar;
(1,18): error CS0311: The type 'BadCar' cannot be used as type parameter 'T' in the generic type or method 'CarClass<T>'. There is no implicit reference conversion from 'BadCar' to 'GoodCar'.
(5) CarInterface<T>
는 IKs
인터페이스만을 매개 변수로 받을 수 있습니다. 그 이외의 형식이 지정되면 에러가 발생합니다.
> public class CarInterface<T> where T : IKs { } // IKs인터페이스
> CarInterface<IKs> h = new CarInterface<IKs>();
> CarInterface<object> ie;
(1,22): error CS0311: The type 'object' cannot be used as type parameter 'T' in the generic type or method 'CarInterface<T>'. There is no implicit reference conversion from 'object' to 'IKs'.
우리가 새롭게 만드는 형식<T>
의 구조에서 T
에 대한 제약을 둘 수 있습니다. 물론, 위에서 나열한 내용 이외에 더 많은 조건을 다룰 수 있습니다만, 이 책에서는 한번 정도 다뤄보는 정도로 마무리 하겠습니다.
제네릭의 T
형식 매개 변수를 여러 개 사용하기
이번에는 제네릭의 T
형식 매개 변수를 여러 개 사용하는 방법 알아보겠습니다. 일반적으로 형식 매개 변수는 관행적으로 T, V, …
형태로 대문자로 시작하는 문자 또는 문자열을 사용할 수 있습니다.
코드: GenericsDemo.cs
> //[1] 형식 매개 변수 2개 사용
> class Pair<T, V>
. {
. public T First { get; set; }
. public V Second { get; set; }
. public Pair(T first, V second)
. {
. First = first;
. Second = second;
. }
. }
>
> //[A] string, bool 2개 형식 받기
> var my = new Pair<string, bool>("나는 멋져!", true);
. Console.WriteLine($"{my.First}: {my.Second}");
나는 멋져!: True
>
> //[B] int, double 2개 형식 받기
> var tuple = new Pair<int, double>(1234, 3.14);
> Console.WriteLine($"{tuple.First}, {tuple.Second}");
1234, 3.14
[1]
번 코드에서 Pair<T, V>
형태로 2개의 형식 매개 변수를 받는 제네릭 클래스를 만들었습니다. [A]
에서는 string
, bool
을 받고 [B]
에서는 int
, double
을 받아서 사용하는 형태로 2개의 값을 받아 출력해 보았습니다.
제네릭 클래스와 제네릭 메서드
형식 매개 변수 T
는 클래스에 사용될 수 있으며 마찬가지로 필드, 속성, 메서드의 매개 변수 형식 또는 반환 형식에 사용될 수 있습니다.
제네릭 클래스에서 제네릭 멤버를 함께 사용하는 예제를 살펴보겠습니다. 프로젝트기반 소스는 GenericMethod.cs 파일에서 살펴볼 수 있습니다.
(1) 제네릭 클래스를 생성하고 제네릭 멤버를 추가합니다.
> //[1] 제네릭 클래스 설계
> public class Hello<T>
. {
. private T _message; //[A] 필드
. public Hello() { _message = default(T); } //[B] 기본 생성자
. public Hello(T message) { this._message = message; } //[C] 매개 변수가 있는 생성자
. public void Say(T message) =>
. Console.WriteLine("{0}", message); //[D] 제네릭 메서드
. public T GetMessage() => this._message; //[E] 일반 메서드
. }
(2) Hello
클래스의 기본 인스턴스를 생성 후 GeMessage()
메서드를 출력하면 기본 생성자에 의해서 넘겨 준 T
의 기본 값이 출력됩니다.
> (new Hello<string>()).GetMessage()
null
> (new Hello<int>()).GetMessage()
0
(3) T
에 string
을 입력 후 매개 변수가 있는 생성자에 문자열을 전달한 후 GetMessage()
메서드를 호출하면 전달된 문자열이 그대로 출력됩니다.
> (new Hello<string>("안녕")).GetMessage()
"안녕"
(4) decimal
형식을 생성자로 전달 후 출력해 보는 내용입니다.
> (new Hello<decimal>(12.34m)).GetMessage()
12.34
(5) 제네릭 메서드인 Say()
메서드는 전달된 T
형식을 받아서 그대로 출력합니다. 전달된 T
의 형식이 string
이면 string
형식을, double
이면 double
형식을 Say()
메서드의 매개 변수로 받습니다.
> (new Hello<string>().Say("Say Hello"))
Say Hello
> (new Hello<double>().Say(3.14))
3.14
장 요약
사용자 정의 클래스를 형식 매개 변수로 받는 중요한 제네릭 클래스와 제네릭 인터페이스를 사용해보았습니다. 그런 다음에 직접 제네릭 클래스를 만들어보았습니다. 제네릭 클래스는 C#에서 굉장히 중요한 역할을 합니다. 많은 수의 제네릭 클래스를 사용 후 직접 만들어보았기에 제네릭 클래스의 사용의 어려움은 없을 것으로 봅니다.