TheArtandScienceofSmalltalk:Chapter 07
- 제 7 장 컬렉션 클래스
컬렉션 클래스
스몰토크 클래스 라이브러리에 대한 일반 지침을 소개했으니 몇 가지 클래스를 자세히 살펴볼 수 있겠다. 첫 번째로 고려할 클래스 집합은 컬렉션 클래스다. 다양한 컬렉션 유형은 시스템에서 가장 많이 재사용되고, 재사용 가능한 클래스에 속한다. 사실 모든 스몰토크 프로그래머는 거의 모든 프로그램에서 컬렉션 클래스를 사용한다. 클래스 자체는 (꼭 가장 효율적인 구현부일 필요는 없지만) 상당히 개발되었으며 스몰토크 프로그래밍 스타일의 훌륭한 예를 제공한다.
컬렉션 클래스는 우리가 계속해서 언급하게 될 어떤 것의 예가 되기도 한다. 단, 본 저서의 분량상 모든 객체 타입마다 모든 기능과 특징을 완전히 설명할 수는 없다. 스몰토크 매뉴얼조차 그 방대한 내용을 싣기에는 역부족이며, 설사 제시한다 하더라도 최신 정보로 유지할 수가 없을 것이다. 특정 기능이 존재하는지, 어떻게 작동하는지, 또는 어떤 것이 구현되는지를 확실히 알고 싶다면 시스템 자체를 살펴보는 수밖에 없다. 현재로선 컬렉션 클래스의 기본 개념을 이해하는 데 집중해야 한다. 추후에 클래스 계층구조 브라우저를 (또는 다른 브라우저를) 이용해 여러 컬렉션 클래스를 살펴보고, 대안책을 고려하며, 자신의 문제에 대해 알아야 하는 것이 무엇인지 발견할 수 있을 것이다.
컬렉션이란 무엇인가?
컬렉션은 다른 객체 그룹을 포함하는 객체를 의미한다. 포함된 객체들은 컬렉션의 요소(elements)로 알려진다. 다음 페이지에 실린 다이어그램이 이를 잘 나타낸다. 이전에 어떤 프로그래밍 언어를 사용했든 최소한 한 가지 타입의 컬렉션엔 익숙할 것인데, 배열(array)을 예로 들 수 있겠다. 스몰토크에서 배열을 이용 가능하지만 클래스 라이브러리는 다양한 컬렉션 클래스도 제공한다. 각 컬렉션은 다양한 방식으로 전문화되지만 특정 기능은 모두 공통으로 가진다. 예를 들어, 모든 컬렉션은 컬렉션에 객체를 추가하고 그로부터 객체를 제거할 수 있도록 해준다. 또한 컬렉션과 그 내용을 다양한 방식으로 시험할 수 있도록 해준다. 가장 중요한 것은 컬렉션을 열거하도록 해준다-각각의 요소마다 동일한 연산을 수행한다.
스몰토크의 거의 대부분 컬렉션은 이종(heterogeneous) 컬렉션이다. 즉, 어떤 유형의 객체 혼합도 포함할 수 있다는 의미다. 스몰토크는 타입이 없다는 사실을 기억하라. 가령 스몰토크에선 다른 언어와는 달리 시스템에게 '정수 타입의 배열'을 요청할 필요가 없음을 의미한다. 그냥 배열을 요청하기만 하면 된다. 중요한 것은, 컬렉션이 다른 컬렉션을 포함하는 것도 가능하며, 종종 그렇게 한다.
이러한 이질성의 규칙에 예외가 있는데, 문자만 포함할 수 있는 String이나 Symbol(그렇다, 이들은 컬렉션으로서 구현된다)와 같은 클래스와, 이름을 통해 알 수 있듯이 바이트만 포함할 수 있는 ByteArray가 있다.
다양한 컬렉션 클래스들이 루트 클래스 Collection 하에 꽤 복잡한 상속 계층구조를 형성한다. 각 컬렉션의 모든 기본 기능은 Collection 클래스에서 정의되는데, 모든 컬렉션 타입이 할 수 있는 일에 익숙해지려면 시스템의 코드 브라우저 중 하나를 이용해 살펴보는 편이 가장 좋다.
Collection 아래에는 계층구조에 수많은 추상적 슈퍼클래스가 있다. Collection 자체와 같이 이러한 클래스엔 어떤 인스턴스도 존재하지 않는다. 그들은 기능을 그룹화하는 역할만 한다. 이러한 클래스들의 인스턴스를 생성하려 하지 말라-올바로 작동하지 않을 것이다. 브라우저를 이용해 클래스 주석을 살펴보면 클래스가 추상적인지 구체적인지 알 수 있다.
때때로 특정 유형의 컬렉션에서 특정 기능을 검색할 때 특정 컬렉션 서브클래스나 Collection에서 찾을 수 없는 경우도 있음을 기억하라. 이러한 추상적 슈퍼클래스들 중 하나에 숨어있을 것이다.
계층구조의 하층 쪽으로 구체적 클래스가 위치한다. 당신이 사용 중인 스몰토크 버전에 따라 8개의 시스템 카테고리에 50개가 넘는 Collection의 구체적 서브클래스가 위치할 수 있다. 개중에 다수는 시스템에서 매우 특수화된 용도를 갖는다. 하지만 당신이 자신의 프로그램에서 컬렉션을 필요로 하는 경우 보통 다음 구체적 클래스 중 하나를 선택할 수 있다: Array; Bag; Dictionary; OrderedCollection; Set; SortedCollection; Interval. 본장의 나머지 부분에서는 이러한 클래스들을 살펴볼 것이다.
컬렉션 인스턴스 생성하기
새 컬렉션 인스턴스를 만드는 방법에는 네 가지가 있다. 워크스페이스와 inspect를 이용해 각기 시험해볼 수 있겠다:
HyColl := OrderedCollection new.
MyColl := Array new: 27.
HyColl := set with: #red with: #green with: #blue.
MyColl := #('Goodbye' 4 #now).
컬렉션을 만드는 첫 번째 방법은 가장 기본적인 방법으로, 단순히 비어 있는 정렬된 컬렉션을 만든다. 모든 컬렉션 클래스들은 다른 모든 클래스와 마찬가지로 이를 실행하는 방법을 알고 있다. 두 번째 방법은 27개의 요소로 된 새 배열을 만드는 것이다. 세 번째는 세 개의 기호, #red, #green, #blue 를 포함하는 집합을 만들 것이다. with:with:with: 는 매개변수가 세 개인 단일 메서드임을 주목하라. 또한 with:와 with:with: 와 with:with:with:with: 메서드가 제공된다. 컬렉션을 만들면서 예상되는 네 개의 요소 다음에 add: 를 이용해 하나씩 요소들을 추가한다. 마지막으로 네 번째 방법은 세 개의 요소로 된 배열을 만드는 방법이다 ('Goodbye', 4, 그리고 #now). 이는 요소들이 리터럴일 때만 작용한다. 다시 말해, 스몰토크 코드의 일부를 괄호 안에 넣을 수 없다는 의미다. 이는 흔히 발생하는 실수로, 이상한 결과를 야기할 수 있다. 그 이유는 표현식이 컴파일러를 통해 처리되는데, 컴파일 시 배열을 생성하기 때문이다. 사실 런타임 시 평가되는 (실행되는) 것이 아니다.
사용할 컬렉션 선택하기
스몰토크 프로그램에서 컬렉션을 사용하길 원하는 경우 아래와 같은 질문을 해보는 것이 도움이 된다:
- 컬렉션의 요소들을 정렬할 필요가 있는가?
- 그렇다면 정렬을 어떻게 결정할 것인가?
- 요소들을 키를 통해 평가할 것인가?
- 컬렉션에 중복 요소들을 허용할 것인가?
위의 질문에 대한 답은 필요한 컬렉션 타입을 결정하는 데 도움이 될 것이다. 그렇지만 염두에 두어야 할 중요한 사항이 몇 가지 있다. 첫째, 프로그래밍에서, 특히 스몰토크에서는 올바른 답이 하나에 그치지 않는 경우가 종종 있다. 특정 컬렉션 클래스가 효과가 있는 것처럼 보인다면 적어도 사용해선 안 되는 이유가 나타날 때까지는 그냥 사용하라. 컬렉션 클래스의 선택은 필요성 못지않게 스타일에 따라서도 좌우된다. 둘째, Collection의 새 서브클래스를 생성해야 하는 경우는 극히 드물다. 클래스 계층구조에서 제공되는 다양한 서브클래스는 당신이 필요로 하는 서브클래스를 거의 항상 포함할 것이다. 컬렉션에 기능을 추가하고자 한다면 자신만의 클래스 중 하나의 인스턴스에 컬렉션을 캡슐화하여 그 곳에 기능을 추가해야 한다. 이렇게 상속에 대한 대안으로 사용되는 캡슐화는 중요한 내용이므로 제 2부에서 다시 다루도록 하겠다.
여러 종류의 컬렉션
컬렉션 클래스의 기본 특성을 논했으니 사용을 고려할만한 특정 클래스를 조금 더 자세히 살펴보자.
배열
이는 다른 언어들에서 제공되는 배열과 상당히 비슷하다. 요소들은 정수 색인을 (또는 위에 사용된 용어를 이용하자면 정수 키를) 이용해 삽입되고 검색된다. 요소를 검색하려면 at: 메서드를 사용하라. 그리고 요소를 추가하려면 at:put: 메서드를 사용하라. 예를 들어, 아래와 같은 코드는:
MyArray := Array new: 20.
MyArray at: 12 put: #yellow.
MyArray at: 17.
비어 있는 12개의 요소 배열을 생성하여, 그것을 MyArray 변수로 할당한다. 이후에 MyArray의 12 위치에 #yellow 기호를 놓고, 마지막으로 MyArray의 17번째 요소를 리턴한다 (이번 경우 nil이 된다). 스몰토크는 다른 언어들과 달리 배열이 0이 아니라 1부터 시작함을 주목한다!
Bag
bag은 간단히 정렬되지 않고, 키가 없는(unkeyed) 객체 컬렉션이다. add: 를 이용해 객체를 넣고 remove:를 이용해 제거한다. 예를 들어:
MyBag add: #MyNewColour.
MyBag remove: #MyOldColour.
다른 유형의 컬렉션들과 마찬가지로 bag 또한 addAll: 과 removeAll: 을 이해하는데, 이는 컬렉션을 인수로서 사용 시 해당 컬렉션 내 모든 요소를 백으로 추가하고 그로부터 제거할 것이다.
당신에게 객체가 없다면 객체를 검색조차 할 수 없는 컬렉션 유형을 갖는 것이 이상하게 보일지도 모르겠다. 하지만 bag은 컬렉션이 객체를 포함하는지, 아니면 객체들의 컬렉션을 반복(iterate over)하는지 여부를 시험하고자 할 때 유용하다. 이 두 가지 컬렉션 기능을 짧게 살펴보겠다.
동일한 객체가 하나의 bag에 한 번 이상 존재할 수 있음을 주목한다. 이번 경우 Bag는 객체가 그 안에 몇 번 있는지 기록한다. Bag는 =를 이용해 그 총계를 증가시키기 전에 객체가 존재하는지 시험한다. 즉, Bag에 이미 존재하는 객체와 동등하지만 (equal) 등가적이지는 (equivalent) 않은 (동일하지 않은) 객체를 추가하려는 경우 추가되지 않을 것을 의미한다-그리고 동등한 객체에 대한 총계가 대신 증가할 것이다. 다시 말하지만, 혹시 잘못된다 하더라도 걱정마라. Bag를 사용 시 예상치 못한 행위를 얻기 시작한다면 이 점을 기억하길 바란다!
Dictionary
Dictionary는 가장 유용한 컬렉션 클래스 중 하나이다-단 사용 방법을 알고 있을 때 한해서. Dictionary는 배열과 같다고 생각하면 되는데, 단 컬렉션의 키 또는 색인에 대해 정수 대신 어떤 객체든 사용할 수 있다는 점에서만 차이가 있다. 예를 들어 아래는:
MyDictionary at: #name put: 'Simon'.
MyDictionary at: #tage.
'Simon' 문자열을 MyDictionary에 넣고 #name 키를 부여한 후, #age 키를 이용해 dictionary로 이전에 넣은 객체를 검색한다. 'Simon' 문자열, 그리고 #age 키를 가졌던 여느 객체든 값(values)이라 부른다. 이런 방식으로 dictionary는 하나의 객체 집합과 (키) 또 다른 객체 집합 (값) 사이에 매핑(mapping)을 유지한다. dictionary에선 기호를 키로 사용하는 경우가 꽤 흔하다. 하지만 dictionary 내부의 키와 값은 실제로 어떤 유형의 객체든 가능하다. 이는 강력한 기능으로, 사용할수록 점점 더 유용하다고 생각하게 될 것이다.
위의 다이어그램은 국가명을 화폐명으로 (모두 문자열로 보관) 매핑하는 간단한 dictionary의 예제를 보여준다. 아래 코드는 다이어그램과 같은 dictionary를 생성할 것이다:
MyDictionary := Dictionary new
at: 'Britain' put: 'Pounds';
at: 'France' put: 'Francs';
at: 'USA' put: 'Dollars'.
OrderedCollection
OrderedCollection은 객체들의 컬렉션을 특정 순서대로 유지하고 접근할 때 Array 대신 자주 사용된다. 하지만 배열과 달리 그 순서가 정수 색인이 아니라 각 요소들의 상대적 위치에 의해 결정된다.
OrderedCollection의 인스턴스들은 요소가 추가되고 제거되면서 증가했다가 줄어든다. add:, addFirst:, addLast:, add:before:, add:after 등과 같은 메서드는 컬렉션에 새 요소를 삽입하는 데 많은 유연성을 제공한다. first와 last를 이용해 요소를 검색하고 removeFirst, removeLast:, remove: 메서드를 이용해 요소를 제거 가능하다. 이러한 메서드들은 OrderedCollection의 인스턴스가 큐(queue) 또는 스택(stack)인 것처럼 사용하도록 해준다. 요소가 사실상 존재하지 않는 컬렉션에서 remove:를 사용해 해당 요소를 제거할 경우 오류가 발생할 것을 주목한다. remove:ifAbsent: 메서드는 요소가 컬렉션에 없을 경우 어떤 일이 발생할 것인지 명시하도록 해준다:
MyCollection remove: MyName ifAbsent: [].
이는 MyName 이 MyCollection에 없을 경우 아무 일도 하지 않는 유용한 구성이다. [ ] 는 빈 블록으로, mn[1]일 경우 단순히 리턴한다.
다른 언어에서 '배열이 필요하다'고 생각하는 상황이 스몰토크에선 '배열보다OrderedCollection이 더 나을까?'라고 생각할 것이다. 컬렉션의 크기가 고정되어 있는데 그 크기를 분명히 알 수 없거나 배열이 제공하는 효율성을 (거의 OrderedCollection보다 두 배 정도 빠름) 필요로 하는 경우 OrderedCollection을 선택해야 한다. 조금 더 효율적이거나 익숙하다고 해서 Array를 선택하는 덫에 빠지지 않도록 한다. OrderedCollection을 사용하면 훨씬 더 우아하고 유연한 코드를 제공하여 애플리케이션에서 배열만큼 빠른 속도를 보이는 경우가 다반사다. 한 번 시도해보라.
at:이나 at:put:-정수 색인을 이용해-을 이용해 OrderedCollection의 요소로 접근할 수는 있겠지만, 색인을 분명히 알고 있고 addFirst: 메시지 또는 자신의 컬렉션과 비슷한 무언가를 전송하면 색인이 모두 변경될 것을 기억한다는 확신이 없다면 그러한 접근을 피하도록 해야 한다.
Set
이 클래스는 객체의 집합에 대한 수학적 개념을 표현한다. 세트는 중복(duplicate)이 없는 컬렉션이다. Bag 또는 OrderedCollection과 달리 (add:를 이용해) 세트에 요소를 몇 번 추가하는지는 중요하지 않다-set가 요소를 한 번만 추가할 것이다. 중복을 자동으로 버리는 이 기능은 때때로 유용하다.
Set는 자신에게 새 요소가 이미 존재하여 추가할 수 없는지 결정하기 위해 =를 사용한다. 즉, 두 개의 다른 객체가 서로 동일할 경우 동일한 Set에 위치할 수 없음을 의미한다. 기존 요소들과 단순히 동등(=)할 때가 아니라 등가적(==)일 때에만 새 멤버를 거부하는 set를 원한다면 identitySet를 사용하라. ==가 =보다 빠르기 때문에 (인스턴스 변수의 임시 테스트량을 수반할 수 있는) identitySet가 Set보다 빠른 것이 보통이라는 부가적 효과가 있다.
세트 내의 요소들은 특정 순서로 보관되지 않는다. 세트를 인쇄하거나 그 요소들을 반복할 때 (Below 참조) 요소를 접근하는 순서를 통제할 수 없으며, 매번 같은 순서일 것이라고 신뢰할 수 없다.
인스펙터를 사용해 세트를 살펴볼 때는, self를 선택 시 세트 스스로 올바르게 표시할 수는 있으나 그 인스턴스 변수 내 추가로 nil 요소들을 포함하는 것처럼 보일 수도 있음을 인지하라. 이는 Set가 작동하는 방식에 내부적인 것으로, 무시해도 안전하다. Set 내의 반복(iteration) 메서드(do:, collect: 등)는 그러한 요소들을 무시하는 방법을 알고 있다.
SortedCollection
이 클래스의 인스턴스는 요소의 어떠한 특징으로 결정된 순서에 따라 요소들을 모을 수 있는 방법을 제공한다. 예를 들어, 문자열은 알파벳순으로, 혹은 숫자는 절대치로(numerically) 정렬할 수 있다. 스스로를 정렬하도록 SortedCollection에게 알려줄 필요도 없다. (add:를 이용해) 요소들을 추가하면 자동으로 올바른 위치에 삽입된다. 어떤 객체든 필요한 후크(hooks)를 제공한다면 어떤 특징을 기반으로 하여 순서대로 정렬할 수 있다. 이러한 후크를 부여하려면 아직 익숙하지 않을 법한 스몰토크의 기능을 이해해야 한다. 다음 설명을 이해하지 못하더라도 염려하지 말길 바란다. SortedCollections는 그리 흔히 사용되진 않는다. 스몰토크 코드의 블록에 익숙해지면 다시 찾아보면 될 일이다.
SortedCollection은 블록에 보관된 교체 가능한 스몰토크 코드 조각을 이용해 어떤 객체가 컬렉션 내 다른 객체보다 앞서는지 뒤따르는지를 결정하는 데 사용된 비교 연산자(comparison)를 포착한다. 여기서는 알고리즘의 정렬-이것은 고정되어 있고 SortedCollection에 포함되어 있다-을 이야기하는 것이 아니다. 우리는 어떤 객체가 다른 객체에 비해 '더 적은지' 혹은 '알파벳이 앞서는지', '큰지', 혹은 '더 푸른색(green)인지'를 결정하는 검사를 이야기하는 것이다. 코드 조각은 두 개의 객체를 비교한 후 객체가 표현된 순서로 있어야 한다면 true로, 순서를 바꿔야 한다면 false로 답한다.
이러한 비교 코드의 조각 중 하나를 sortBlock: 메시지와 함께 SortedCollection의 인스턴스로 전송하라. 다행히 SortedCollection은 기본 정렬 블록을 제공하므로, 대부분의 경우는 걱정하지 않아도 된다. 기본 정렬 블록은 각 요소가 다른 요소들에 비해 '더 적은지' 검사한다. 즉 <= 메시지를 이해하는 객체 유형이라면 기본 SortedCollection에 따라 정렬할 수 있음을 의미한다. 객체가 <= 메시지를 이해하지 못한다면 이 메서드를 자신의 클래스에서 작성하거나, 다른 메서드를 이용하는 정렬 블록을 제공하는 선택권이 있다. 예를 들어, 아래의 블록은 정렬된 컬렉션에게 그 요소들을 적절히 비교하여 true 또는 false로 답함으로써 요소들을 이름 길이에 따라 정렬하라고 말한다:
[:x :y ㅣ x name size < y name size].
Interval
해당 클래스는 임시 객체의 컬렉션을 보유할 수 없다는 점에서 우리가 봐온 Collection의 다른 서브클래스와는 약간 다르다. 대신 '등차 산술 수열(finite arithmetic progression)'이란 개념을 표현한다. 다시 말해, 숫자로 된 컬렉션인 것처럼 행동한다는 말이다. Interval 클래스의 인스턴스 또한 다른 컬렉션과 다른 방식으로 생성된다. 예를 들어 아래의 두 표현식은:
MyInterval := Interval from: 50 to: 100 by: 2.
MyInterval := 50 to: 100 by:2.
Interval의 인스턴스를 생성할 것인데, 이는 50, 52, 54, ..., 100 숫자를 포함하는 것처럼 행동할 것이다. 두 번째 사례에서 50 객체가 어떻게 Interval의 인스턴스 생성 방법을 아는 '편리한' 메서드를 (to:by:) 제공하는지를 주목하라. 이 메서드를 비롯해 다른 메서드들은 Number 클래스를 살펴봄으로써 찾을 수 있다.
Interval의 인스턴스로는 새 요소를 추가할 수 없다. 사실 add;, at:put:, 기타 메서드의 정의부를 살펴보면 Collection으로부터 분명히 '상속취소됨(un-inherited)'을 볼 수 있다. 이는 스몰토크에서 중요한 상속의 기능인데 후에 다시 살펴보도록 하겠다. 이 클래스의 중점은 모든 컬렉션과 마찬가지로 그 내용을 반복하는 방식을 제공한다는 데 있다 (그 방법을 짧게 살펴보겠다). 다시 말해, do-loop와 유사한 제어 구조를 제공한다. 하지만 앞서 관찰하였듯 훌륭한 스몰토크 코드에서는 이와 같은 루프를 구성할 필요가 거의 없다.
컬렉션 시험하기
컬렉션에서 요소를 추가, 검색, 제거할 수 있을 뿐 아니라 여러 특성들을 시험하는 방법도 여러 가지를 관찰할 수 있다. 각 컬렉션 서브클래스는 특정 시험을 갖고 있지만 아래와 같은 메시지는 모두 공통으로 이해한다:
- size – 컬렉션이 얼마나 많은 요소를 포함하는지.
- isEmpty – 컬렉션이 비어 있다면 true.
- includes:anObject – 컬렉션이 anObject를 포함한다면 true.
컬렉션으로 메시지 크기를 전송할 때 그것은 첫 번째 컬렉션에 포함된 다른 컬렉션들에 얼마나 많이 포함되어 있는지가 아니라 (다시 말해, 재귀적이 아니라) 컬렉션에 의해 직접적으로 포함된 요소가 얼마인지를 알려준다. 이는 당신이 스스로 해결해야 한다.
컬렉션 변환하기
한 타입으로 된 컬렉션에 asArray, asBag, asOrderedCollection, asset, asSortedCollection 메시지 중 하나를 전송함으로써 다른 컬렉션 타입으로 변환할 수 있다. 예를 들어, MyCollection이 Array인 경우 아래 코드 단편은 Set로 변환할 것이다:
MySet := MyCollection asset.
컬렉션의 변환에 대해서 이야기하지만 원본 컬렉션 객체는 사실 변경되거나 파괴되지 않는다. 대신 컬렉션에 변환 메시지 중 하나를 전송하면 새로운 타입으로 된 새 컬렉션이 생성되고, 기존 컬렉션의 요소들이 새 컬렉션으로 들어가게 된다. 요소들 자체는 복사되지 않는다. 각 객체는 기존 컬렉션과 새 컬렉션 모두에 포함된다. 요소를 변경하는 경우 이를 주의하라-흔히 버그를 발생시킨다. 예를 들어, 아래 다이어그램에서 aSet 내의 A 객체를 수정하면 anArray 내의 A 객체로 수정될 것인데, 이는 두 개가 동일한 객체이기 때문이다.
컬렉션을 변환하는 것과 관련해 주의해야 할 사항이 몇 가지 더 있다. 첫째, 정렬되지 않은 컬렉션을 정렬된 컬렉션으로 변환 시 그 순서를 예측할 수 없고 반복되지 않을 수도 있다. 둘째, dictionaries는 키와 값을 포함하기 때문에 상황이 더 복잡해진다. 기본 값으로 새 컬렉션은 값만 포함할 것이다. 키만 있는 새 컬렉션을 얻기 위해서는 dictionary로 keys 메시지를 먼저 전송한다. 예를 들어:
MyValues := MyDictionary asset.
Mykeys := MyDictionary keys asSortedCollection.
컬렉션 열거하기
Collection의 모든 서브클래스는 열거자(enumerator)라 불리는 메시지 집합을 이해한다. 이들은 컬렉션의 요소마다 동일한 연산을 실행하는 것과 관련된다. 하지만 열거자마다 다른 구체적 효과를 가진다.
열거자는 컬렉션의 가장 강력하고 편리한 기능 중 하나에 속한다. 열거자의 사용 방법을 알면 당신이 상상했던 것보다 일부 코드 조각의 작성이 더 간단해질 것이다. 사실 이러한 메서드들 때문에 'do-loops'와 같은 구성이 스몰토크에서 극히 드문 것이다.
우선 여섯 가지 열거자 타입을 간략하게 소개하고, 세부적인 내용은 아래에서 설명하도록 하겠다:
- do: - 컬렉션의 요소마다 동일한 연산을 실행한다.
- collect: - do:와 같지만 결과를 컬렉션으로 리턴한다.
- select: - 모든 요소를 시험하여 통과한 요소를 리턴한다.
- reject: - 모든 요소를 시험하여 실패한 요소를 리턴한다.
- detect: - 시험을 통과하는 첫 번째 요소를 리턴한다.
- inject:into: - 한 연산의 결과를 다음 연산에 반영한다.
do:
모든 열거자들 중 가장 기본은 do:이다. 이는 단순히 컬렉션 내 모든 요소마다 동일한 연산을 반복한다. 예를 들어, 아래 표현식은:
MyCollection do: [:piece ㅣ piece reset].
MyCollection 내 모든 요소로 reset 메시지를 전송한다. 이 예제의 목적을 위해 우리는 reset이 무슨 일을 하는지 알지 못할 뿐더러 신경 쓰지도 않는다고 가정한다. 중괄호 안의 코드는 MyCollection 내 요소마다 한 번씩만 실행시키는 스몰토크의 블록이다. 수직 막대 ㅣ 앞의 :piece 단어는 단순히 ㅣ 이후 코드에서 각 요소를 참조하기 위해 piece라는 이름을 사용할 것을 말할 뿐이다. 블록이 실행될 때마다 컬렉션에서 다음 요소는 piece reset 표현식의 piece로 대체된다.
do: 메시지는 MyCollection의 크기가 어떤지 알 필요 없이, 우리만의 루프를 준비할 필요도 없이, 다른 언어에서와 같이 카운터를 증가시키고 시험할 필요도 없이 단순히 검색(iterate through)할 수 있도록 해주는 모습을 관찰할 수 있다. 하지만 다른 언어에서 do-loop와 유사한 제어 구조를 준비하고 싶다면, Interval 클래스의 인스턴스로 동일한 do: 메시지를 전송할 수 있다. 다음을 시도해보라:
어떤 컬렉션 유형에도 do:를 이용할 수 있지만 순서가 정의되지 않은 컬렉션의 경우 (Bag, Set, Dictionary) 요소들이 예측하지 못한 순서대로 처리될 수 있음을 유념에 둔다. 보통은 상관없지만 특정 순서로 요소를 처리하거나 반복 가능한 순서로 처리할 것이라 신뢰하는 경우는 피하도록 주의한다. 컬렉션을 반복하는 동안 열거 중인 컬렉션을 수정하지 않는 것이 매우 중요하다. 즉, 컬렉션에 요소를 추가하거나 그로부터 제거하면 안 됨을 의미한다 (요소 자체를 수정하는 것은 괜찮다). 이러한 코드는 쉽게 작성할 수 있다:
Fruits do: [:fruit ㅣ fruit isOrange ifTrue:
[Fruits remove: fruit3].
위의 코드는 Fruits로부터 orange에 해당하는 모든 fruit를 꽤 합리적으로 제거하고자 시도한다. 이는 Fruits 컬렉션의 크기를 변경하기 때문에 예상대로 작동하지 않고, 예기지 못한 결과를 제공할 것이다. 이러한 유형의 연산을 실행하고자 한다면 orange가 아닌 fruit를 모두 포함하는 새 컬렉션을 만들어 Fruits에서 반복한 후 기존 컬렉션을 새 컬렉션으로 대체해야 한다.
collect:
또 다른 열거자로 collect:를 들 수 있는데, do:와 비슷하지만 컬렉션 내 요소마다 동일한 연산을 실행한 결과를 모두 포함하는 새 컬렉션을 만든다는 점이 다르다. 예를 들어, 아래와 같은 표현식은:
Names := People collect: [:person ㅣ person name].
People 컬렉션의 모든 요소에 name 메시지를 전송한 결과를 모두 포함하는 새 컬렉션을 (Names) 생성할 것이다. 생성된 새 컬렉션은 열거된 컬렉션과 동일한 클래스를 가질 것이다. 따라서 People가 OrderedCollection이었다면 Names 또한 OrderedCollection이 될 것이다.
select:
다음으로 select:가 있는데, 이는 블록을 true로 만든 열거 컬렉션의 요소들만으로 구성된 새 컬렉션을 생성한다. 사실 위에서 우리가 원했던 작업이다. 아래 표현식은:
Oranges := Fruits select: [:fruit ㅣ fruit isOrange].
isOrange 메시지에 true로 답한 모든 fruit를 Oranges로 수집할 것이다. select:의 상반되는 열거자로, false를 막는 모든 요소를 수집하는 reject:가 있다.
detect:
다른 반복자(iterator)와 쉽게 혼동되는 detect:는 블록이 true를 리턴하도록 만드는 첫 번째 요소가 발견되는 즉시 열거를 중단시키고 그 요소를 리턴한다. detect:가 그러한 요소를 발견하지 못하면 오류가 발생한다. 이를 피하려면 detect:ifNone:을 사용하라. 가령 아래의 코드는:
Winner := Employees
detect: [:worker ㅣ worker age>60]
ifNone: [Employees last].
연령이 50세가 넘는 첫 번째 고용인을 고를 것이다. 50세를 넘는 이가 없을 경우 컬렉션의 마지막 고용인이 리턴될 것이다.
inject:into:
마지막 열거자 inject:into: 는 다른 열거자들에 비해 좀 더 복잡하다. 이전 요소를 이용해 블록을 실행한 결과를 다음 요소에 대한 블록으로 '추가하도록' 해준다. 이의 전형적인 예는 숫자로 된 컬렉션에서 모든 값을 더한다:
Total := Number inject: 0 into:
[:subtotal :numberㅣ subtotal + number].
이것이 어떻게 작용하는지 이해하는 것은 사소한 일은 아니지만, 사실상 inject:into는 다른 열거자들만큼 자주 사용되지 않으니 이해하지 못하더라도 크게 염려할 필요 없다. 원하는 열거자의 이름을 기억하지 못한다거나 처음에 잘못된 열거자를 고른다 하더라도 걱정마라. 숙련된 스몰토크 프로그래머들조차 자신이 필요로 하는 것을 기억해내기 위해 Collection 클래스를(프로토콜을 열거하여) 살펴봐야 하기 때문이다.
컬렉션 클래스는 Collection 클래스 하에 거대하고 복잡한 계층구조를 형성한다. 하지만 시스템 내 모든 컬렉션 클래스 중에서 정기적으로 사용하는 클래스는 대여섯 개에 그칠 것이다. 컬렉션을 이용해 다양한 객체 집합을 그룹화하고 몇 가지 기본 제어 구조를 구현할 수 있겠다 (열거와 루핑).
우리가 논한 내용 중 일부는 이미 익숙해졌을 것이고 나머지는 세부 내용을 잊어버릴 수도 있을 것이지만, 존재하는 기능의 타입을 기억하는 한 다양한 코드 브라우저의 기능을 이용해 필요한 세부내용을 찾아낼 수 있을 것이다.
컬렉션 클래스는 시스템에서 가장 유용한 주제기 때문에 세부 내용은 추후 논하기 위해 남겨두었다. 하지만 요소의 삽입, 검색, 제거, 시험, 열거에 대한 기본 기능들은 모든 컬렉션에 공통적이며, 탐구하고 기억할만한 가치가 있다.
마지막으로, 전체적인 컬렉션 계층구조는 행동을 슈퍼클래스로 추상화하는 방법과 상속을 이용해 클래스들 간 코드를 공유하는 방법을 보여주는 훌륭한 예가 된다. 상속을 어떻게 사용하는지 제 2부에서 논한 다음 컬렉션 클래스와 그것의 구현 방법을 다시 살펴보는 것이 좋겠다. 그 동안 의존성 메커니즘을 살펴봄으로써 클래스 라이브러리를 좀 더 상세히 알아보고자 한다.
Notes
- ↑ 이 speelling 이 맞는지 검증이 필요합니다.