프로그래밍을 하다보면 컨테이너 객체를 필수적으로 사용하게 된다. C++의 std::vector
, Java의 ArrayList
와 같은 단순한 리스트 컨테이너가 있으며, C#에서의 Dictionary<V, K>
와 같은 비선형적 컨테이너도 종종 사용한다. 컨테이너라는 이름에서 유추 할 수 있듯이, 어떠한 요소(element)를 저장하는 객체다. 때문에 이와 관련하여 공통 된 행위들이 존재하는데, 이에 대해서 다루는 인터페이스가 ICollection
이다
먼저, ICollection
인터페이스의 코드부터 살펴보도록 하자.
public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
int Count { get; }
bool IsReadOnly { get; }
void Add(T item);
void Clear();
bool Contains(T item);
void CopyTo(T[] array, int arrayIndex);
bool Remove(T item);
}
앞서 언급했듯이, 컨테이너라면 가질 법한 기능들을 제공하고 있다. 컨테이너가 가지고 있는 요소들의 갯수라던가, 추가하거나, 제거하는 등의 메소드가 존재한다. 그리고 눈에 띄는 점이 2개의 IEnumerable
인터페이스를 상속받는다는 점이다.
본 인터페이스를 상속받는 이유는, 첫번째로는 컨테이너 객체가 열거가능함을 보장하는 점이다. 그리고 두번째로는 Generic 와 Non-Generic 2 경우에 대한 열거자 객체를 반환한다는 것이다. 이에 대해서는 크게 2가지 설명이 있는데, 하나는 레거시와의 호환성이다.
C#이 처음 나왔을 때에는 제네릭이라는 개념이 없었다. 2005년 2.0이 발표되면서 추가되었는데, 당연히 이전 컨터이네는 제네릭 구현체를 가지고 있지 않았다. 때문에 구버전 코드와의 호환성을 유지하기 위함이라는 것이다. 본 내용은 Stack overflow에서 언급 된 내용으로 공식적인 답변은 아니다.
refer link : Why does ICollection implement both IEnumerable and IEnumerable
두번째는 Generic 열거자를 반환하는 GetEnumerator
메소드가 Non-Generic 열거자를 반환 하는 데에도 사용 될 수가 있다는 것이다. 이 때문에 Non-Generic 열거자를 반환하도록 강제하는 것이다. 이것은 공식 홈페이지에서 공식적으로 언급하는 내용이다.
그렇다면 예제 코드를 보면서 더 자세히 살펴보도록 하자.
public class IntegerCollection : ICollection<int>
{
//해당 열거자의 구현체는 후술
public IEnumerator<int> GetEnumerator() => new IntegerEnumerator(this);
IEnumerator IEnumerable.GetEnumerator() => _array.GetEnumerator();
//기본 배열 크기 및 확장 계수
private const int DEFAULT_LENGTH = 4;
private const int EXTEND_FACTOR = 2;
//내부적인 배열 및 크기와 원소 갯수
private int[] _array;
private int _size;
private int _count;
//크기를 지정해주거나 그렇지 않으면 기본 크기만큼 생성
public IntegerCollection() : this(DEFAULT_LENGTH) { }
public IntegerCollection(int capacity)
{
_array = new int[capacity];
_size = capacity;
_count = 0;
}
//인덱싱 오버로딩
public int this[int index] => _array[index];
//안전한 참조 연산
public int At(int index)
{
if(index < 0 || index >= _array.Length) throw new ArgumentOutOfRangeException("index");
return _array[index];
}
//ICollection의 구현체, 포함 여부 체크
public bool Contains(int target)
{
foreach(var e in _array) if (target == e) return true;
return false;
}
//ICollection의 구현체, 원소 추가
public void Add(int item)
{
if (_count >= _size) Reassign(item);
_array[_count] = item;
_count++;
}
//할당 된 크기를 넘을 경우엔 확장 후 재할당함
private void Reassign(int value)
{
_size *= EXTEND_FACTOR;
var tmp = new int[_size];
//_array.CopyTo(tmp, 0);
foreach (var idx in Enumerable.Range(0, _array.Length))
{
tmp[idx] = _array[idx];
}
_array = tmp;
}
//ICollection의 구현체, 컨테이너를 초기화함
public void Clear()
{
_array = new int[DEFAULT_LENGTH];
_size = DEFAULT_LENGTH;
_count = 0;
}
//ICollection의 구현체, 컨테이너가 포함한 요소를 대상 어레이에 복사함
public void CopyTo(int[] array, int arrayIndex)
{
if (array == null) throw new ArgumentNullException("array");
if (arrayIndex < 0) throw new ArgumentOutOfRangeException("index");
if (_count > array.Length - arrayIndex) throw new ArgumentException();
for(var i = 0; i < _size; i++) array[i+ arrayIndex] = _array[i];
}
//ICollection의 구현체, 요소 갯수를 반환
public int Count => _count;
public bool IsReadOnly => false;
//ICollection의 구현체, 일치하는 항목이 있으면 제거
public bool Remove(int item)
{
for(int i = 0; i < _count; i++)
{
if(item == _array[i])
{
PullArray(i + 1);
return true;
}
}
return false;
}
//제거 후에 요소들을 이동해 재배치함
private void PullArray(int idx)
{
for(var i = idx; i < _count; i++)
{
_array[i] = _array[i + 1];
}
_count--;
}
}
예시를 보여주기 위해서 대략적으로만 구현하였다. 그래서 테스트를 돌려보면 미쳐 발견하지 못한 에러가 있을 수도 있다. 하지만 어지간하게는 동작 할 것이다.
기본적으로 구현 내용은 List<T>
와 유사하다. 아마 다른 선형 컨테이너들도 유사한 구현 내용을 가지고 있을 것이며, Dictionary<K,V>
나 Set<T>
같은 비선형적인 컨테이너의 경우엔 구현체가 많이 다를 것이다. 하지만 중요한 부분은 ICollection
의 구현을 통해 사용자에게 동일하거나 비슷한 사용 인터페이스를 제공하는 것이다.
그리고 본 컨테이너에 대한 열거자 클래스는 다음과 같다.
public class IntegerEnumerator : IEnumerator<int>
{
private IntegerCollection _collection;
private int _index;
private int _value;
public IntegerEnumerator(IntegerCollection collection)
{
_collection = collection;
_index = -1;
_value = default(int);
}
public bool MoveNext()
{
if(++_index >= _collection.Count)
{
return false;
}
else
{
_value = _collection[_index];
}
return true;
}
public void Reset() => _index = -1;
public void Dispose() { }
public int Current => _value;
object IEnumerator.Current => Current;
}
본 클래스의 구현에 대한 설명은 이전에 다루었으므로 생략하도록 하겠다.
ICollection
은 IEnumerable
을 상속받은 인터페이스며, 기능적인 면에서 상위호환 격인 인터페이스다. 그래서 열거기능을 지원함과 동시에 컨테이너와 관련한 기능들을 제공한다. 단순히 foreach
의 열거 기능뿐만 아니라 컨테이너에 대한 일반화 된 처리도 가능하다.
그렇지만 무조건 ICollection
을 상속받아서 구현 할 필요는 없다. 동적인 컨테이너가 아니라, 정적인 컨테이너일 경우엔 열거기능만 제공해도 충분 할 수도 있으며, ICollection
의 인터페이스와 프로그래머가 필요한 사양이 다를 수도 있다. 결론은 필요에 맞게 잘 사용하는 것이다.