공부/C#

Enumeration, IEnumerator, 이터레이터 사용, ICollection, IList, 읽기 전용 인터페이스, 배열 클래스

월러비 2025. 8. 26. 17:31

Enumeration

열거의 개념과 필요성

  • 컴퓨팅에서는 다양한 종류의 컬렉션이 존재함
    • 컬렉션 : 벡터, 리스트 등의 컨테이너다.
    • 단순한 데이터 구조인 배열이나 연결 리스트
    • 이러한 다양한 컬렉션들의 가장 기본적인 공통 요구사항은 내용을 순회하는 것임
    • 복잡한 구조인 레드블랙 트리나 해시테이블
  • 각 인터페이스는 계층화 되어있다.
  • 확인 해야할 것들 : 인터페이스를 이용한 상속받은 클래스는 각 기능들을 사용자도 사용할 수 있게 된다.
  • IEnumerable<T>는 ‘Enumeration only’ 즉, 순회하는것만 가능하다.
    • 순회 기능을 제공하는 것이다.

IEnumerable과 IEnumerator

IEnumerator 인터페이스

  • 컬렉션의 요소를 순방향으로 열거하는 기본적인 저수준 프로토콜을 정의함
public interface IEnumerator
{
    bool MoveNext();       // 다음 요소로 이동, 더 이상 요소가 없으면 false 반환
    object Current { get; } // 현재 위치의 요소 반환
    void Reset();          // 처음 위치로 되돌림
}

  • MoveNext()
    • 첫 요소를 얻기 전에 반드시 호출해야 함
    • 빈 컬렉션의 경우를 처리하기 위한 설계임
    • 다음 순회 요소가 첫 요소가 되고 true를 반환한다.
      • 불가능하면 false를 반환한다.
  • Reset()
    • 주로 COM 상호운용성을 위해 존재함
      • COM : 상호 작용할 수 있는 이진 소프트웨어 구성 요소를 만들기 위한 플랫폼 독립적이고 분산된 개체 지향 시스템이다.
        • 런타임에 상호 작용하는 재사용 가능한 소프트웨어 라이브러리를 만들기 위한 이진 상호 운용성 표준을 정의한다.
      • 상호 운용성 : 하나의 시스템이 동일 또는 다른 시스템과 아무런 제약 없이 서로 호환되어 사용할 수 있는 성질이다.
    • 보편적으로 지원되지 않아 직접 호출은 피해야 함
    • 대신 새로운 열거자를 인스턴스화 하는 것이 좋음

IEnumerable 인터페이스

  • 컬렉션은 보통 직접 열거자를 구현하지 않고, IEnumerable을 통해 열거자를 제공함
public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

  • 장점:

저수준 사용 예제

string s = "Hello";
// string이 IEnumerable을 구현하므로 GetEnumerator() 호출 가능
IEnumerator rator = s.GetEnumerator();

while (rator.MoveNext())
{
    char c = (char)rator.Current;
    Console.Write(c + ".");
}
// 출력: H.e.l.l.o.

  • C#은 편의를 위해 foreach 문을 제공함
string s = "Hello";
foreach (char c in s)
    Console.Write(c + ".");
// 출력: H.e.l.l.o.

비제네릭 인터페이스의 사용

비제네릭 인터페이스가 필요한 경우

  • IEnumerable<T>를 구현할 때는 반드시 IEnumerable도 함께 구현해야 함
    • 보통 제네릭 버전을 주로 사용한다.
  • 대부분의 경우 직접 구현보다는 반복기 메서드나 Collection<T>, LINQ를 사용함

컬렉션 타입 통합

  • 비제네릭 인터페이스는 모든 요소 타입에 대해 컬렉션을 통합할 수 있음
  • 예: 재귀적으로 모든 요소를 카운트하는 메서드
public static int Count(IEnumerable e)
{
    int count = 0;
    foreach (object element in e)
    {
        var subCollection = element as IEnumerable;
        if (subCollection != null)
            count += Count(subCollection);
        else
            count++; //순회가 불가능할 경우
    }
    return count;
}

public IEnumerator<int> GetEnumerator()
{
	yield return 0;
}

IEnumerator IEnumerator GetEnumerator()
{
	GetEnumerator<int>(); //이런 식으로 호출해라
}
  • IEnumerable<object>를 사용하면 값 타입 요소나 IEnumerable<T>를 구현하지 않는 레거시 컬렉션(예: Windows Forms의 ControlCollection)에서 작동하지 않음
  • IEnumerable은 Object 형으로 값을 반환하고, IEnumerable<T>는 특정 데이터 형으로 저장된다.

이터레이터 함수(Iterator Function)를 이용한 구현

  • yield return을 포함하는 메서드, 속성, 인덱서를 이터레이터 함수라고 함
  • 이터레이터 함수는 클래스를 작성하지 않고도 IEnumerable/IEnumerable<T>를 구현할 수 있게 해줌

이터레이터 함수의 특징

  • 컴파일러가 yield return 문을 발견하면 숨겨진 중첩 클래스를 자동으로 생성함
  • 이 숨겨진 클래스가 IEnumerator/IEnumerator<T>를 구현함
  • GetEnumerator() 메서드는 이 숨겨진 클래스의 인스턴스를 반환하도록 리팩터링됨

이터레이터 함수의 장점

  • 열거 로직을 간단하게 작성할 수 있음
  • LINQ to Objects의 표준 쿼리 연산자 구현에도 사용됨
    • LINQ도 대부분 반복자 함수로 구현된다.
  • 컴파일러가 자동으로 올바른 열거자 클래스를 생성해줌
  • 원래는 인터페이스는 객체를 생성하지 못하지만 IEnumerator나 IEnumerable 같은 경우 yield return으로 객체 반환이 가능해진다.
// 메서드에서 이터레이터 함수 사용
public static IEnumerable<int> GetSomeIntegers()
{
    yield return 1;
    yield return 2;
    yield return 3;
}

// 속성에서 이터레이터 함수 사용
public IEnumerable<int> Numbers
{
    get
    {
        yield return 1;
        yield return 2;
        yield return 3;
        //return new ~~ 원래 이렇게 써야한다.
    }
}

// 인덱서에서 이터레이터 함수 사용
public IEnumerable<int> this[bool evenNumbers]
{
    get
    {
        if (evenNumbers)
        {
            yield return 2;
            yield return 4;
            yield return 6;
        }
        else
        {
            yield return 1;
            yield return 3;
            yield return 5;
        }
    }
}

// 사용 예:
foreach (int i in GetSomeIntegers())
    Console.WriteLine(i);

foreach (int i in obj.Numbers)
    Console.WriteLine(i);

foreach (int i in obj[true])  // 짝수만 열거
    Console.WriteLine(i);

제네릭 열거 인터페이스

IEnumerator<T>와 IEnumerable<T>의 특징

public interface IEnumerator<T> : IEnumerator, IDisposable
{
    T Current { get; }
}

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

  • 반복자 함수로 yield return을 대체할 수 있다.
  • IDisposable : 리소스 정리가 필요한 객체는 이것을 상속하면 해당 함수를 사용할 수 있게 된다.
    • using 문을 사용할 때 사용된다.
  • 장점:
    • Current와 GetEnumerator의 타입 안전성 강화
    • 값 타입 요소의 박싱 오버헤드 방지
    • 소비자 입장에서 더 편리함

배열과 제네릭 열거

  • 배열은 자동으로 IEnumerable<T> 구현함
int[] data = { 1, 2, 3 };
// 명시적 인터페이스를 통해 제네릭 IEnumerator<T> 얻기
var rator = ((IEnumerable<int>)data).GetEnumerator();

// foreach를 사용하면 위 과정이 자동으로 처리됨
foreach (int x in data)
    Console.Write(x);

IEnumerable<T>와 IDisposable

  • IEnumerator<T>는 IDisposable을 상속함
    • 데이터베이스 연결 등 리소스를 보유한 열거자가 리소스를 해제할 수 있게 함
  • foreach 문은 이를 인식하여 다음과 같이 변환됨:
// foreach (var element in somethingEnumerable) { ... }
// 위 코드는 아래처럼 변환됨:

using (var rator = somethingEnumerable.GetEnumerator())
while (rator.MoveNext())
{
    var element = rator.Current;
    // ...
}

열거 인터페이스 구현

구현이 필요한 경우

  • foreach 문 지원
  • 표준 컬렉션과의 상호운용
  • 더 복잡한 컬렉션 인터페이스 요구사항 충족
  • 컬렉션 초기화자 지원
  • LINQ를 사용하면 IEnumerable을 상속한 클래스는 LINQ에넣어 사용할 수 있게 된다.

구현 방법

  • 다른 컬렉션의 열거자 반환
  • yield return 사용
    • 반복자 함수로 대체할 수 있다.
  • IEnumerator/IEnumerator<T> 직접 구현

1. 다른 컬렉션의 열거자 반환

  • 가장 간단한 방법이지만 내부 컬렉션의 항목이 정확히 원하는 것일 때만 가능함
// 사용 예:
var collection = new StringCollection();
collection.Add("첫 번째");
collection.Add("두 번째");
collection.Add("세 번째");

foreach (string item in collection)  // 내부 List<string>의 열거자를 사용하여 열거
    Console.WriteLine(item);

// 문자열 리스트를 감싸는 단순한 래퍼 클래스
public class StringCollection : IEnumerable<string>
{
    // 내부 컬렉션
    private List<string> items = new List<string>();

    // 내부 컬렉션에 항목 추가
    public void Add(string item)
    {
        items.Add(item);
    }

    // 내부 컬렉션의 열거자를 반환
    public IEnumerator<string> GetEnumerator()
    {
        return items.GetEnumerator();  // List<string>의 열거자를 그대로 반환
    }

    // 비제네릭 버전도 구현
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

2. yield return 사용 -

public class MyCollection : IEnumerable
{
    int[] data = { 1, 2, 3 };

    public IEnumerator GetEnumerator()
    {
        foreach (int i in data)
            yield return i;
    }
}

public class MyGenCollection : IEnumerable<int>
{
    int[] data = { 1, 2, 3 };

    public IEnumerator<int> GetEnumerator()
    {
        foreach (int i in data)
            yield return i;
    }

    // 명시적 구현으로 숨김
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

3. IEnumerator 직접 구현

public class MyIntList : IEnumerable
{
    int[] data = { 1, 2, 3 };

    public IEnumerator GetEnumerator() => new Enumerator(this);

	    class Enumerator : IEnumerator
    {
        MyIntList collection;
        int currentIndex = -1;

        public Enumerator(MyIntList items) => this.collection = items;

        public object Current
        {
            get
            {
                if (currentIndex == -1)
                    throw new InvalidOperationException("Enumeration not started!");
                if (currentIndex == collection.data.Length)
                    throw new InvalidOperationException("Past end of list!");
                return collection.data[currentIndex];
            }
        }

        public bool MoveNext()
        {
            if (currentIndex >= collection.data.Length - 1) return false;
            return ++currentIndex < collection.data.Length;
        }

        public void Reset() => currentIndex = -1;
    }
}

  • 제네릭 버전:
class MyIntList : IEnumerable<int>
{
    int[] data = { 1, 2, 3 };

    public IEnumerator<int> GetEnumerator() => new Enumerator(this);
    IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); //위의 호출을 이렇게도 사용할 수 있다.

    class Enumerator : IEnumerator<int>
    {
        int currentIndex = -1;
        MyIntList collection;

        public Enumerator(MyIntList items) => this.collection = items;

        public int Current => collection.data[currentIndex];
        object IEnumerator.Current => Current;

        public bool MoveNext() => ++currentIndex < collection.data.Length;

        public void Reset() => currentIndex = -1;

        void IDisposable.Dispose() { }
    }
}

구현 시 주의사항

  • Reset 메서드는 선택적으로 구현 가능함
    • 지원하지 않을 경우 NotSupportedException을 던지면 됨
  • MoveNext의 첫 호출은 첫 번째 항목으로 이동해야 함
    • 두 번째 항목이 아닌 첫 번째 항목으로 이동해야 함

성능 고려사항

  • 제네릭 버전이 값 타입에서 박싱/언박싱이 없어 더 빠름
  • yield return을 사용한 반복기는 컴파일러가 상태 기계를 생성하므로 편리함
  • 직접 구현은 더 복잡하지만 특별한 최적화가 필요한 경우에 유용함

ICollection과 IList Interface

  • .NET은 열거(Enumeration) 기능을 제공하는 인터페이스 외에도 컬렉션의 크기를 확인하고, 인덱스로 멤버에 접근하며, 검색하고, 수정할 수 있는 기능을 제공하는 인터페이스들을 정의함
  • 이러한 기능은 ICollection, IList, IDictionary 인터페이스를 통해 제공됨
  • 각각의 인터페이스는 제네릭과 비제네릭 버전이 있음. 단, 비제네릭 버전은 주로 이전 버전과의 호환성을 위해 존재함

인터페이스 계층 구조

  • 각 인터페이스는 다음과 같은 기능을 제공함:
    • IEnumerable<T> (및 IEnumerable): 최소 기능(열거만 가능)
  • ICollection<T> (및 ICollection): 중간 기능(Count 속성 등)
  • IList<T>/IDictionary<K,V> (및 비제네릭 버전): 최대 기능(인덱스/키로 임의 접근 가능)
  • 대부분의 경우 이러한 인터페이스를 직접 구현할 필요는 없으며, Collection<T>를 상속하여 컬렉션 클래스를 작성하는 것이 더 효율적임

ICollection<T>와 ICollection 인터페이스

ICollection<T> 인터페이스

  • ICollection<T>는 개수를 셀 수 있는 객체의 컬렉션을 위한 표준 인터페이스임
  • 다음과 같은 기능을 제공함:
    • 컬렉션의 크기 확인(Count)
    • 아이템 존재 여부 확인(Contains)
    • 배열로 복사(ToArray)
    • 읽기 전용 여부 확인(IsReadOnly)
    • 쓰기 가능한 컬렉션의 경우 아이템 추가(Add), 제거(Remove), 모두 제거(Clear) 가능
  • IEnumerable<T>를 상속하므로 foreach 문으로 순회 가능함
    • IEnumerable<T>를 상속하려면 IEnumerable(비제네릭 버전)도 같이 상속받아야한다.
public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
    int Count { get; }
    bool Contains(T item); //탐색 함수
    void CopyTo(T[] array, int arrayIndex); //복사 함수
    bool IsReadOnly { get; }
    void Add(T item);
    bool Remove(T item); 
    void Clear();
}

ICollection 인터페이스

  • 비제네릭 버전은 아이템의 추가/제거 기능이 없음
  • 대신 스레드 동기화를 위한 속성들을 제공함(제네릭 버전에서는 제외됨)
public interface ICollection : IEnumerable
{
    int Count { get; }
    bool IsSynchronized { get; }
    object SyncRoot { get; }
    void CopyTo(Array array, int index);
}

IList<T>와 IList 인터페이스

IList<T> 인터페이스

  • IList<T>는 위치로 인덱싱할 수 있는 컬렉션을 위한 표준 인터페이스임
  • ICollection<T>와 IEnumerable<T>에서 상속받은 기능 외에도 다음을 제공함:
    • 위치로 요소 읽기/쓰기(인덱서 사용)
    • 위치로 삽입/제거
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
    T this[int index] { get; set; } //인덱서가 추가된것이다.
    int IndexOf(T item);
    void Insert(int index, T item);
    void RemoveAt(int index);
}
  • IndexOf 메서드는 배열에서 선형 검색을 수행하며, 지정된 아이템이 없으면 -1을 반환함

IList 인터페이스

  • 비제네릭 버전은 ICollection에서 적은 기능을 상속받기 때문에 더 많은 멤버를 가짐
public interface IList : ICollection, IEnumerable
{
    object this[int index] { get; set }
    bool IsFixedSize { get; }
    bool IsReadOnly { get; }
    int Add(object value);
    void Clear();
    bool Contains(object value);
    int IndexOf(object value);
    void Insert(int index, object value);
    void Remove(object value);
    void RemoveAt(int index);
}

  • 비제네릭 IList의 Add 메서드는 새로 추가된 아이템의 인덱스를 반환함. 반면 ICollection<T>의 Add 메서드는 void 반환형을 가짐

다차원 배열과 IList 사용 시 주의사항

  • 다차원 배열을 IList의 인덱서로 접근하려고 하면 ArgumentException이 발생함
  • 이는 겉보기에는 안전해 보이는 다음과 같은 메서드에서도 문제가 될 수 있음:
public object FirstOrNull(IList list)
{
    if (list == null || list.Count == 0) return null;
    return list[0];
}

  • 다차원 배열을 런타임에 확인하려면 다음 표현식을 사용함:
list.GetType().IsArray && list.GetType().GetArrayRank() > 1

제네릭과 비제네릭 인터페이스 구현

  • IList<T>가 IList를 상속하지 않는 것은 타입 안전성을 위한 것임
  • 만약 상속했다면, IList<T>로 캐스팅할 때 Add(T)와 Add(object) 멤버를 모두 갖게 되어 정적 타입 안전성이 깨질 수 있음
    • 이러면 어떤 타입의 객체든 Add로 추가할 수 있게 됨

읽기 전용 인터페이스

IReadOnlyCollection<T>와 IReadOnlyList<T>

  • .NET은 읽기 전용 작업에 필요한 멤버만 노출하는 컬렉션과 리스트 인터페이스도 정의함
public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
{
    int Count { get; }
}

public interface IReadOnlyList<out T> : IReadOnlyCollection<T>,
IEnumerable<T>, IEnumerable
{
    T this[int index] { get; }
}

  • 이러한 인터페이스들의 타입 파라미터는 출력 위치에서만 사용되므로 공변성(covariant)으로 표시됨
    • 예: 고양이 리스트를 동물의 읽기 전용 리스트로 취급할 수 있음
  • 읽기 전용 인터페이스들은 컬렉션이나 리스트의 읽기 전용 뷰를 나타내며, 기본 구현은 여전히 쓰기 가능할 수 있음
    • 대부분의 쓰기 가능한(가변) 컬렉션들은 읽기 전용과 읽기/쓰기 인터페이스를 모두 구현함
  • IReadOnlyList<T>는 Windows Runtime 타입 IVectorView<T>에 매핑됨
  • 읽기 전용 인터페이스를 사용하면 클래스가 공개적으로는 읽기 전용 뷰를 노출하면서 내부적으로는 쓰기 가능한 private 컬렉션을 유지할 수 있음
  • 이는 클래스의 캡슐화(encapsulation)와 불변성(immutability)을 향상시킴

읽기 전용 인터페이스 사용 예제

  • 팀과 팀원을 관리하는 간단한 예제:
public class Team
{
    // 내부적으로는 수정 가능한 리스트를 사용
    private List<string> members = new List<string>();

    // 외부에는 읽기 전용 인터페이스로 노출
    public IReadOnlyList<string> Members => members; //읽기전용으로 형변환

    // 멤버 추가는 오직 Team 클래스의 메서드를 통해서만 가능
    public void AddMember(string name)
    {
        members.Add(name);
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Team team = new Team();
        team.AddMember("Alice");
        team.AddMember("Bob");

        // 읽기는 가능
        foreach (string member in team.Members)
        {
            Console.WriteLine(member);
        }

        // 컴파일 에러 - Members는 읽기 전용
        // team.Members.Add("Charlie");    // 이 줄은 컴파일되지 않음

        // 인덱서로 읽기는 가능
        Console.WriteLine(team.Members[0]); // "Alice" 출력

        // 컴파일 에러 - 수정 불가능
        // team.Members[0] = "Carol";      // 이 줄은 컴파일되지 않음
    }
}

  • 내부적으로 List<string>을 사용하여 멤버를 관리함
  • 외부에는 IReadOnlyList<string>을 통해 읽기 전용 접근만 허용함
  • 멤버 추가는 오직 AddMember 메서드를 통해서만 가능함
  • 이를 통해 팀 멤버 리스트의 무결성(integrity)을 보장할 수 있음

배열 클래스(Array Class)

기본 개념

  • 배열 클래스(Array class)는 모든 단일 차원과 다차원 배열의 암시적 기본 클래스임.
  • 배열은 IList<T>와 IList 인터페이스를 제네릭과 비제네릭 형태 모두 구현함.
    • IList를 상속하고 있으니 IEnumerable, IEnumerator도 상속하고 있다.
  • IList<T> 구현은 명시적으로 되어있어 Add나 Remove 같은 크기를 변경하는 메서드는 호출 시 예외를 발생시킴.
  • Array는 실제로 정적 메서드를 많이 제공하므로, 인스턴스 메서드가 있을 것으로 예상되는 부분에서도 정적 메서드를 찾아봐야 함.
  • 기본 자료형에 관계없이 모든 배열에 공통된 메서드를 제공함.
  • Array 클래스는 배열의 형식 통합(type unification)을 제공하여 차원이나 요소 형식에 관계없이 일관된 작업이 가능함.

배열의 특징

  • C#은 배열 선언과 초기화를 위한 특별한 구문을 제공함.
  • CLR은 배열 선언 시 암시적으로 Array 클래스를 상속받는 의사 형식(pseudo-type)을 생성함.
  • 생성된 의사 형식은 제네릭 컬렉션 인터페이스(IList<string> 등)를 구현함.
  • 배열은 메모리에 연속적으로 할당되어 인덱싱이 매우 효율적임.
  • 크기가 고정되어 있어 나중에 크기를 변경할 수 없음.

배열의 구조

  • CLR은 배열에 대해 2GB 크기 제한을 둠(32비트나 64비트 환경 모두 해당).
  • Array 클래스는 64비트 인덱서를 제한적으로 지원하지만, 2GB 제한으로 인해 실제로는 활용이 제한됨.
  • 컬렉션이 고민되는 경우라면 일단 ‘배열’로 구현하는게 좋다.

값 형식과 참조 형식 요소

// 값 형식 배열: 각 요소가 실제 값을 저장
long[] numbers = new long[3];    // 각 요소 8바이트, 총 24바이트
numbers[0] = 12345;
numbers[1] = 54321;

// 참조 형식 배열: 각 요소가 참조를 저장
StringBuilder[] builders = new StringBuilder[5]; // 64비트에서 각 요소 8바이트
builders[0] = new StringBuilder("builder1");
builders[1] = new StringBuilder("builder2");
builders[2] = new StringBuilder("builder3");
  • 값형식의 경우가 연속적인 공간 할당의 장점이 적용되는 예시다.

배열 참조 특성

  • 배열은 요소 형식에 관계없이 항상 참조 형식임.
    • Array ‘클래스’ 이기 때문이다.
  • 동일한 배열을 참조하는 두 변수는 같은 배열을 가리킴.
  • 별도의 배열은 구조적으로 동일해도 등가 비교시 false를 반환함.
object[] a1 = { "string", 123, true };
object[] a2 = { "string", 123, true };
Console.WriteLine(a1 == a2);     // False
Console.WriteLine(a1.Equals(a2)); // False

// 구조적 비교시에는 true -> 값 비교라는 것이다.
IStructuralEquatable se1 = a1;
Console.WriteLine(se1.Equals(a2, StructuralComparisons.StructuralEqualityComparer)); // True

  • StructuralEqualityComparer : 구조적 동등성 비교 옵션이다.
    • 값 형식 비교 라는 뜻이다.
    • 이렇게 하면 굳이 형변환해서 값을 비교하지 않아도 된다.

배열 복제

  • Clone() 메서드로 배열을 복제할 수 있음.
  • 얕은 복사(shallow copy)가 수행됨:
    • 값 형식: 값이 복사됨
    • 참조 형식: 참조만 복사됨
StringBuilder[] builders = new StringBuilder[5];
builders[0] = new StringBuilder("one");
builders[1] = new StringBuilder("two");
builders[2] = new StringBuilder("two");

StringBuilder[] builders2 = builders;

// 얕은 복사 수행
StringBuilder[] shallowClone = (StringBuilder[]) builders.Clone();

// 원본과 복제본이 같은 객체를 참조
builders[0].Append("!");
Console.WriteLine(shallowClone[0]); // "one!" 출력

배열 생성과 인덱싱

기본 문법

// 배열 생성과 초기화
int[] myArray = { 1, 2, 3 };

// 인덱스로 접근
int first = myArray[0];
int last = myArray[myArray.Length - 1];

동적 배열 생성

  • Array.CreateInstance() 메서드로 런타임에 배열 생성 가능함
  • 요소 형식, 차원, 하한 경계를 지정할 수 있음
  • 이런게 있다는것만 알아둬라
// 기본 사용법 - 2개 요소를 가진 문자열 배열 생성
Array a = Array.CreateInstance(typeof(string), 2);
a.SetValue("hi", 0);     // a[0] = "hi" 와 동일
a.SetValue("there", 1);  // a[1] = "there" 와 동일

string s = (string)a.GetValue(0); // s = a[0] 와 동일

// C# 배열로 캐스팅 가능
string[] cSharpArray = (string[])a;
string s2 = cSharpArray[0];

// 하한 경계가 1인 배열 생성
int[] lengths = { 3 };        // 길이 지정
int[] lowerBounds = { 1 };    // 하한 경계 지정
Array oneBasedArray = Array.CreateInstance(typeof(int), lengths, lowerBounds);

oneBasedArray.SetValue(100, 1);   // 인덱스 1에 값 설정
oneBasedArray.SetValue(200, 2);   // 인덱스 2에 값 설정
oneBasedArray.SetValue(300, 3);   // 인덱스 3에 값 설정

Console.WriteLine(oneBasedArray.GetValue(1)); // 100
Console.WriteLine(oneBasedArray.GetLowerBound(0)); // 1
Console.WriteLine(oneBasedArray.GetUpperBound(0)); // 3

배열 요소 접근과 열거

GetValue와 SetValue

  • 모든 배열에서 사용 가능한 통합된 메서드
  • 다차원 배열의 경우 인덱스 배열을 매개변수로 사용
public object GetValue(params int[] indices)
public void SetValue(object value, params int[] indices)

// 사용 예시
void WriteFirstValue(Array a)
{
    Console.Write(a.Rank + "-dimensional; ");
    int[] indexers = new int[a.Rank];
    Console.WriteLine("First value is " + a.GetValue(indexers));
}

void Demo()
{
    int[] oneD = { 1, 2, 3 };
    int[,] twoD = { {5,6}, {8,9} };
    WriteFirstValue(oneD); // "1-dimensional; First value is 1" 출력
    WriteFirstValue(twoD); // "2-dimensional; First value is 5" 출력
}

배열 열거

  • foreach 문으로 쉽게 열거 가능
  • Array.ForEach 정적 메서드 사용 가능
int[] myArray = { 1, 2, 3 };

// foreach 사용
foreach (int val in myArray)
    Console.WriteLine(val);

// Array.ForEach 사용
Array.ForEach(new[] { 1, 2, 3 }, Console.WriteLine);

  • ForEach<int>가 작동되는 것이다.
    • 이유 : new[]의 요소가 int형이기 때문이다.

길이와 차원

관련 메서드와 속성

public int GetLength(int dimension);           // 지정 차원의 길이
public long GetLongLength(int dimension);      // 64비트 버전
public int Length { get; }                     // 총 요소 수
public long LongLength { get; }                // 64비트 버전
public int GetLowerBound(int dimension);       // 하한 경계
public int GetUpperBound(int dimension);       // 상한 경계
public int Rank { get; }                       // 차원 수

사용 예제

// 2차원 배열 생성
int[,] matrix = new int[3, 4];

// 차원과 길이 정보 출력
Console.WriteLine($"차원 수: {matrix.Rank}");           // 2
Console.WriteLine($"총 요소 수: {matrix.Length}");      // 12
Console.WriteLine($"첫 번째 차원 길이: {matrix.GetLength(0)}"); // 3
Console.WriteLine($"두 번째 차원 길이: {matrix.GetLength(1)}"); // 4

// 경계 정보 출력
Console.WriteLine($"첫 차원 하한: {matrix.GetLowerBound(0)}"); // 0
Console.WriteLine($"첫 차원 상한: {matrix.GetUpperBound(0)}"); // 2
Console.WriteLine($"둘째 차원 하한: {matrix.GetLowerBound(1)}"); // 0
Console.WriteLine($"둘째 차원 상한: {matrix.GetUpperBound(1)}"); // 3

  • GetLowerBound(차원) : 해당 차원의 최솟값
  • GetUpperBound(차원) : 해당 차원의 최댓값