230 likes | 385 Views
C#. 06a 장 . 컬렉션. 컬렉션 : 변수들의 조직적인 집합 컬렉션 클래스 : 컬렉션 집합을 저장하고 관리하는 클래스 컬렉션의 종류 : 배열 , 배열 리스트 , 해시 테이블 , 큐 , 스택 컬렉션의 네임 스페이스 : System.Collections. 비제네릭 컬렉션 ; System.Collections 제네릭 컬렉션 : System.Collections.Generic. 제너릭. 제너릭 (Generic)
E N D
C# 06a장. 컬렉션
컬렉션 : 변수들의 조직적인 집합 • 컬렉션 클래스 : 컬렉션 집합을 저장하고 관리하는 클래스 • 컬렉션의 종류 : 배열, 배열 리스트, 해시 테이블, 큐, 스택 • 컬렉션의 네임 스페이스 : System.Collections • 비제네릭 컬렉션 ; System.Collections • 제네릭 컬렉션 : System.Collections.Generic
제너릭 제너릭(Generic) : 타입 인수를 사용하여 일반화된 클래스나 메서드를 정의하는 기법 C# 2.0 부터 지원 C++의 템플릿과 유사
제너릭이 필요한 이유 class CSTest { static void Main() { WrapperIntgi = new WrapperInt(1234); gi.OutValue(); WrapperStringgs = new WrapperString("문자열"); gs.OutValue(); } } using System; class WrapperInt { int Value; public WrapperInt() { Value = 0; } public WrapperInt(intaValue) { Value = aValue; } public int Data { get { return Value; } set { Value = value; } } public void OutValue() { Console.WriteLine(Value); } } class WrapperString { string Value; public WrapperString() { Value = null; } public WrapperString(string aValue) { Value = aValue; } public string Data { get { return Value; } set { Value = value; } } public void OutValue() { Console.WriteLine(Value); } } 1. int형을 정의한 클래스 2 개의 클래스(WrapperInt, WrapperString)가 모두 내부 코드는 동일하다. 제네릭으로 간단히 정의 가능 2. string 형을 정의한 클래스
제너릭이 필요한 이유 using System; class Wrapper<T> { T Value; public Wrapper() { Value = default(T); } public Wrapper(T aValue) { Value = aValue; } public T Data { get { return Value; } set { Value = value; } } public void OutValue() { Console.WriteLine(Value); } } class CSTest { static void Main() { Wrapper<int> gi = new Wrapper<int>(1234); gi.OutValue(); Wrapper<string> gs = new Wrapper<string>("문자열"); gs.OutValue(); } } 1개의 클래스(Wrapper<T>)로 정의 선언문의 <T>가 타입 인수(Type Parameter)임 T는 실제 타입을 위한 자료 표시이며실제 타입은 객체를 생성할 때 지정된다. 타입 인수는 모든 곳에 사용 가능함(필드, 프로퍼티의 타입, 메서드의리턴값, 메서드의 인수 타입 등) 예제에서는 3군데에서 사용됨- Value 필드- 생성자의 인수 aValue- Data 프로퍼티의 타입
T의 실제 타입 지정 하는 곳 : 객체 생성문의<>괄호 안에 지정 • 앞의 예에서 Wrapper<int>, Wrapper<string>으로 정의하였다. 이와 같이 Wrapper<double>, Wrapper<double> 같이 많은 클래스를 정의할 수 있다. • 제네릭은 클래스를 찍어내는 형틀이다. class Wrapper<int> { int Value; public Wrapper() { Value =0; } public Wrapper(string aValue) { Value = aValue; } public int Data int class Wrapper<string> { string Value; public Wrapper() { Value =null; } public Wrapper(string aValue) { Value = aValue; } public string Data class Wrapper<T> { T Value; public Wrapper() { Value = default(T); } public Wrapper(T aValue) { Value = aValue; } public T Data string double class Wrapper<double> { double Value; public Wrapper() { Value =0.0; } public Wrapper(double aValue) { Value = aValue; } public double Data
개방형 타입 : 아직 타입이 결정되지 않은 Wrapper<T> • 폐쇄형 타입 : 타입이 결정된 Wrapper<int> • 개방형 타입은 클래스를 만드는 도구일 뿐 실제 클레스는 아니므로 객체를 생성하지 못한다. • 제네릭 타입 구체화(Generic Type Instantiation): 개방형 타입의 타입 인수를 지정하여 폐쇄형 타입인 클래스를 생성하는 것 • T가 값 타입일 경우 : 컴파일러가 각 타입별로 구체화 한다. • T가 참조 타입일 경우 : 하나의 클래스만 생성되고 모든 참조 타입에 대해 생성된 클래스를 재사용한다. • default 키워드 : T의 기본값을 정의함 • 제네릭에서는T 가 어떤 타입이 될지 미리 알 수 없으므로 Value=0,Value=null식으로 상수를 대입할 수 없다. • default(T)라는 표현식으로 T 에 따른 기본값을 표현한다.
제너릭을 이용한 2 개의 값 교환 using System; class CSTest { static void Swap<T>(ref T a, ref T b) { T t; t = a; a = b; b = t; } static void Main() { int i1 = 3, i2 = 4; Console.WriteLine("i1 = {0}, i2 = {1}", i1, i2); Swap(ref i1, ref i2); //Swap<int>(ref i1, ref i2); //<int> 생략 가능 Console.WriteLine("i1 = {0}, i2 = {1}", i1, i2); string s1 = "멍멍", s2 = "꼬꼬댁"; Console.WriteLine("s1 = {0}, s2 = {1}", s1, s2); Swap(ref s1, ref s2); //Swap<string>(ref s1, ref s2); Console.WriteLine("s1 = {0}, s2 = {1}", s1, s2); } } using System; class CSTest { static void Swap(ref int a, ref int b) { int t; t = a; a = b; b = t; } static void Swap(ref string a, ref string b) { string t; t = a; a = b; b = t; } static void Main() { int i1 = 3, i2 = 4; Console.WriteLine("i1 = {0}, i2 = {1}", i1, i2); Swap(ref i1, ref i2); Console.WriteLine("i1 = {0}, i2 = {1}", i1, i2); string s1 = "멍멍", s2 = "꼬꼬댁"; Console.WriteLine("s1 = {0}, s2 = {1}", s1, s2); Swap(ref s1, ref s2); Console.WriteLine("s1 = {0}, s2 = {1}", s1, s2); } }
제약 조건 • 제너릭 타입 인수 T는 별다른 지정이 없으면 모든 타입을 적용할 수 있다. • 제약 조건은 제네릭 선언문에 where 와 함께 지정하며 다음과 같은 종류가 있다.
값 타입만 가능한 예제(제네릭) using System; class Wrapper<T> where T : struct { T Value; public Wrapper() { Value = default(T); } public Wrapper(T aValue) { Value = aValue; } public T Data { get { return Value; } set { Value = value; } } public void OutValue() { Console.WriteLine(Value); } } class CSTest { static void Main() { Wrapper<int> gi = new Wrapper<int>(1234); gi.OutValue(); //Wrapper<string> gs = new Wrapper<string>("문자열"); //gs.OutValue(); } } 왼쪽의 마지막 2줄을 실행 시켰을 때 메시지
앞의 예는 Wrapper 제네릭T는 값 타입만 가능하다. • Wrapper<int>는 가능하지만 Wrapper<string>이나 Wrapper<Human>은 사용할 수 없다. • where T:class 로 바꾸면 T는 참조 타입만 사용할 수 있고 값 타입은 사용할 수 없게 된다. • 제약 조건 중 가장 실용적인 것은 where T: base 형식이다. 이 조건은 T를 base나 base 파생 클래스로 제한한다.
제약 조건 where T:base using System; class Human//1. Human 클래스 정의 { public virtual void Intro() { Console.WriteLine("나 사람"); } } class Student : Human // 2. Student 파생 클래스 정의 { public override void Intro() { Console.WriteLine("나 학생"); } } class CSTest//3. 제너릭메서드 정의 { public static void OutValue<T>(T man) where T : Human { man.Intro(); } // 타입 인수 T 의 객체 man 을 인수로 받아 man.Intro호출 // T 가 Human의 후손이라는 제약 조건이 있기 때문에 호출 가능 static void Main() { Human A = new Human(); Student B = new Student(); string C = "나 문자열"; OutValue(A); OutValue(B); //OutValue(C); } } C 객체는 string 타입이며 Human과는 관계가 없기 때문에 컴파일 되지 않는다. string 클래스는 Intro 메서드를 가지고 있지 않기 때문에 이 타입의 객체로는 OutValue가 동작하자 않아 컴파일 거부가 된다.
제약 조건 where T:base 제약 조건 제거 시 using System; class Human { public virtual void Intro() { Console.WriteLine("나 사람"); } } class Student : Human { public override void Intro() { Console.WriteLine("나 학생"); } } class CSTest { public static void OutValue<T>(T man) { Human t = man as Human; if (t != null) { t.Intro(); } } static void Main() { Human A = new Human(); Student B = new Student(); string C = "나 문자열"; OutValue(A); OutValue(B); OutValue(C); } } 제약 조건 없이 제네릭으로 작성한 예제임 man 을 Human으로 캐스팅하여 성공하면 호출하고 그렇지 않으면 아무런 동작도 하지 않는다 그렇기 때문에 문자열 같은 잘못된 타입이 전달되어도 호출되었다가 그냥 리턴 함. 제약 조건은 컴파일할 때 타입을 체크하여 불가능한 호출을 원천적으로 차단하고 캐스팅을 쵷소화 하는 역할을 한다.
제너릭 컬렉션 • 제네릭은 원래 C# 언어의 스펙에 포함되어 있던 기능이 아니다. • 제네릭은 문법을 복잡하게 만들고 컴파일을 느리게 만드는 주범인데다 컴파일러까지 복잡해져 비용이 많이 든다. • C#이 이런 비용을 감수해 가며 2.0 부터 제네릭을 지원하는 가장 큰 이유는 제네릭 컬렉션 클래스를 지원하기 위함이다.
제너릭 컬렉션 • 기본적인 자료의 집합을 관리하는 컬렉션은 모든 응용 프로그램에 필수적인 자료 구조 이다. • C#은 처음부터 컬렉션 클래스를 지원 하였지만, 제네릭 이전에는 일반 클래스였으며 일반 클래스에는 문제점이 있었다.
일반 클래스의 제네릭 using System; using System.Collections; class CSTest { static void Main() { ArrayListar = new ArrayList(10); ar.Add(1); ar.Add(2.34); ar.Add("string"); inti = (int)ar[0]; double d = (double)ar[1]; string str = (string)ar[2]; Console.WriteLine("{0}, {1}, {2}", i, d, str); } } 일반 컬렉션의 요소 타입은 object 이므로 임의의 요소를 저장할 수 있다. 어떤 객체든지 컬렉션에 넣을 수 있으며 이를 막을 수 있는 문법적인 방법이 전혀 없다. 예제에서 정수, 실수, 문자열을 하나의 배열에 넣을 수 있다. 빼 낼 때는 과다한 캐스팅이 발생함
일반 클래스의 컬렉션 관련 문제점 • 컬렉션에 저장된 정보를 읽을 때 object 타입으로 리턴되므로 원하는 타입으로 캐스팅해야 한다. • 그러기 위해서는 컬렉션에 어떤 타입의 객체가 저장되어 있는지 일일이 기억해 놓거나 아니면 실행 중에 타입을 조사해야 하는데 이 작업이 아주 번거롭다. 부모는 자식을 가리킬 수 있기 때문에 넣을 때는 아무 것이나 넣을 수 있지만 빼낼 때는 그렇지 못한 것이다. 위 코드에서 캐스트 연산자를 빼고 int I = ar[0]; 로 수정하면 에러가 난다.
일반 클래스의 컬렉션 관련 문제점 • 캐스팅을 잘못하면 위험해진다. ar[0]를 string 타입으로 캐스팅하여 읽으면 정수가 가리키는 번지를 읽으려고 시도할 것이므로 잘못하면 다운될 수도 있다. • 컴파일러가 이런 위험한 문장을 에러로 처리할 수 없는 이유는 ar[0]에 어떤 타입의 객체가 저장될지 컴파일 중에는 알 방법이 없기 때문이다. • C#은 이런 문제를 해결하기 위하여 is, as 같은 연산자를 제공하기는 하지만 이 방법은 실행 중에만 쓸 수 있어 불편할 뿐만 아니라 완전하지도 않다.
일반 클래스의 컬렉션 관련 문제점 • 값 타입을 컬렉션에 저장할 때는 object 타입으로 변환하는 박싱이 필요하고 꺼낼 때는 언박싱이필요한다. 이 처리는 컴퓨터가 자동으로 해 주지만 성능상의 불이익은 피할 수없다. • 정수 값 하나늘 넣어도 object로 바꾼 후에 넣기 때문에 메모리도 많이 소모되고 속도도 느리다. • 이러한 문제가 발생하는 근본 원인은 컬렉션에 저장될 수 있는 타입이 너무 일반적이어서 컴파일러가 잘못된 코드를 적발해 낼 수 있는 정보가 충분하지 않기 때문이다. • 제너릭을 사용하면 타입 인수로 처리 대상을 지정할 수 있으므로 위의 문제를 해결할 수 있다.
일반 클래스의 컬렉션 관련 문제점 • 닷넷는 기존의 컬렉션 클래스를 대체할 수 있는 제네릭 컬렉션을 제공한다. • ArrayList의 제네릭 버전은 List<T> 이다.
ArrayList의 제네릭 버전인 List<T> 예제(문자열의 컬렉션 관리) using System; using System.Collections.Generic; class CSTest { static void Main() { List<string> ar = new List<string>(10); ar.Add("이승만"); ar.Add("박정희"); ar.Add("최규하"); //ar.Add(1234); //ar.Add(5.678); foreach (string s in ar) Console.Write(s + ","); } } 네임 스페이스 : System.Collections.Generic ar은 List<string>타입으로 선언되었으므로 문자열만 저장할 수 있다.
제네릭의 장점 • 컬렉션에 저장 가능한 타입이 선언 시점에 명시되므로 컴파일러는 어떤 타입이 안전하게 저장될 수 있는지 분명하게 알 수 있다. • 실행 중이 아닌 컴파일 타임에 수행할 수 있다는 점이 제네릭의 장점이다. • 저장되는 타입이 정해져 있으므로 꺼낼 때도 캐스팅을 할 필요가 업으며 실수를 할 잠재적인 위험도 없다.
제네릭의 장점 • List의 프로퍼티나 메서드는ArrayList와 거의 동일하다. • ArrayList는 비제네릭 버전이고 List는 이 클래스를 새로 만든 제네릭 버젼이기 때문에 인터페이스가 비슷하다 • 닷넷 공식 문서에서는 가능하면 제레릭 버전을 쓸 것을 권장한다. • List가 가장 자주 사용되는 범용적인 컬렉션이지만 이외에도 이중 연결 리스트를 제공하는 LinkedList가 있고, Queue, Stack, Dictionary 같은 제네릭 컬렉션도 제공한다.