<4D F736F F D20BEBEBCA520C4DAB5F920BFACBDC0202D20C4C3B7BABCC72C20DFED2E646F6378>

Similar documents
JAVA PROGRAMMING 실습 08.다형성

<4D F736F F D20BEBEBCA520C4DAB5F920BFACBDC0202D20B8D6C6BC20BEB2B7B9B5E5BFCD20C0CCBAA5C6AE2E646F6378>

PowerPoint Presentation

JAVA PROGRAMMING 실습 05. 객체의 활용

PowerPoint Presentation

선형대수학 Linear Algebra

gnu-lee-oop-kor-lec06-3-chap7

q 이장에서다룰내용 1 객체지향프로그래밍의이해 2 객체지향언어 : 자바 2

PowerPoint Presentation

PowerPoint Presentation

Microsoft PowerPoint - CSharp-10-예외처리

<4D F736F F F696E74202D20C1A63038C0E520C5ACB7A1BDBABFCD20B0B4C3BC4928B0ADC0C729205BC8A3C8AF20B8F0B5E55D>

설계란 무엇인가?

Microsoft PowerPoint - 2강

PowerPoint Presentation

C# Programming Guide - Types

JAVA 프로그래밍실습 실습 1) 실습목표 - 메소드개념이해하기 - 매개변수이해하기 - 새메소드만들기 - Math 클래스의기존메소드이용하기 ( ) 문제 - 직사각형모양의땅이있다. 이땅의둘레, 면적과대각

PowerPoint Presentation

chap 5: Trees

쉽게 풀어쓴 C 프로그래밍

Cluster management software

JAVA PROGRAMMING 실습 09. 예외처리

Microsoft PowerPoint - C++ 5 .pptx

(Microsoft Word - C# \304\332\265\371 \277\254\275\300.docx)

슬라이드 1

Microsoft PowerPoint - additional01.ppt [호환 모드]

Microsoft PowerPoint - Java7.pptx

No Slide Title

Microsoft PowerPoint - java1-lab5-ImageProcessorTestOOP.pptx

C++ Programming

Microsoft PowerPoint 자바-기본문법(Ch2).pptx

PowerPoint Presentation

제11장 프로세스와 쓰레드

JVM 메모리구조

Microsoft PowerPoint - chap02-C프로그램시작하기.pptx

쉽게 풀어쓴 C 프로그래밍

PowerPoint 프레젠테이션

<322EBCF8C8AF28BFACBDC0B9AEC1A6292E687770>

쉽게 풀어쓴 C 프로그래밍

슬라이드 1

JAVA PROGRAMMING 실습 02. 표준 입출력

PowerPoint 프레젠테이션

PowerPoint Presentation

JUNIT 실습및발표

(Microsoft PowerPoint - 07\300\345.ppt [\310\243\310\257 \270\360\265\345])

설계란 무엇인가?

쉽게

PowerPoint Template

Microsoft PowerPoint - ch09 - 연결형리스트, Stack, Queue와 응용 pm0100

[ 마이크로프로세서 1] 2 주차 3 차시. 포인터와구조체 2 주차 3 차시포인터와구조체 학습목표 1. C 언어에서가장어려운포인터와구조체를설명할수있다. 2. Call By Value 와 Call By Reference 를구분할수있다. 학습내용 1 : 함수 (Functi

슬라이드 1

Microsoft PowerPoint - ch07 - 포인터 pm0415

A Tour of Java V

유니티 변수-함수.key

Microsoft PowerPoint - CSharp-2-기초문법

A Hierarchical Approach to Interactive Motion Editing for Human-like Figures

Chapter 4. LISTS

PowerPoint 프레젠테이션

PowerPoint Presentation

PowerPoint 프레젠테이션

PowerPoint Presentation

. 스레드 (Thread) 란? 스레드를설명하기전에이글에서언급되는용어들에대하여알아보도록하겠습니다. - 응용프로그램 ( Application ) 사용자에게특정서비스를제공할목적으로구현된응용프로그램을말합니다. - 컴포넌트 ( component ) 어플리케이션을구성하는기능별요

11장 포인터

Poison null byte Excuse the ads! We need some help to keep our site up. List 1 Conditions 2 Exploit plan 2.1 chunksize(p)!= prev_size (next_chunk(p) 3

PowerPoint 프레젠테이션

Microsoft PowerPoint - Lect04.pptx

PowerPoint 프레젠테이션

강의 개요

OCW_C언어 기초

adfasdfasfdasfasfadf

비트와바이트 비트와바이트 비트 (Bit) : 2진수값하나 (0 또는 1) 를저장할수있는최소메모리공간 1비트 2비트 3비트... n비트 2^1 = 2개 2^2 = 4개 2^3 = 8개... 2^n 개 1 바이트는 8 비트 2 2

gnu-lee-oop-kor-lec11-1-chap15

작성자 : 김성박\(삼성 SDS 멀티캠퍼스 전임강사\)

금오공대 컴퓨터공학전공 강의자료

@OneToOne(cascade = = "addr_id") private Addr addr; public Emp(String ename, Addr addr) { this.ename = ename; this.a

쉽게 풀어쓴 C 프로그래밍

Microsoft PowerPoint 장강의노트.ppt

Microsoft PowerPoint - Lect07.pptx

Microsoft Word - Crackme 15 from Simples 문제 풀이_by JohnGang.docx

Microsoft PowerPoint - MonthlyInsighT-2018_9월%20v1[1]

학습목표 함수프로시저, 서브프로시저의의미를안다. 매개변수전달방식을학습한다. 함수를이용한프로그래밍한다. 2

Microsoft Word - java19-1-midterm-answer.doc

iii. Design Tab 을 Click 하여 WindowBuilder 가자동으로생성한 GUI 프로그래밍환경을확인한다.

Design Issues

JAVA PROGRAMMING 실습 02. 표준 입출력

Network Programming

PowerPoint Presentation

Microsoft PowerPoint - chap04-연산자.pptx

PowerPoint Presentation

슬라이드 1

Microsoft PowerPoint - Chap12-OOP.ppt

JAVA PROGRAMMING 실습 05. 객체의 활용

Tcl의 문법

Chapter 4. LISTS

1. auto_ptr 다음프로그램의문제점은무엇인가? void func(void) int *p = new int; cout << " 양수입력 : "; cin >> *p; if (*p <= 0) cout << " 양수를입력해야합니다 " << endl; return; 동적할

학습목차 2.1 다차원배열이란 차원배열의주소와값의참조

2002년 2학기 자료구조

PowerPoint Presentation

예제 2) Test.java class A intvar= 10; void method() class B extends A intvar= 20; 1"); void method() 2"); void method1() public class Test 3"); args) A

Spring Data JPA Many To Many 양방향 관계 예제

Transcription:

C# 코딩연습 컬렉션, 序 2008-03-12 김태현 (kimgwajang@hotmail.com)

이문서는세편으로기획된 C# 코딩연습 컬렉션 시리즈중첫번째입니다. 이시리즈는 닷넷프레임웍에서차지하는컬렉션의높은비중을이해하고, 이를효율적으로사용하여코드의 품질을높이는방법에대한고민을나누는것을목적으로기획되었습니다. 현재이시리즈는총세편으로기획되어있으며각각의주제는다음과같습니다. 서 : 닷넷프레임웍의컬렉션을이해하기위한기반기술들을살펴봅니다. 특히컬렉션의내부에서사용되는인터페이스와대리자와클래스등의역할에중점을둡니다. 본 : 가장빈번히사용되는동시에다른고급컬렉션들의기반이되는세가지컬렉션들을직접만들어보며컬렉션의내부구조와로직을이해합니다. 이는컬렉션의내부구조와로직을이해함으로써컬렉션을효율적으로사용하는데목적이있습니다. 결 : 닷넷베이스클래스라이브러리에포함된컬렉션의한계를보완하는두개의공개된컬렉션셋을소개합니다. 이들컬렉션셋은필드에서바로사용할수있을만큼의높은완성도를가지고있습니다. I. 序의序 컬렉션 : 서로밀접하게관련된데이터를그룹화하여좀더효율적으로처리 할수있게한특수한클래스혹은구조체 (MSDN 라이브러리 ) 컬렉션을사용하면데이터를효율적으로처리할수있다는말을거꾸로생각해봅시다. 대표적인컬렉션인배열이없다고가정을해볼까요? 배열이없다면우리가일상적으로작성하는코드는어떻게될까요? 예를들어 100 명의시험점수를저장해야한다면 100 개의변수를만들어야할까요? 어떻게든구현은된다하더라도, 그너저분함은생각하기도싫을만큼끔찍할것입니다. 하지만이런우울한상황은배열이라는컬렉션을도입하는것만으로훨씬효율적으로변경될수있습니다. 1

그렇다면배열이만병통치약일까요? 물론그렇지는않습니다. 배열은나름의장점을분명히가지고있지만, 동시에다른컬렉션들에비해서적지않은단점도가지고있습니다. 대표적으로요소를저장할수있는용량이고정되어있다는점을들수있겠습니다. 그래서경우에따라서는배열이아닌다른컬렉션을선택하여야하는상황도얼마든지있습니다. 베이스클래스라이브러리에는우리가사용할수있는수많은컬렉션이있고, 또서드파티라이브러리로제공되는컬렉션도부지기수입니다. 물론사용자가고유한컬렉션을만들수도있고요. 그렇다면이많은컬렉션중에서상황에가장적합한컬렉션을선택하는것이관건이되겠습니다. 여기서 선택 이라는말이중요합니다. 대부분의경우에있어서우리는이미존재하는컬렉션 중에서가장적합한것을고르게됩니다. 아마도기존의컬렉션으로는도저히처리할수없는 특별한기능을가진컬렉션을만드는일은거의없을것입니다. 우리의목표는뛰어난컬렉션을만드는것이아닙니다. 사용목적에가장적합한컬렉션을선택하여정확하게사용하는것이목표입니다. 그렇다면어떻게하면이 선택 을잘할수있을까요? 아는만큼보인다 는말이있지요. 컬렉션의경우에도그렇습니다. 우리가일상적으로사용하는컬렉션에대해서더많이알수록, 우리는컬렉션을더잘선택할수있을것입니다. 그때에컬렉션을사용하는것은확실히예전과다르겠지요. 하지만컬렉션, 특히그내부구현에대해서아는것은말처럼간단하지가않습니다. 베이스클래스라이브러리를포함하여, 공개된컬렉션라이브러리들의복잡함은그완성도를감안할때쉬이짐작할수있을것입니다. 실제로닷넷프레임웍의컬렉션클래스들의소스를보면정말이지대단합니다. 수많은예외상황들에대한처리는기본이고, 쓰레드안정성, 내부버전관리, 얕은복사와깊은복사, 거미줄처럼얽혀있는상속과포함관계등을모두고려하려면당연히복잡할수밖에없겠지요. 2

다시한번강조하지만, 우리의목표는베이스클래스라이브러리수준의컬렉션을만드는것이아닙니다. 실제필드에서사용할목적으로 List<T> 나 Dictionary<T> 와같은컬렉션을다시만드는것은현명하지못한방법입니다. 대신에우리는각컬렉션을최대한으로간소화시킨모델을이용하여그핵심로직을이해하고, 그에대한이해를바탕으로상황에맞는컬렉션을선택하고또효율적으로사용하는전략에집중을할것입니다. II. 컬렉션과인터페이스 모든닷넷컬렉션은적어도 ICollection<T> 인터페이스는구현을합니다. 바꾸어말하면모든닷넷컬렉션은 ICollection<T> 인터페이스에정의되어있는연산을수행할수있습니다. 이에더하여몇몇컬렉션들은추가적인인터페이스를구현하기도합니다. 예를들어, Stack<T> 클래스는 ICollection<T> 을구현하므로 ICollection<T> 에정의되어있는 Add 와 Remove 같은작업을수행할수있습니다. 반면에 List<T> 클래스의경우에는 IList<T> 인터페이스를구현하므로 ICollection<T> 인터페이스와 IList<T> 인터페이스에있는연산을모두수행할수있습니다. IList<T> 에정의되어있는 Insert 와 RemoveAt 같은연산은 Stack<T> 에서는지원되지않고 List<T> 에서만지원이된다는이야기이지요. 닷넷컬렉션의연산을정의하고있는인터페이스들에대해서좀더자세히살펴봅시다. 먼저 다음클래스다이어그램을보지요. 3

그림 1 주요컬렉션인터페이스들의클래스다이어그램 물론베이스클래스라이브러리에는훨씬더많은수의컬렉션인터페이스들이있습니다. 이 그림에서는최상위에있는가장중요한몇가지만을표시하였습니다. 각각의인터페이스들은계층구조를형성하고있습니다. 예컨데 ICollection<T> 는 IEnumerable<T> 를상속하고있으며, IList<T> 는 ICollection<T> 를상속하고있습니다. 따라서 IList<T> 역시 IEnumerable<T> 를간접적으로상속한다고할수있겠습니다. 그러나사실은, IList<T> 는 ICollection<T> 를통해 IEnumerable<T> 를간접적으로상속하는것이아니라, 직접 IEnumerable<T> 를상속합니다. 하지만 ICollection<T> 가 IEnumerable<T> 의모든계약을상속하고있기때문에, ICollection<T> 를상속하면 IEnumerable<T> 는자동적으로상속을하게되는것입니다. 4

대부분의컬렉션클래스들은적어도 ICollection<T> 를구현합니다. Stack<T> 이나 Queue<T> 등이이에속합니다. 조금더특수한컬렉션들은 IList<T> 나 IDictionary<TKey, TValue> 혹은더특화된인터페이스를구현하기도합니다. 보통인덱스로요소에접근하는것을허용하는컬렉션은 IList<T> 를구현합니다. 배열과 List<T> 가대표적인 IList<T> 를구현하는컬렉션입니다. 반면에키-값쌍으로데이터를저장하는컬렉션은 IDictionary<TKey, TValue> 를구현합니다. Dictionary<TKey, TValue> 를예로들수있겠네요. IList<T> 와 IDictionary<TKey, TValue> 가모두 ICollection<T> 을상속하는것에주목하여주십시오. 따라서 IList<T> 혹은 IDictionary<TKey, TValue> 를구현하는컬렉션은동시에 ICollection<T> 을구현한것이기도합니다. 즉 IList<T> 나 IDictionary<TKey, TValue> 을구현한컬렉션은 ICollection<T> 만구현한컬렉션에비해서좀더특화된컬렉션이라고할수있겠습니다. 이제부터는위클래스다이어그램에등장하는각각의인터페이스에대해서하나씩알아보도록 하겠습니다. 1. IEnumerable 과 IEnumerable<T> IEnumerable 인터페이스는이름이의미하는바와같이, 이인터페이스를구현하는클래스를열거가능한클래스로만들어줍니다. 즉어떠한클래스이더라도 IEnumerable 만구현한다면그클래스는열거가능한클래스가되어그클래스를 foreach 문에서사용할수있다는이야기입니다. ( 물론인터페이스를구현할수있는것은클래스만이아니라구조체도가능합니다. 서술의편의상여기서는, 인터페이스를구현하는클래스혹은구조체 라는표현대신에그냥 인터페이스를구현하는클래스 라고만하겠습니다. 이러한문맥에서는특별한언급이없는한, 클래스와구조체를구별하지마시기바랍니다..) 5

또한 IEnumerable<T> 는 IEnumerable 의제네릭버전이고 IEnumerable 를상속합니다. 따라서 여기서는 IEnumerable<T> 에대해서만다루도록하겠습니다. A. 열거 먼저코드부터보면서 IEnumerable<T> 인터페이스가왜필요한지에대해서생각해봅시다. 여자친구들의이름을저장하는 GirlFriendCollection 라는현실성없는클래스를생각해보지요. 1 public class GirlFriendCollection 2 { 3 private string[] _names; 4 5 public GirlFriendCollection(params string[] names) 6 { 7 _names = names; 8 } 9 } 코드 1 5 번라인에서 params 키워드를사용하는것말고는흔한코드이네요. 배열을매개변수로 전달할때 params 키워드를사용하면좀더편리하게활용할수있습니다. 말이나온김에 params 키워드에대해잠시알아보도록하겠습니다. 예를들어아래와같은 메서드가있다고합시다. 1 void Foo(int[] array) 2 { 3 } 4 5 void CallFoo() 6 { 7 Foo(new int[]{1, 2}); 8 } 7 번라인에서보듯이 Foo 메서드에 1 과 2 를넘기고자한다면먼저배열을만든후여기에 1 과 2 를채워서넘겨주어야합니다. 하지만 params 키워드를이용하면, 01 void Foo(params int[] array) 03 } 04 05 void CallFoo() 6

06 { 07 Foo(1, 2); 08 Foo(1, 2, 3); 09 } 와같이그냥 1 과 2 만넘기면컴파일러가자동으로배열을만들어줍니다. 8 번라인처럼 1, 2, 3 세값을넘기는경우에도그냥주욱넘기면됩니다. 다음으로는아래와같이여자친구들의이름을 foreach 문을통해출력을하며이국소녀들의 이름을불러봅시다. 1 private static void Main(string[] args) 2 { 3 GirlFriendCollection mygirls = new GirlFriendCollection(" 패 ", " 경 ", " 옥 "); 4 5 foreach (string girl in mygirls) 6 Console.WriteLine(girl); 7 } 코드 2 5 번라인을확인하시기바랍니다. 의도대로 mygirls 를 foreach 문을통해열거하기위해서는 GirlFriendCollection 클래스를열거가능하게만들어야합니다. 이는이름이의미하는것처럼바로 IEnumerable<T> 를구현하는것입니다. B. 반복기를사용하지않는 IEnumerable<T> 구현 열거가능한클래스로만들기위해구현하여야할 IEnumerable<T> 인터페이스의멤버는 다음과같습니다. (MSDN 라이브러리 ) 메서드 IEnumerator<T> GetEnumerator () 컬렉션을반복하는열거자를반환합니다. 표 1 IEnumerable<T> 의멤버 GirlFriendCollection 가 IEnumerable<T> 인터페이스를구현하는코드는이런형태가되겠지요. 01 public class GirlFriendCollection : IEnumerable<string> 03 private string[] _names; 04 7

05 public GirlFriendCollection(params string[] names) 06 { 07 _names = names; 08 } 09 10 #region IEnumerable<string> Members 11 IEnumerator<string> IEnumerable<string>.GetEnumerator() 12 { 13 throw new System.NotImplementedException(); 14 } 15 #endregion 16 17 #region IEnumerable Members 18 public IEnumerator GetEnumerator() 19 { 20 return ((IEnumerable<string>) this).getenumerator(); 21 } 22 #endregion 23 } 코드 3 위에서이야기했듯이 IEnumerable<T> 는 IEnumerable 인터페이스를상속합니다. 따라서 IEnumerable<T> 를구현하기위해서는 IEnumerable 의멤버인 GetEnumerator 도같이구현하여야합니다. 그것이 18 ~ 21 번라인입니다. 실제구현은 11 ~ 14 번라인의 IEnumerable<string>.GetEnumerator 를다시호출하고있습니다. 또한 11 번라인처럼인터페이스를명시적으로구현하는방법에대해서도유념하시기바랍니다. 코드 3 의경우에는이름이동일한 GetEnumerator 메서드가두개있습니다. ( 각각 IEnumerable 와 IEnumerable<T> 의멤버 ) 따라서아래와같이두인터페이스멤버를모두암시적으로구현을하게되면이름충돌이발생하게됩니다. 01 #region IEnumerable<string> Members 02 public IEnumerator<string> GetEnumerator() 03 { 04 throw new System.NotImplementedException(); 05 } 06 #endregion 07 08 #region IEnumerable Members 09 public IEnumerator GetEnumerator() 10 { 11 return ((IEnumerable<string>) this).getenumerator(); 12 } 13 #endregion 8

코드 4 이를해결하기위해서코드 3 에서는 IEnumerable<T> 의 GetEnumerator 를명시적으로 구현하고있는것입니다. 코드 4 의 11 번라인과같이 IEnumerable.GetEnumerator() 가 IEnumerable<T>.GetEnumerator() 를호출하는코드도유심히보시기바랍니다. 그냥 GetEnumerator() 라고해서는어떤 GetEnumerator() 를말하는지알수가없습니다. 따라서위코드에서는 this. GetEnumerator() 라고호출하는것이아니라 this 를 IEnumerable<string> 로형변환한후 GetEnumerator() 를호출하고있습니다. 어려운문법은아니지만생각을좀해봐야하는부분이네요. 코드 3 에서 IEnumerable 의 GetEnumerator 는 IEnumerable<T> 의 GetEnumerator 를호출하는것으로이미구현이되어있기때문에, 우리는 IEnumerable<T> 의 GetEnumerator 만구현을하면되겠습니다. 인터페이스의정의에의하면이메서드는열거자를반환하는데, 이열거자는 IEnumerator <string> 를구현하여야합니다. 그렇다면우리는 IEnumerator<string> 인터페이스를구현하는클래스나구조체도작성하여야한다는말이되겠네요. 여기서잠시 IEnumerable( 혹은 IEnumerable<T>) 와 IEnumerator( 혹은 IEnumerator<T>) 에 대해서정리를하는게좋을것같습니다. 둘다클래스를열거가능하게하는데사용되는 인터페이스이긴한데, 어떤차이점이있을까요? IEnumerable 은단어의의미대로이인터페이스를구현하면열거가가능한클래스가된다는 의미입니다. 이에반해 IEnumerator 인터페이스를구현한클래스는열거가능한클래스가되는 것이아니라열거자클래스가된다는뜻이고요. 우리의예에서 GirlFriendCollection 클래스는열거가가능한클래스이지, 열거자클래스는 아닙니다. 따라서 GirlFriendCollection 는 IEnumerable 를구현합니다. 그렇다면 IEnumerator 는 언제사용하느냐? 위에서보신것처럼 IEnumerable 의 GetEnumerator() 메서드는반환값으로 9

IEnumerator, 즉 IEnumerator 나이를구현한열거자클래스를반환하여야합니다. 곧보시겠지만, 우리는 GirlFriendCollection 의열거자클래스인 GirlFriendEnumerator 클래스를만들고, 이를 GetEnumerator() 의반환값으로반환할것입니다. 참고로닷넷프레임웍에는이렇게 able 과 er(or) 로끝나는인터페이스쌍이몇가지더있습니다. IComparable 과 IComparer 가대표적인예이지요. 만약에 GirlFriend 라는클래스가있고이를비교가가능한클래스로만들고싶다면 GirlFriend 가 IComparable 를구현하도록만들어야하고, GirlFriend 를비교하는클래스를만들고싶다면 IComparer 를구현하는 GirlFriendComparer 클래스를만들어야할것입니다. IEnumerator<T> 인터페이스는아래와같은멤버를가지고있습니다. (MSDN 라이브러리 ) 속성 T Current { get; } 컬렉션에서열거자의현재위치에있는 요소입니다. 표 2 IEnumerator<T> 의멤버 그리고 IEnumerator<T> 인터페이스는 IEnumerator 와 IDisposable 을상속하기때문에, 우리가 작성하려고하는열거자는아래와같은 IEnumerator 와 IDisposable 의멤버도구현을하여야 합니다. (MSDN 라이브러리 ) 속성 Object Current { get; } 컬렉션의현재요소입니다. 메서드 bool MoveNext () 열거자를컬렉션의다음요소로이동합니다. void Reset () 컬렉션의첫번째요소앞의초기위치에열거자를 설정합니다. 표 3 IEnumerator 의멤버 메서드 void Dispose () 관리되지않는리소스의확보, 해제또는다시 설정과관련된응용프로그램정의작업을 10

수행합니다. 표 4 IDisposable 의멤버 그래서우리가작성하려고하는열거자의이름이 GirlFriendEnumerator 라면, GirlFriendEnumerator 는다음과같이 IEnumerator<T> 와 IEnumerator 와 IDisposable 의멤버를 모두구현하하여야합니다. 01 public class GirlFriendCollection : IEnumerable<string> 03 private string[] _names; 04 05 public GirlFriendCollection(params string[] names) 06 { 07 _names = names; 08 } 09 10 #region IEnumerable<string> Members 11 IEnumerator<string> IEnumerable<string>.GetEnumerator() 12 { 13 GirlFriendEnumerator enumerator = new GirlFriendEnumerator(this); 14 return enumerator; 15 } 16 #endregion 17 18 #region IEnumerable Members 19 public IEnumerator GetEnumerator() 20 { 21 return ((IEnumerable<string>) this).getenumerator(); 22 } 23 #endregion 24 25 private class GirlFriendEnumerator : IEnumerator<string> 26 { 27 private readonly GirlFriendCollection _girlfriends; 28 29 public GirlFriendEnumerator(GirlFriendCollection girlfriends) 30 { 31 _girlfriends = girlfriends; 32 } 33 34 private int _index = -1; 35 36 #region IDisposable Members 37 public void Dispose() 38 { 39 } 40 #endregion 41 42 #region IEnumerator Members 43 public bool MoveNext() 44 { 11

45 _index++; 46 return _index < _girlfriends._names.length; 47 } 48 49 public void Reset() 50 { 51 _index = -1; 52 } 53 54 public object Current 55 { 56 get { return ((IEnumerator<string>)this).Current; } 57 } 58 #endregion 59 60 #region IEnumerator<string> Members 61 string IEnumerator<string>.Current 62 { 63 get 64 { 65 try 66 { 67 return _girlfriends._names[_index]; 68 } 69 catch (IndexOutOfRangeException) 70 { 71 throw new InvalidOperationException(); 72 } 73 74 } 75 } 76 #endregion 77 } 78 } 코드 5 먼저 GirlFriendEnumerator 를 GirlFriendCollection 의내포클래스로구현한것을유심히보아주십시오. GirlFriendEnumerator 가반드시 GirlFriendCollection 의내포된클래스로구현되어야하는것은아닙니다. 여기서는 GirlFriendEnumerator 클래스를 GirlFriendCollection 내부에서만사용하기때문에외부로노출시키지않았습니다. 필드가아닌클래스레벨의캡슐화원칙이라고할수있겠네요. GirlFriendEnumerator 를내포된클래스로선언하는 25 번라인을보면한가지재미있는곳이 있습니다. 클래스의접근지정자가 private 으로되어있습니다. 클래스가다른클래스에내포되지 않고네임스페이스에서바로정의될경우에지정할수있는접근지정자는두가지밖에 12

없습니다. public 과 internal 이지요. 그외의다른세가지접근지정자 (private, protected, protected internal) 은지정할수없습니다. 하지만클래스가다른클래스에내포가된다면다섯 가지의접근지정자를모두사용할수있습니다. 왜일까요? 아마도 C# 을설계한사람들은이문제에대해서많은고민을하고결정을내렸을것입니다. 이글을읽는분들도 C# 을만든사람들의마음을생각하며같은고민을해보시기바랍니다. 그러한고민을많이할수록언어에대한이해의폭이늘어나는것이아닌가합니다. ( 비슷한예로추상클래스의생성자는오직 protected 만이의미가있습니다. 왜그런지에대한답을스스로고민하여찾아낼수있다면, 추상클래스, 더나아가서 OOP 에대한이해가부쩍증가한걸느끼실수가있을것입니다.) 다시 GirlFriendEnumerator 의이야기를계속하자면, GirlFriendEnumerator 는 GirlFriendCollection 형의 _girlfriends 와 int 형의 _index, 두개의필드를가지고있습니다. _girlfriends 는실제로열거를할객체에대한참조이고요, _index 는여자친구들의이름이담긴배열의인덱스를나타냅니다. 11 번라인의 IEnumerable<string>.GetEnumerator 메서드의구현은 GirlFriendEnumerator 객체를생성하여반환하는것으로되어있습니다. 이때 GirlFriendEnumerator 의생성자에매개 변수로 GirlFriendCollection 의객체를넘기는것을확인하시기바랍니다. 37 번라인에서는 IDisposable.Dispose 를구현하는데, 여기서는아무런일도하지않습니다. 43 번라인의 MoveNext 메서드는현재의인덱스를뒤로한칸옮기고, 남아있는요소가있으면 true, 없으면 false 를반환합니다. 그리고 Reset 메서드는현재의인덱스를 -1 로설정을합니다. 인덱스가 0 이라면첫번째요소를가리키는것이므로, -1 은첫번째요소의앞을의미합니다. 마지막으로 Current 속성은현재요소를반환하는데, 위코드에서는단순히 IEnumerator<T>.Current 를반환하고있습니다. 13

61 번라인부터시작하는 IEnumerator<T>.Current 속성이실제로값을열거하는코드인데요, 구현은간단합니다. 단순히이름배열의 _index 번째요소를반환합니다. 또한여기서도 IEnumerator.Current 와 IEnumerator<T>.Current 의이름이중복되기때문에 IEnumerator<T>.Current 를명시적으로구현한것을볼수있습니다. 이제코드 2 는정상적으로실행이됩니다. 패경옥 Press any key to continue... 여기까지가 GirlFriendCollection 클래스에열거기능을추가하기위한작업이었습니다. 간단한열거기능을구현하기위해서꽤많은양의코드를작성하였습니다. 경우에따라서는지루하고반복적인작업이라서실수를할가능성도있습니다. 하지만반갑게도 C# 2.0 에는이러한반복작업을대폭줄일수있는기능이언어에추가되었습니다. 바로반복기를사용하는것입니다. C. 반복기를사용하는 IEnumerable<T> 구현 C# 2.0 에서도입된반복기를사용하면열거자를구현하는작업을획기적으로줄일수 있습니다. 한두줄의코드만추가해주면나머지코드는컴파일러가컴파일타임에자동으로 만들어냅니다. 반복기를지원하기위해서 C# 2.0 에는 yield 라는새로운키워드가도입되었습니다. 바로 예제를보면서이야기를하지요. 아래는반복기를사용하여 GirlFriendCollection 에열거기능을 추가한코드입니다. 01 public class GirlFriendCollection : IEnumerable<string> 03 private string[] _names; 04 05 public GirlFriendCollection(params string[] names) 06 { 07 _names = names; 08 } 09 10 #region IEnumerable<string> Members 14

11 IEnumerator<string> IEnumerable<string>.GetEnumerator() 12 { 13 for (int i = 0; i < _names.length; i++) 14 yield return _names[i]; 15 } 16 #endregion 17 18 #region IEnumerable Members 19 public IEnumerator GetEnumerator() 20 { 21 return ((IEnumerable<string>) this).getenumerator(); 22 } 23 #endregion 24 } 코드 6 GirlFriendEnumerator 와같은열거자를만드는코드는몽땅빠지고대신에 13 ~ 14 라인만이추가되었습니다. 앞에서했던작업들에비하면허무할만큼코드가간단해졌습니다. 14 번라인에서 return 문앞에 yield 키워드가붙으면현재위치를저장하고현재요소의값을반환합니다. 이후다음반복에서는저장된현재위치를한칸뒤로옮기고나서새로운현재요소의값을반환합니다. 즉 GirlFriendEnumerator 와동일한역할을하는것입니다. C# 2.0 이상을사용할수있는환경이라면아마도반복기를사용하지않고열거가능한클래스를만드는경우는없을것입니다. 그렇다고해서코드 5 와같이반복기를사용하지않는코드가의미가없다는의미는아닙니다. 코드 5 는이른바이터레이터패턴입니다. 이터레이터패턴의구조를숙지하고있다면 yield 키워드에의해컴파일러가생성해내는코드에대해서도쉽게추정이가능할것입니다. 즉닷넷프레임웍의내부동작에대해더많은것을이해하게되고, 그만큼더정확하고효율적으로닷넷프레임웍을사용할수있다는의미가되겠습니다. 2. ICollection<T> 두번째로살펴볼인터페이스는 ICollection<T> 입니다. ICollection<T> 인터페이스는자신을구현하는클래스혹은구조체를컬렉션으로만들어줍니다. 어떤객체를우리가컬렉션이라고부르려면기본적으로몇가지연산을수행할수있어야합니다. 예를들어새로운요소를추가 / 삭제한다든지, 요소의개수를반환한다든지하는연산을들수있습니다. ICollection<T> 15

인터페이스는바로이러한연산을구체적으로정의해놓은것입니다. ICollection<T> 의멤버는 다음과같습니다.(MSDN 라이브러리 ) 속성 int Count { get; } ICollection 에포함된요소수를가져옵니다. bool IsReadOnly { get; } ICollection 가읽기전용인지여부를나타내는 값을가져옵니다. 메서드 void Add (T item) ICollection 에항목을추가합니다. void Clear () ICollection 에서항목을모두제거합니다. bool Contains (T item) void CopyTo (T[] array, int arrayindex) bool Remove (T item) ICollection 에특정값이들어있는지여부를 확인합니다. 특정 Array 인덱스부터시작하여 ICollection 의 요소를 Array 에복사합니다. ICollection 에서맨처음발견되는특정개체를 제거합니다. 표 5 ICollection<T> 의멤버 간략히정리를하자면, ICollection<T> 인터페이스를구현한컬렉션은요소를추가, 삭제할수 있으며, 특정요소를가지고있는지검사하거나, 전체요소를모두지울수도있습니다. 그리고 현재요소의개수를반환할수도있습니다. 또한 ICollection<T> 는 IEnumerable<T> 를상속하고있습니다. 따라서 ICollection<T> 인터페이스를구현하는클래스는 IEnumerable<T> 의멤버도구현하여야하는데, 이는이 클래스가열거가능한클래스가된다는말이기도합니다. 3. IList<T> IList<T> 인터페이스는인덱스를사용하여요소에개별적으로접근할수있는컬렉션을 정의합니다. 멤버는다음과같습니다.(MSDN 라이브러리 ) 속성 T this [int index] { get; set; } 지정한인덱스에있는요소를가져오거나 설정합니다. 16

메서드 int IndexOf (T item) IList 에서특정항목의인덱스를확인합니다. void Insert (int index, T item) 항목을 IList 의지정한인덱스에삽입합니다. void RemoveAt (int index) 지정한인덱스에서 IList 항목을제거합니다. 표 6 IList<T> 멤버 1 개의속성과 3 개의메서드가있는데, 모두인덱스와관련이있습니다. 인덱서를통해요소의값을읽거나쓸수있고, 특정요소의인덱스번호를알수도있습니다. ICollection<T> 으로부터상속받은 Add 와 Remove 를사용하여요소를추가, 삭제할수있지만, 이에더하여특정인덱스에요소를삽입하거나특정인덱스에있는요소를삭제할수있는메서드도가지고있습니다. 4. IDictionary<TKey, TValue> IDictionary<TKey, TValue> 인터페이스는키-값쌍으로데이터를그룹화하는컬렉션을정의합니다. 또한 IDictionary<TKey, TValue> 인터페이스는이때까지등장한인터페이스와는달리제네릭형식매개변수를두개를가지고있습니다. 따라서 IDictionary<TKey, TValue> 는 IList<T> 처럼 ICollection<T> 를상속하지만서로약간다릅니다. IList<T> 의정의가아래와같음에비해, public interface IList<T> : ICollection<T> { } IDictionary<TKey, TValue> 의정의는이렇습니다. public interface IDictionary<TKey,TValue> : ICollection<KeyValuePair<TKey,TValue>> { } 키 - 값쌍을나타내는제네릭구조체인 KeyValuePair 를 ICollection<T> 의형식매개변수로 지정하고있습니다. IDictionary<TKey, TValue> 멤버는다음과같습니다. 속성 TValue this [TKey key] { get; set; } ICollection<TKey> Keys { get; } 지정된키가있는요소를가져오거나설정합니다. IDictionary 의키를포함하는 ICollection 을 17

ICollection<TValue> Values { get; } 메서드 void Add (TKey key, TValue value) bool ContainsKey (TKey key) bool Remove (TKey key) bool TryGetValue (TKey key, out TValue value) 가져옵니다. IDictionary 의값을포함하는 ICollection 을가져옵니다. 제공된키와값이있는요소를 IDictionary 에추가합니다. IDictionary 에지정된키가있는요소가포함되어있는지여부를확인합니다. IDictionary 에서지정한키를가지는요소를제거합니다. 지정된키와연결된값을가져옵니다. IDictionary<TKey, TValue> 는키를통해서특정요소에접근할수있습니다. 문법은 IList<T> 와같이인덱서를사용하지만, IList<T> 는인덱서의매개변수가 int 형인인덱스인반면, IDictionary<TKey, TValue> 는인덱서의매개변수가 TKey 형인키라는것이다릅니다. 또한각각키혹은값만으로된 ICollection<T> 객체를반환할수도있습니다. (Keys 속성과 Values 속성 ) 두개의형식매개변수가있기때문에 Add 와 Remove 메서드역시시그니처가다릅니다. 또한컬렉션이특정키값을가지고있는지를알아볼수도있습니다. III. 컬렉션과메서드, 그리고대리자 C# 1.0 에서가장많이사용된컬렉션은아마도 ArrayList 일것입니다. List<T> 는닷넷프레임웍 2.0 에서추가된 ArrayList 의제네릭버전입니다. ArrayList 는 List<T> 에비하면어떠한장점도가지고있지않습니다. 따라서 C# 2.0 이상에서코딩을하는경우라면, 하위호환성을고려해야하는등의특수한사정이있지않다면, ArrayList 를사용하는경우는없을것입니다. 그래서아마도 C# 2.0 이상에서가장많이사용할컬렉션은 List<T> 가될것입니다. 또한 List<T> 는 ArrayList 에서 object 를 T 로일반화시킨것이상입니다. 새로도입된제네릭의 도움을받아다수의메서드와대리자를제공하기도합니다. 이단원에서는새로도입된헬퍼 메서드와대리자에대해서살펴보도록하겠습니다. 18

( 註 : 이단원에서다루는대부분의메서드와대리자는배열, 즉 Array 객체에서도사용할수 있습니다. 단지 Array 클래스는제네릭이아니므로, List<T> 의메서드와동일한이름의메서드가 정적제네릭메서드로구현되어있습니다.) ( 註의註 : List<T>. Sort 는제네릭클래스의메서드이지제네릭메서드가아닙니다. 반면에 Array.Sort<T>() 는제네릭메서드가맞습니다.) 1. 컬렉션의헬퍼메서드 List<T> 컬렉션의멤버중에는몇가지흥미로운메서드가있습니다. ForEach 나 TrueForAll 과 같은이름을가진이메서드들을살펴보기전에먼저아래예제를보도록합시다. 여자친구의나이와이름을표현하는 GirlFriend 객체를몇개만들어이를 List<T> 컬렉션에 담는코드를생각해보지요. 01 public class GirlFriend 03 private readonly string _name; 04 05 public string Name 06 { 07 get { return _name; } 08 } 09 10 private readonly int _age; 11 12 public int Age 13 { 14 get { return _age; } 15 } 16 17 public GirlFriend(string name, int age) 18 { 19 _name = name; 20 _age = age; 21 } 22 23 public override string ToString() 24 { 25 return string.format(" 이름 : {0}\t 나이 : {1}", _name, _age); 26 } 27 } 코드 7 19

1 private static void Main(string[] args) 2 { 3 List<GirlFriend> mygirls = new List<GirlFriend>(); 4 mygirls.add(new GirlFriend(" 패 ", 19)); 5 mygirls.add(new GirlFriend(" 경 ", 21)); 6 mygirls.add(new GirlFriend(" 옥 ", 20)); 7 } 코드 8 이제 mygirls 컬렉션에포함된세이국소녀가모두 19 세이상인지를확인하는코드를 추가합시다. 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.age < 19) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 9 8 ~ 16 라인의로직은두부분으로나눌수있습니다. 1) mygirls 컬렉션객체의각요소를 열거하고, 2) 각요소가특정조건을만족하는지를검사하는것이그것입니다. 이번에는요구사항을조금수정하여세소녀의이름이모두외자인자를체크하도록코드를 변경해봅시다. 8 ~ 16 라인의로직이두부분으로이루어져있고, 새로운요구사항은이중 2) 부분에만변경이일어나고있습니다. 그렇다면위 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 20

07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.age < 19) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 9 의 11 번라인만아래와같이바꾸면되지않을까요. 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.name.length!= 1) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 10 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.age < 19) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 9 과 01 private static void Main(string[] args) 21

03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.name.length!= 1) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 10 는 11 번라인을제외하면완전히동일합니다. 그렇다면 11 번라인을제외한공통인부분을재사용할수는없을까요? 메서드로묶을수있을겉같은데, 이메서드는 List<T> 의각요소를열거하며어떤일을하고있으니까, 아예 List<T> 의메서드로만드는것이어떨까요? 한번만들어볼까요? 01 public delegate bool Predicate<T>(T item); 02 03 public class List<T> : 04 { 05... 06 07 public bool TrueForAll(Predicate<T> match) 08 { 09 bool result = true; 10 foreach (T item in this) 11 { 12 if (match(item) == false) 13 { 14 result = false; 15 break; 16 } 17 } 18 } 19 } 코드 11 코드 11 은 List<T> 클래스의 TrueForAll 메서드의구현을추정을해서만들어본것입니다. 먼저 1 번라인에서 T 형의형식매개변수를가지는제네릭대리자를선언하였습니다. 이 대리자는 T 형의객체를매개변수로받고 bool 형을반환하고있습니다. 7 번라인에서는 22

TrueForAll 이라는메서드를정의하고있는데, 이름이의미하는바와같이, 컬렉션의모든원소에 대해매개변수로받은대리자를실행시켰을때 (12 번라인 ) 참이라는결과가나오는지를 검사하는일을합니다. 이제 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.age < 19) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 9 과 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.name.length!= 1) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 10 는아래와같이바뀌게됩니다. 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 23

07 08 // 모든소녀가 19 세이상인지를검사 09 bool result1 = mygirls.trueforall(isadult); 10 11 // 모든소녀가외자이름인지를검사 12 bool result2 = mygirls.trueforall(delegate(girlfriend mygirl) 13 { 14 return mygirl.name.length == 1; 15 }); 16 } 17 18 private static bool IsAdult(GirlFriend mygirl) 19 { 20 return mygirl.age >= 19; 21 } 코드 12 9 번과 12 번라인에서각각모든소녀가 19 세이상인지, 혹은외자이름을가지고있는지를 검사하고있습니다. 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.age < 19) 12 { 13 result = false; 14 break; 15 } 16 } 17 } 코드 9 와 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 bool result = true; 09 foreach (GirlFriend mygirl in mygirls) 10 { 11 if (mygirl.name.length!= 1) 12 { 13 result = false; 14 break; 15 } 24

16 } 17 } 코드 10 에비하면재사용을통해코드의양이대폭줄어든것을확인할수있습니다. 또한 9 번라인과 12 번라인은대리자메서드를전달할때이름있는메서드와이름없는 메서드 ( 무명메서드 ) 를각각사용하고있습니다. 메서드가비교적간단하다면무명메서드를 사용하는것이코드의가독성을높이는방법이될수있습니다. 코드 11 에서사용된 Predicate 대리자나 List<T>.TrueForAll 메서드는사실이미베이스클래스라이브러리에존재하는대리자와메서드입니다. 베이스클래스라이브러리의컬렉션클래스는이외에도컬렉션작업에서유용하게사용할수있는대리자와메서드를다수제공하고있습니다. 다음은이런메서드의목록입니다. public List<TOutput> ConvertAll<TOutput>(Converter<T, TOutput> converter) List<TInput> 객체의각원소를 TOutput 형으로변환하여 List<TOutput> 을반환합니다. public bool Exists(Predicate<T> match) 리스트에있는모든원소중 match 조건을만족하는원소가있는지를검사합니다. public T Find(Predicate<T> match) 리스트에있는모든원소중 match 조건을만족하는첫번째원소를반환합니다. public List<T> FindAll(Predicate<T> match) 리스트에있는모든원소중 match 조건을만족하는모든원소를반환합니다. public int FindIndex(Predicate<T> match 25

리스트에있는모든원소중 match 조건을만족하는첫번째원소의인덱스를 반환합니다. public int FindLastIndex(Predicate<T> match) 리스트에있는모든원소중 match 조건을만족하는마지막원소의인덱스를 반환합니다. public void ForEach(Action<T> action) 리스트의모든원소에대해 action 을수행합니다. public bool TrueForAll(Predicate<T> match) 리스트에있는모든원소가 match 조건을만족하는지검사합니다. 2. 컬렉션의헬퍼대리자 앞단원에서거론된모든메서드는자체적으로기능을수행할수없고, 다른메서드의도움을받아야합니다. 예를들어 ForEach 메서드의경우, List<T> 의모든원소를순회하면서어떤작업을하는데, 이 어떤작업 에대한정보를매개변수로받습니다. 작업 이의미하는것처럼이는메서드를나타냅니다. 결국 ForEach 메서드는다른메서드를매개변수로받습니다. 메서드를매개변수로전달하기위해서는대리자를사용합니다. 아래목록은컬렉션의작업을편리하게하는제네릭대리자의목록입니다. public delegate void Action<T>(T obj) T 형의매개변수를하나받고반환값이없는메서드를나타냅니다. public delegate TOutput Converter<TInput, TOutput>(TInput input) 26

TInput 형의매개변수를받고이를 TOutput 형을변환하여반환하는메서드를 나타냅니다. public delegate bool Predicate<T>(T obj) T 형의매개변수를받아그것이특정조건을만족하는지를반환하는메서드를 나타냅니다. public delegate int Comparison<T>(T x, T y) 두객체를비교하는메서드를나타냅니다. x 가 y 보다작으면음수, 같으면 0, 크면 양수를반환합니다. 이러한대리자를사용하는예는코드 12 를참조하시기바랍니다. IV. 순서비교자 1. Sort 메서드 List<T> 에는 Sort 메서드가있어쉽게정렬이가능합니다. 코드를보면서이야기를해봅시다. GirlFriend 라는클래스가있다고하지요. 01 public class GirlFriend 03 private readonly string _name; 04 05 public string Name 06 { 07 get { return _name; } 08 } 09 10 private readonly int _age; 11 12 public int Age 13 { 14 get { return _age; } 15 } 16 17 public GirlFriend(string name, int age) 18 { 19 _name = name; 27

20 _age = age; 21 } 22 23 public override string ToString() 24 { 25 return string.format(" 이름 : {0}\t 나이 : {1}", _name, _age); 26 } 27 } 코드 13 여자친구몇명을 List<T> 에추가하고정렬을해보도록합시다. 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 mygirls.sort(); 09 10 foreach (GirlFriend girl in mygirls) 11 Console.WriteLine(girl); 12 } 코드 14 안타깝게도 InvalidOperationException 예외가발생합니다. 어떻게보면당연한이야기입니다. 아무런기준이나근거도없이여자친구들을정렬한다는게어불성설이지요. 하마못해, 나이혹은 이름순으로정렬하겠다는정보라도있어야정렬을할수있지않겠습니까? 이야기를진행시키기전에 List<T>. Sort 메서드의시그니처를살펴봅시다. public void Sort() public void Sort(Comparison<T> comparison) public void Sort(IComparer<T> comparer) public void Sort(int index, int count, IComparer<T> comparer) 네가지오버로드가있는데요, 세번째와네번째는동일한로직입니다. 그렇다면 List<T> 를 정렬하는방법에는세가지가있다고하겠습니다. 각각에대해서알아봅시다. A. Sort() 의경우 28

첫번째방법은 Sort 메서드에아무런매개변수를요구하지않습니다. 그럼나이혹은이름 순으로정렬하라는것과같은정보를어디서찾는것일까요? 바로 List<T> 의원소가이러한정보를제공하여야합니다. 좀더정확히이야기하면 List<T> 의원소가비교가능한객체이어야한다는말입니다. ( 여러개의객체가있을때먼저이들을서로비교할수있어야정렬이가능한법이니까요.) 우리는 List< GirlFriend> 를사용하니까바로 GirlFriend 객체가비교가능한객체가되어야합니다. 그렇다면 GirlFriend 객체를어떻게비교가능한객체로만들수있을까요? 또 Sort 메서드는어떻게두개의 GirlFriend 객체를서로비교할수있는것일까요? 예컨데 GirlFriend 객체에 CompareWithOther 과같은비교메서드가있다고해도, Sort 가이메서드의존재를어떻게알고호출을할수있을까요? 짐작하셨겠지만, 바로여기서계약으로서의인터페이스가등장을합니다. 즉, 매개변수가없는 Sort 메서드 (void Sort()) 는 List<T> 의원소인객체와계약을맺었는데, 그계약의내용은 List<T> 의원소인객체는 int CompareTo(T other) 와같은메서드를가지고있다는것입니다. 따라서실제 List<T> 의원소의객체가무엇이든지간에 Sort 메서드는그객체의 CompareTo 메서드를호출하면정렬을수행할수있습니다. 계약의다른당사자인 List<T> 의원소인객체는 int CompareTo(T other) 를구현하여야합니다. 이러한계약을문법으로표시한것이바로인터페이스입니다. 위예를들어설명을하자면 Sort 메서드와 GirlFriend(List<T> 의원소 ) 사이에는 GirlFriend 가 int CompareTo(GirlFriend other) 메서드를구현하여야한다는계약이성립되어있는것이고, 이계약을 C# 문법으로표현하자면, int CompareTo(T other) 메서드를멤버로가지는인터페이스를정의하고 GirlFriend 가이인터페이스를구현하면되겠습니다. 29

이인터페이스는이미닷넷프레임웍라이브러리에정의가되어있기때문에새로만들필요가 없습니다. 바로 IComparable( 제네릭버전은 IComparable<T>) 인터페이스인데, 멤버는메서드 하나밖에없습니다. int CompareTo(T other) 이메서드는현재객체가비교할객체 (other) 보다작으면음수, 같으면 0, 크면양수를 반환합니다. 음수, 양수가헷갈리신다면 T 를 int 형이라생각하시고, 현재객체에서매개변수인 객체 (other) 를뺀값이반환값이라고생각을하시면쉽습니다. 이제이 IComparable<T> 인터페이스를구현하여 GirlFriend 객체를비교가능한객체로만들어 보겠습니다. 01 public class GirlFriend : IComparable<GirlFriend> 03 private readonly string _name; 04 05 public string Name 06 { 07 get { return _name; } 08 } 09 10 private readonly int _age; 11 12 public int Age 13 { 14 get { return _age; } 15 } 16 17 public GirlFriend(string name, int age) 18 { 19 _name = name; 20 _age = age; 21 } 22 23 public override string ToString() 24 { 25 return string.format(" 이름 : {0}\t 나이 : {1}", _name, _age); 26 } 27 28 #region IComparable<GirlFriend> Members 29 public int CompareTo(GirlFriend other) 30 { 31 return Name.CompareTo(other.Name); 32 } 33 #endregion 30

34 } 코드 15 1 번라인에서 GirlFriend 클래스가 IComparable<GirlFriend> 를구현한다고하였습니다. 그리고 int CompareTo(GirlFriend other) 메서드를구현하고있습니다. int CompareTo(GirlFriend other) 는 현재객체의 Name 과 other 객체의 Name 을비교한결과를반환합니다. 이제 GirlFriend 객체는비교가능한객체가되어 List<GirlFriend> 는정렬을할수있게 되었습니다. 코드 14 을다시실행시킨결과는아래와같습니다. 이름 : 경나이 : 21 이름 : 옥나이 : 20 이름 : 패나이 : 19 계속하려면아무키나누르십시오... 의도한대로여자친구들이이름순으로정렬이되었습니다. B. Sort(Comparison<T> comparison) 의경우 위코드 15 는제대로동작하지만, 이번에는이름이아니라나이순으로정렬을하는경우를 생각해봅시다. 앞의방법대로라면코드 15 의 int CompareTo(GirlFriend other) 가이름이아니라 나이를기준으로정렬하도록수정하여야합니다. 1 public int CompareTo(GirlFriend other) 2 { 3 return Age.CompareTo(other.Age); 4 } 코드 16 물론제대로동작은하지만 GirlFriend 객체를수정하여야하는한계가있습니다. 만일 List<GirlFriend> 객체를런타임의입력에따라이름순혹은나이순으로정렬을해야한다면이러한방법으로는불가능합니다. ( 리플렉션이나동적코드생성기법을사용하면가능하지만여기서는논외로하겠습니다.) 31

그래서 List<T> 를정렬하는두번째방법이등장하는데요. 이번에는 List<T> 의원소를비교 가능한객체라고전제하는것이아니라, Sort 메서드의매개변수로아예 List<T> 의원소들을 비교하는메서드를전달하는것입니다. 메서드의매개변수로다른메서드를전달한다, 그렇습니다, 바로대리자를사용하는것입니다. 물론닷넷프레임웍라이브러리에는이런대리자도이미정의되어있습니다. public delegate int Comparison<T>(T x, T y) Comparison 대리자의의미는 x 가 y 보다작으면음수, 같으면 0, 크면양수를반환한다는 것입니다. 이방법으로정렬하는예를보도록하겠습니다. GirlFriend 클래스는더이상 IComparable< GirlFriend > 를구현할필요가없습니다. 따라서코드 13 과동일하므로생략합니다. 01 private static int CompareGirlFriendByName(GirlFriend x, GirlFriend y) 03 return x.name.compareto(y.name); 04 } 05 06 private static int CompareGirlFriendByAge(GirlFriend x, GirlFriend y) 07 { 08 return x.age.compareto(y.age); 09 } 10 11 private static void Main(string[] args) 12 { 13 List<GirlFriend> mygirls = new List<GirlFriend>(); 14 mygirls.add(new GirlFriend(" 패 ", 19)); 15 mygirls.add(new GirlFriend(" 경 ", 21)); 16 mygirls.add(new GirlFriend(" 옥 ", 20)); 17 18 mygirls.sort(comparegirlfriendbyname); 19 // mygirls.sort(comparegirlfriendbyage); 20 21 foreach (GirlFriend girl in mygirls) 22 Console.WriteLine(girl); 23 } 코드 17 32

먼저 Comparison 대리자의시그니처와일치하는두개의메서드를정의합니다. 각각이름과나이순으로두개의 GirlFriend 객체간의비교결과를반환합니다. 18 번라인에서 Sort 메서드를호출하는데매개변수로 Comparison 대리자를전달합니다. 아시겠지만 C# 2.0 부터는아래두줄은동일합니다. mygirls.sort(comparegirlfriendbyname); mygirls.sort(new Comparison<GirlFriend>(CompareGirlFriendByName)); 18 번라인을주석으로묶고 19 번라인의주석을풀면이번에는이름이아니라나이순으로 정렬을합니다. 앞의방법에비하면런타임에도정렬방법을바꿀수있는등한결유연해진 모습입니다. C. Sort(IComparer<T> comparer) 의경우 의미상으로대리자와인터페이스는물론분명히서로다른목적을가지고있지만, 일반적으로 인터페이스는대리자대용으로사용할수있습니다. 둘다 위임 구조에기반하고있기때문에 이것이가능합니다. Sort 메서드의세번째오버로드에서이러한예를확인할수있습니다. 이번에는 Sort 가 IComparer<T> 인터페이스를매개변수로받습니다. 이인터페이스도멤버 메서드를하나만가지고있는간단한인터페이스입니다. int Compare(T x, T y) 의미는 Comparison<T> 대리자와동일합니다. 코드 17 을대리자가아니라인터페이스를사용하는형태로고치면다음과같습니다. 01 public class GirlFriendNameComparaer : IComparer<GirlFriend> 03 public int Compare(GirlFriend x, GirlFriend y) 04 { 05 return x.name.compareto(y.name); 06 } 07 } 08 09 public class GirlFriendAgeComparaer : IComparer<GirlFriend> 10 { 11 public int Compare(GirlFriend x, GirlFriend y) 33

12 { 13 return x.age.compareto(y.age); 14 } 15 } 16 17 internal class Program 18 { 19 private static void Main(string[] args) 20 { 21 List<GirlFriend> mygirls = new List<GirlFriend>(); 22 mygirls.add(new GirlFriend(" 패 ", 19)); 23 mygirls.add(new GirlFriend(" 경 ", 21)); 24 mygirls.add(new GirlFriend(" 옥 ", 20)); 25 26 mygirls.sort(new GirlFriendNameComparaer()); 27 // mygirls.sort(new GirlFriendAgeComparaer()); 28 29 foreach (GirlFriend girl in mygirls) 30 Console.WriteLine(girl); 31 } 32} 코드 18 대리자대신 IComparer<T> 인터페이스를구현하는두개의비교자클래스를만든것말고는 코드 17 과매우유사합니다. 2. BinarySearch 메서드 자료구조나알고리즘을공부하신분들은익숙하시겠지만, 이진검색이라는것이있습니다. O(logN) 의성능을보장하는, 현재까지알려진검색알고리즘중에서는가장빠른검색방법중 하나입니다. O(logN) 에대해첨언을하자면, 정확하게는 O(log 2 N) ( 2N 이아니라 2 를밑수로가지는 logn 입니다.) 이라고해야할것인데, 자료구조의성능을표현하는이른바 big O 표현식에서는밑수가큰의미가없기때문에관례적으로 O(logN) 으로표기를합니다. 어쨌거나이게어떤의미인가하면, 예컨데 100 개의원소가있는컬렉션이있고, 이중한원소를검색하는상황을생각해봅시다. 순차검색의경우라면첫원소부터차례대로검색을해나가는수밖에없습니다. 따라서최악의경우에는 100 번의비교를하여야합니다. 반면에이컬렉션에대해 34

이진검색을한다면, log 2 100 = 7.XXX 이므로 8 번만비교를하면됩니다. 원소의개수가만일 1,000,000 개라면요? 1,000,000 vs. 20 이라는경이적인성능차이가발생하게됩니다. List<T> 에는이런이진검색을하는 BinarySearch 메서드가있습니다. 따라서컬렉션에서특정 원소를찾는검색작업이매우효율적으로이루어질수있습니다. BinarySearch 에대해본격적인이야기를시작하기전에, 몇가지흥미로운사항부터 짚어보도록하겠습니다. 먼저 List<T> 에서 BinarySearch 를수행하기위해서는반드시 List<T> 객체가정렬된상태로있어야합니다. 이는 List<T>. BinarySearch 뿐만아니라모든이진검색의전제조건이기도합니다. 정렬되지않은컬렉션에대해이진검색을수행한결과는엉터리입니다. 그렇다면, 이진검색을수행하고자하는컬렉션에원소가삽입된다면이컬렉션을다시정렬된상태로만들기위한조작이필요하다는말이됩니다. 그렇습니다. 그리고이문제는자료구조과목에서다루는큰이슈이기도합니다. 그래서일반적으로자료의삽입이빈번하게일어나고동시에이진검색을지원해야하는경우에는 List<T> 컬렉션은좋은선택이아닙니다. 이러한경우에는 Red-Black 트리나 SkipList 같은컬렉션이좋은대안이될것입니다. 또한가지흥미로운점은 BinarySearch 의반환값입니다. BinarySearch 메소드의반환값이찾는원소의인덱스라는건직관적으로알수있겠는데, 흥미로운것은컬렉션내에찾는원소가없는경우입니다. 순차검색을하는 List<T>.IndexOf() 메서드의경우에는찾는원소가컬렉션에없으면항상 -1 을반환합니다. 여기서 -1 은 0 보다작은값이라는의미밖에없습니다. 왜냐하면반환값이 0 보다크거나같다면찾는원소가컬렉션에있다는의미가되니까, 0 보다작은값이라면어떤것이라도상관이없고, List<T>.IndexOf() 을설계한개발자는 -1 을선택한것입니다. 반면에 BinarySearch 메서드의경우에는이반환값에부가적인정보가들어있습니다. 코드를 보면서이야기해볼까요? 01 private static void Main(string[] args) 35

03 List<string> list = new List<string>(); 04 list.add("a"); 05 list.add("c"); 06 list.add("e"); 07 08 int returnvalue = list.binarysearch("b"); 09 10 Console.WriteLine(returnValue); 11 Console.WriteLine(~ returnvalue); 12 } 코드 19 결과는다음과같습니다. - 2 1 계속하려면아무키나누르십시오... 8 번라인에서 list 에없는 b 를이진검색한반환값을 returnvalue 에저장합니다. 10 번과 11 번라인에서는 returnvalue 와 returnvalue 의비트보수연산 (~ 연산자 ) 한결과를각각 출력합니다. 비트보수연산에대해서는부록 : 비트보수연산과정수의표현을참고하시기 바랍니다. 그렇다면위결과로나온 -2 와 1 의의미는무엇일까요? 일단 -2 의의미는 0 보다작기때문에 list 컬렉션에 b 객체가포함되어있지않다는 의미입니다. 그럼 1 의의미는무엇일까요? 1 은만일검색한원소인 b 를 list 에삽입하고자할 때 list 의정렬상태를깨뜨리지않고 b 를삽입할수있는위치 ( 인덱스 ) 를나타냅니다. 그림 2 원소 b 를삽입하기전의 list ( 정렬되어있음 ) 그림 3 원소 b 를삽입한후의 list ( 정렬상태가유지됨 ) 36

그림 2 와그림 3 은각각원소 b 를삽입하기전과후의 list 입니다. 원소 b 를삽입할때 list 의정렬상태를깨뜨리지않고삽입할수있는가장낮은인덱스는 1 이며, 이것은바로 BinarySearch 의반환값을비트보수연산한값입니다. BinarySearch 의반환값을이용하여 List<T> 에원소를삽입하는예제는다음과같습니다. 01 private static void Main(string[] args) 03 List<string> list = new List<string>(); 04 list.add("a"); 05 list.add("c"); 06 list.add("e"); 07 08 string newitem = "b"; 09 10 int returnvalue = list.binarysearch(newitem); 11 12 list.insert(~ returnvalue, newitem); 13 14 foreach (string item in list) 15 Console.WriteLine(item); 16 } 코드 20 실행결과는다음과같습니다. a b c e 계속하려면아무키나누르십시오... 결과를보면원소 b 가삽입되고나서도 list 는여전히정렬상태가유지되고있음을알수 있습니다. 결과적으로 BinarySearch 의반환값은두가지의정보를동시에표현하고있습니다. 통상적인메서드설계지침에의하면이는권장되지않는사항입니다. 하나의반환값은하나의정보만을표현하는것이정석입니다. 반환해야할값의정보가두가지이상이라면, 1) 반환값대신두개이상의 out 매개변수를사용하거나, 2) 반환할정보를필드로가지고있는클래스혹은구조체를반환하는것이일반적인패턴입니다. 37

BinarySearch 의반환값의경우에도 1) 검색하는원소의인덱스와 2) 새원소를삽입할위치라는두가지의정보는, 비록서로논리적으로배타적인관계를형성하고는있지만, 분명히서로다른별개의정보로간주되어야하고, out 매개변수를받거나클래스혹은구조체를반환하는형태로디자인하는것이더원칙에충실한디자인이되지않았을까하는생각을해봅니다. 제가보기에 BinarySearch 메서드는원칙보다는실용성을선택한것같습니다. BinarySearch 에대한서론이길었습니다. BinarySearch 는세개의오버로드를가지고있습니다. public int BinarySearch(T item) public int BinarySearch(T item, IComparer<T> comparer) public int BinarySearch(int index, int count, T item, IComparer<T> comparer) 세번째오버로드는두번째오버로드와동일한것으로간주해도무방하므로처음과두번째 오버로드에대해서만살펴보도록하겠습니다. 메서드의시그니처를보고이미짐작하셨겠지만, BinarySearch 에도 Sort 와동일한이슈가있습니다. 즉, BinarySearch 메서드를사용하기위해서는 1) List<T> 객체의원소가비교가능한객체이거나, 2) List<T> 의원소를비교하는일을하는비교자객체를 BinarySearch 의매개변수로전달하여야합니다. Sort 와다른점은매개변수로대리자를전달하는오버로드가없다는점말고는사용법이거의동일합니다. 3. Comparer<T> 이번단원의제목은순서비교자입니다. 그런데왜쌩뚱맞게 Sort 와 BinarySearch 메서드에대해, 그것도각각의오버로드까지거론하면서길게이야기를하였을까요? 또순서비교자와 Sort, BinarySearch 메서드가어떤관계가있는것일까요? 답은두메서드의오버로드들중첫번째형태, 즉매개변수없는형태의 Sort 와 BinarySearch 가순서비교자를사용한다는것입니다. ( 순서비교자와관련해서는 Sort 와 BinarySearch 를구별할필요가없으므로여기서는 Sort 에 대해서만이야기를할것인데, 특별한언급이없으면 BinarySearch 도동일합니다.) 38

Comparer<T> 는이름이의미하는것처럼비교자입니다. 정확하게이야기하면순서비교자이므로그이름도, 다음단원에서이야기할같음비교자의이름이 EqualityComparer 인것과대구를맞춰, OrderComparer 정도가되면더명확했을것같습니다. 대체로완벽에가까운닷넷프레임웍의작명에있어서옥의티라고나할까요. Comparer 가설계될당시에는 EqualityComparer(2.0 에서추가 ) 가존재하지않았기때문에빚어진사소한아쉬움이라고하겠습니다. Comparer 의상속관계를먼저보겠습니다. 그림 4Comparer 의상속관계 Comparer 는 Comparer<T> 를구현합니다. 따라서 int Compare(T x, T y) 메서드를구현하여야 합니다. 매개변수가없는형태의 Sort() 는 List<T> 의각원소를정렬할때바로이 Compare 메서드를호출하여두원소사이의순서를결정합니다. 그런데 Comparer 는추상클래스입니다. 인스턴스를만들수없다는말이지요. 대신 Comparer 를상속받은세개의클래스중하나의인스턴스를만듭니다. 그런데이세개의자식클래스들의접근지정자가모두 internal 이어서어셈블리외부에서는접근할수가없습니다. 바꿔말하면이자식클래스들은모두닷넷프레임웍내부에서만사용되는클래스라는이야기입니다. 그래서문서화도되어있지않아 MSDN 에도나오지않습니다. 오직닷넷프레임웍의소스를 39

통해서만그존재를확인할수있습니다. 위다이어그램도닷넷프레임웍의소스를복사하여 생성한것입니다. 다시정리를하자면 Sort 메서드는 Comparer 의 Compare 메서드를호출하여야합니다. 그런데 Compare 메서드는 Comparer 에서는추상메서드로선언되어있으며, 실제구현은 Comparer 의자식클래스들에들어있습니다. 그런데이자식클래스들은 internal 이라서 Sort 메서드가접근하지못합니다. 대신 Comparer 메서드는 Default 라는속성을제공하고있습니다. public static Comparer<T> Default { get; } Default 속성은 T 의형식에따라 GenericComparer, NullableComparer, ObjectComparer 중한하나의인스턴스를생성하여 Comparer 형으로반환합니다. 이를좀더자세히설명하면, T 가 IComparable<T> 를구현하면 GenericComparer 를, 그렇지않고 T 가 IComparable<U> 를구현한 Nullable<U> 형이라면 NullableComparer 를, 그외에는 ObjectComparer 의인스턴스를반환합니다. IComparer<T> 의 Compare 메서드는이자식클래스들에서각각오버라이드되어있는데, GenericComparer 의 Compare 는다음과같이재정의되어있습니다. 01 internal class GenericComparer<T> : Comparer<T> where T : IComparable<T> 03 public override int Compare(T x, T y) 04 { 05 if (x!= null) 06 { 07 if (y!= null) return x.compareto(y); 08 return 1; 09 } 10 if (y!= null) return -1; 11 return 0; 12 } 13... 14 } 40

1 번라인에서 T 가 Comparable<T> 를구현한다고제약조건이지정되어있기때문에 7 번 라인에서보는바와같이 Comparable<T> 의 CompareTo 메서드의결과를반환하고있습니다. 반면에 ObjectComparer 의 Compare 는다음과같이정의되어있습니다. 1 internal class ObjectComparer<T> : Comparer<T> 2 { 3 public override int Compare(T x, T y) 4 { 5 return Comparer.Default.Compare(x, y); 6 } 7... 8 } 5 번라인에서제네릭이아닌 Comparer 클래스가나오는데요, 이의 Default 속성은언제나제네릭이아닌 Comparer 입니다. 그래서결국 5 번라인은제네릭이아닌 Comparer 클래스의 Compare 메서드를호출하는것인데요. 이는닷넷프레임웍에다음과같이정의되어있습니다. ( 사실은 Shared Source Common Language Infrastructure 의소스인데아마도실제닷넷프레임웍의소스와다르지는않을것입니다.) 01 public int Compare(Object a, Object b) 03 if (a == b) return 0; 04 if (a == null) return -1; 05 if (b == null) return 1; 06 if (m_compareinfo!= null) 07 { 08 String sa = a as String; 09 String sb = b as String; 10 if (sa!= null && sb!= null) 11 return m_compareinfo.compare(sa, sb); 12 } 13 14 IComparable ia = a as IComparable; 15 if (ia!= null) 16 return ia.compareto(b); 17 18 throw new ArgumentException(Environment.GetResourceString("Argument_ImplementIComparable")); 19 } 중간에 m_compareinfo 와같은필드가등장하긴하는데무시하시고, 14 번부터 16 번라인 까지만보시면되겠습니다. 이부분을문장으로표현하자면, 만일매개변수로받은 a 가 IComparable 인터페이스를구현하고있다면 IComparable 의 CompareTo 메서드를실행하고 41

그렇지않다면예외를발생시킨다는것입니다. Sort 메서드를시작할때든예제코드 ( 코드 14) 에서예외가발생한것을기억하시나요? 그것이바로여기서발생한예외였던것입니다. 위단락의마지막한문장을설명하기위해서참으로많은이야기를했습니다. 하지만아직한 가지가더남았습니다. 순서비교자를했으니그와쌍을이루어다니는같음비교자에대한 이야기까지해야제대로마무리가될것같습니다. 지겹더라도조금만더가봅시다. V. 같음비교자 1. Contains 메서드 이번에는 List<T> 의 Contains 메서드에대해서이야기를해보겠습니다. Contains 는매개변수도 하나없이워낙에간단하기때문에별로주목을받지못하는경우가많은데, 사실은여기에 상당한함정이도사리고있습니다. 아래는 Contains 메서드의시그니처입니다. public bool Contains (T item) 누구나알다시피, Contains 는 List<T> 의각원소를돌면서 item 과같은원소가발견되면 true, 그렇지않으면 false 를반환합니다. 여기서문제가되는부분은바로이 같다 는말이됩니다. 예를들어봅시다. 아래와같이여자친구의인스턴스를두개만듭니다. GirlFriend girl1 = new GirlFriend(" 패 ", 19); GirlFriend girl2 = new GirlFriend(" 패 ", 19); girl1 과 girl2 는이름과나이, 즉모든필드가동일합니다. 그렇다면 girl1 과 girl2 는서로같다고할수있을까요? 물론두인스턴스는같지않습니다. 왜냐하면 GirlFriend 는클래스이며또클래스는참조형인데, 닷넷프레임웍에서참조형의인스턴스는그필드의값이아니라참조변수의값 ( 객체가생성된메모리의주소 ) 이같을경우에같은객체로정의를하기때문입니다. ( 참고로값형의경우에는필드의값이모두같으면같은객체로간주합니다.) 42

하지만경우에따라서는참조형의경우에도참조변수의값이아니라다른기준에의해서두 객체가같다고정의해야하는경우가있습니다. 코드를보면서이야기해보도록하겠습니다. 다음 코드는 Contains 메서드를사용하는예입니다. 01 private static void Main(string[] args) 03 List<GirlFriend> mygirls = new List<GirlFriend>(); 04 mygirls.add(new GirlFriend(" 패 ", 19)); 05 mygirls.add(new GirlFriend(" 경 ", 21)); 06 mygirls.add(new GirlFriend(" 옥 ", 20)); 07 08 GirlFriend targetgirl = new GirlFriend(" 패 ", 19); 09 10 bool found = mygirls.contains(targetgirl); 11 12 Console.WriteLine(found); 13 } 코드 21 mygirls 컬렉션에서이름이패이고나이가 19 살인소녀를찾고있습니다. 이런코드라면아마도 true 가출력되는것이자연스러울것같습니다. 하지만결과는그렇지않습니다. 두소녀의같음을이름과나이로판단하는것이아니라, GirlFriend 객체가생성된메모리주소로판단하기때문에그렇습니다. 이코드를수정하여결과가 true 가출력되게, 즉소녀의이름과나이가같으면같은소녀로 간주하도록해봅시다. 이는 Sort 메서드의경우와유사할것같습니다. 즉정렬을하기위해서는세가지옵션이있었는데, 1) List<T> 의원소를비교가능한객체로만들거나, 2) 두원소를비교하여순서를결정하는대리자를 Sort 의매개변수로전달하거나, 3) 두원소를비교하여순서를결정하는비교자의인스턴스를전달하는방법이그것이었습니다. 하지만 Contains 의경우는다른오버로드가없기때문에 2) 와 3) 의방법을사용할수없고 1) 번방법밖에사용할수가없습니다. 즉바꿔말하면 GirlFriend 를같음비교가능한객체로만드는것입니다. 43

같은비교가능한객체로만들기위해서는 IEquatable<T> 인터페이스를구현하면됩니다. IEquatable<T> 인터페이스역시단한개의메서드를가지고있습니다. bool Equals (T other) Equals 는현재객체와 other 을비교하여같으면 true, 다르면 false 를반환합니다. 이제 GirlFriend 클래스가 IEquatable<T> 인터페이스를구현하도록수정을하여야합니다. 01 public class GirlFriend : IEquatable<GirlFriend> 03 private readonly string _name; 04 05 public string Name 06 { 07 get { return _name; } 08 } 09 10 private readonly int _age; 11 12 public int Age 13 { 14 get { return _age; } 15 } 16 17 public GirlFriend(string name, int age) 18 { 19 _name = name; 20 _age = age; 21 } 22 23 public override string ToString() 24 { 25 return string.format(" 이름 : {0}\t 나이 : {1}", _name, _age); 26 } 27 28 public bool Equals(GirlFriend other) 29 { 30 return Name.Equals(other.Name) && Age.Equals(other.Age); 31 } 32 } 30 번라인에서현재 GirlFriend 객체와 other 객체를비교하는데, Name 과 Age 가모두같으면 같은객체로정의하고있습니다. 이제코드 21 을실행시키면 true 가반환됩니다. 2. EqualityComparer<T> 44

EqualityComparer 는앞단원에서이야기한 Comparer 를이해하였다면쉽게이해할수있습니다. 먼저 EqualityComparer 의상속관계를보도록합시다. 그림 5EqualityComparer 의상속관계 Comparer 와동일한구조임을알수있습니다. 다만 ByteEqualityComparer 라는것이추가되었는데, 이는닷넷프레임웍에달려있는주석에의하면 byte 형간의비교를할때성능을위하여특별히고안된같음비교자라고합니다. 소스를보면 C 의런타임함수인 memchr 를호출하는 unsafe 코드로되어있습니다. 그외는 Comparer 와거의동일합니다. Contains 메서드가매개변수로받은객체와 List<T> 의각원소가같은지를검사할때 IEqualityComparer 의 bool Equals (T x, T y) 메서드를호출합니다. 물론이 Equals 메서드는 IEqualityComparer 인터페이스로부터구현상속받은것이며, EqualityComparer 는추상클래스이기때문에 Equals 메서드를추상메서드로선언만하고실제구현은그자식메서드들에게맡기고있습니다. 45

이제 GenericEqualityComparer 의경우에는굳이소스를확인하지않아도, T 가 IEquatable<T> 를구현한형이기때문에 IEquatable<T> 의 Equals 메서드를호출할것이라는 사실은짐작이가능합니다. 하지만 ObjectEqualityComparer 의 Equals 메서드재정의는 Comparer 와약간다릅니다. 01 internal class ObjectEqualityComparer<T> : EqualityComparer<T> 03 public override bool Equals(T x, T y) 04 { 05 if (x!= null) 06 { 07 if (y!= null) return x.equals(y); 08 return false; 09 } 10 if (y!= null) return false; 11 return true; 12 } 13... 14 } 제네릭이아닌 EqualityComparer 와같은클래스는없기때문에바로 object 클래스에서정의된, 혹은하위클래스중어딘가에서재정의된 Equals 메서드를호출합니다. 따라서기본순서비교자인 Comparer<T>.Default 는 T 가 IComparable 인터페이스를구현하지않으면 ( 구체적으로 IComparable 의멤버인 CompareTo 메서드를구현하지않으면 ) 예외를발생시키는것에반해, 기본같음비교자인 EqualityComparer<T>.Default 는 T 가 Equals 메서드만구현하면괜찮습니다. Equals 는최상위클래스인 object 에서이미가상이나마구현이되어있긴때문에 EqualityComparer<T>.Default 는특수한상황이아니라면예외를발생시키지않습니다. 바로위문장에서말하는특수한상황에는어떠한것이있을까요? 한가지예를들자면 T 가 object 의 Equals 메서드를재정의하는데, 예외를발생시키도록재정의된상황을들수있겠습니다. ( 물론재정신을가진프로그래머라면이런짓을할리가없겠지요?) 그외에도많은예를들수있을것입니다. 다른예들을생각해낼수있다면, 제생각에는아마도이글의내용을충분히이해한것이라고생각하셔도좋을것같습니다. 46

지금까지많은이야기를하였습니다. 한가지주제만놓고직선을달려온것이아니라, 중간중간주제와직접적인연관이없는샛길로도왕왕빠지다보니더에둘러온것같은느낌도듭니다. 하지만이시리즈의큰제목은 C# 코딩연습 이며, 동시에그것이제가이글을쓰는최종목표이기도합니다. 즐거운연습이되셨기를바랍니다. 47

VI. 부록 : 비트보수연산과정수의표현 비트보수연산자체는피연산자의각비트를반전시키는간단한연산이지만, 이는컴퓨터에서 정수를표현하는방법과밀접한연관이있습니다. 컴퓨터에서정수를표현하는방법을설명하기위해 4 비트짜리부호있는정수형을가정합시다. 첫번째비트는부호를나타내고 (0 은양수, 1 은음수 ) 나머지 3 비트는숫자를표현합니다. 따라서표현가능한숫자의개수는, 3 개의비트를사용할수있고각비트는 0 또는 1 의두가지값을가질수있으므로 2 의 3 승 = 8 개가됩니다. 8 개의숫자를표현할수있으므로양수의경우 1 이아니라 0 부터시작해서 0 ~ 7 까지의값을가질수있습니다. 또한부호비트가 1 이되어음수가되는경우에도마찬가지로 -7 ~ 0 까지의값을표시할수있습니다. 이때 0 은양수와음수양쪽에모두포함되어있으므로, 음수에서는표현할수있는 8 개숫자의범위를 1 씩내립니다. 즉 -8 ~ -1 이됩니다. 결과적으로부호있는 4 비트정수형의표현가능한범위는 -8 ~ 7 이됩니다. () 양수 7 의경우는비트로표현하면부호비트는 0, 나머지비트가모두켜져있는상태이므로 0111 이될것입니다. 그럼음수 7 은어떻게표현할까요? 단순히부호비트만 1 로바꾼 1111 이될까요? 그렇다면음수 8 을표현할수없게됩니다. 또한 0000 과 1000 이각각 +0 과 -0 을의미하는것이라면이는중복이됩니다. 따라서양수와음수의표현에는다른방법을사용합니다. 바로비트보수연산을한후 1 을더하는것입니다. 예를들어양수 1 이 0001 이면음수 1 은비트보수연산을한 1110 에다 1 을더한 ( 비트에대한연산이므로정확하게는 비트연산 ) 1111 이된다는것입니다. 양수 1 과음수 1 을더하면 0 이되어야하는데, 이를비트로표현해도 0001 1111 이니까 0000 즉 0 이맞습니다. 아래는부호있는 4 비트정수의가능한모든값입니다. 48

표 7 부호있는 4 비트정수의비트값 이표에는일련의규칙이있습니다. 0 과 -8 을제외한다른수들은, 자신의비트를반전한후 1 을더하면부호가변경됩니다. 예컨데 3 의경우 0011 을반전하면 1100, 여기에서 1 을더하면 1101, 즉 -3 이됩니다. 거꾸로 -3 의경우를볼까요? 1101 을반전하면 0010, 1 을더하면 0011 다시 3 이됩니다. 3 이 0011 이면 -3 은그냥 1011 로표현할일이지왜이렇게복잡하게만들어놓았을까요? 단순히중복되는 0 의표현을제거하여숫자하나 (4 비트에서는 -8) 를더표현하기위해서요? 물론이것도맞는말이지만진짜이유는비트연산을통하여덧셈과뺄셈연산을처리하기위해서입니다. 잠시 0 과 1, 즉비트만아는기계의입장에서생각해봅시다. 1 + 2 이것을기계는어떻게이해할까요? 이식을우리의부호있는 4 비트정수의비트표현으로 고치면이렇습니다. 0001 + 0010 결과는 0011 즉 3 이나옵니다. 이번에는뺄셈을볼까요? 1 2 물론비트표현으로다시쓰면, 49

0001 0010 이되고그결과는 1111 이니까 -1 이맞습니다. 하지만위식을 1 + (-2) 로보고뺄셈이아닌 덧셈을해도결과는동일합니다. 0001 + 1110 이제결론을이야기할때가됐습니다. 위표 7 과같은식으로양수와음수의비트가설정되어있다면, 덧셈연산과뺄셈연산을동일하게덧셈연산으로만처리할수있습니다. 또한덧셈연산은기계의연산중에서비트연산다음으로빠른연산입니다. 결국, 정수를기계에서표현하는방법은우리의직관과는달리성능과효율성을위해고도로정교하게고안되어있는것니다. 50

인용자료 MSDN 라이브러리. http://msdn2.microsoft.com/ko-kr/library/default.aspx. 51