[C#] ICollection Interface

LeeTaeHwa·2023년 3월 18일
0

C#

목록 보기
2/4

Introduction


프로그래밍을 하다보면 컨테이너 객체를 필수적으로 사용하게 된다. C++의 std::vector, Java의 ArrayList 와 같은 단순한 리스트 컨테이너가 있으며, C#에서의 Dictionary<V, K> 와 같은 비선형적 컨테이너도 종종 사용한다. 컨테이너라는 이름에서 유추 할 수 있듯이, 어떠한 요소(element)를 저장하는 객체다. 때문에 이와 관련하여 공통 된 행위들이 존재하는데, 이에 대해서 다루는 인터페이스가 ICollection 이다

Collection Interface


먼저, 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 인터페이스를 상속받는다는 점이다.

본 인터페이스를 상속받는 이유는, 첫번째로는 컨테이너 객체가 열거가능함을 보장하는 점이다. 그리고 두번째로는 GenericNon-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 열거자를 반환하도록 강제하는 것이다. 이것은 공식 홈페이지에서 공식적으로 언급하는 내용이다.

Example Code


그렇다면 예제 코드를 보면서 더 자세히 살펴보도록 하자.

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;
}

본 클래스의 구현에 대한 설명은 이전에 다루었으므로 생략하도록 하겠다.

Conclusion

ICollectionIEnumerable을 상속받은 인터페이스며, 기능적인 면에서 상위호환 격인 인터페이스다. 그래서 열거기능을 지원함과 동시에 컨테이너와 관련한 기능들을 제공한다. 단순히 foreach 의 열거 기능뿐만 아니라 컨테이너에 대한 일반화 된 처리도 가능하다.

그렇지만 무조건 ICollection을 상속받아서 구현 할 필요는 없다. 동적인 컨테이너가 아니라, 정적인 컨테이너일 경우엔 열거기능만 제공해도 충분 할 수도 있으며, ICollection의 인터페이스와 프로그래머가 필요한 사양이 다를 수도 있다. 결론은 필요에 맞게 잘 사용하는 것이다.

profile
하늘을 향해 걸어가고 있습니다.

0개의 댓글