카테고리 없음

[C# 8.0] 배열 접근을 파이썬처럼! Range와 Index (ft. 깊은 복사)

카루-R 2022. 3. 2. 15:54
반응형

환영합니다, Rolling Ress의 카루입니다.

 

C# 8.0 들어서 슬라이싱 기능이 추가가 됐죠. 이제 더 이상 substring() 같은 거 쓸 필요 없습니다. 바로 예제 코드부터 볼게요.

 

** C# 9.0 / 10.0 의 최상위 문 및 전역 using을 사용합니다.

int[] classes = new int[] 
{
    1, 2, 3, 4, 5, 6, 7, 8,
};

Console.WriteLine($"마지막 반은 {classes[^1]}반 입니다.");
 

출력 결과: 마지막 반은 8반 입니다.

 

배열 형식 컨테이너에 접근할 수 있는 방법이 다양해졌습니다. 인덱스 연산이 많아졌는데, 역순 인덱스가 추가되었습니다. 파이썬에서 [-1]라 하면 마지막 원소를 선택했는데, C# 에서는 [^1]을 통해 마지막 원소를 가져올 수 있습니다. ^2 하면 마지막에서 두 번째 원소를 뜻합니다. 여기선 '7'이 되겠네요.

 

대신, 수를 나타낼 때 0과 -0은 같지만 여기선 달라요. ^0은 첫번째가 아니라 인덱스의 끝을 뜻합니다. 실제로 참조하려고 하면 IndexOutOfRangeException이 나오는 부분.

var lists = classes[0..^0] // 또는 [..]
 

.. 연산자는 파이썬의 : 연산자와 같다고 보시면 됩니다. a..b라고 쓰면 [a, b) 구간의 요소를 반환합니다. 즉, 위 코드는 전체 array를 깊은 복사로 전달하는 것과 같습니다.

int[] numArray = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int[] copyArray = numArray;
copyArray[1] = 9;
copyArray[^1] = 2;

Array.ForEach(numArray, n => Console.Write($"{n}, "));
 

출력 결과: 1, 9, 3, 4, 5, 6, 7, 8, 9, 2,

 

아까 말씀드렸지만, ^1은 마지막 원소를 반환합니다. 1이 두번째 원소를 반환하는 것과 대조적이죠. 헷갈리지 마세요. 아무튼, 여기 보시면 나는 분명히 copyArray 변경했는데, 생뚱맞게 numArray의 값이 바뀝니다. 왜일까요? copyArray = numArray 는 그냥 얕은 복사를 수행합니다. 관리 힙에 실제 배열이 있는데, 그냥 주소만 똑같이 가리키고 있을 뿐이죠. 그래서 하나를 변경하면 다른 것도 변경됩니다. 똑같은 주소를 가리키고 있으니까요.

 

깊은 복사로 이걸 해결해봅시다.

int[] numArray = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int[] copyArray = numArray[0..^0]; 
copyArray[1] = 9;
copyArray[^1] = 2;

Array.ForEach(numArray, n => Console.Write($"{n}, "));
 

출력 결과: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

 

이제 제대로 됐군요. 값 형식 원소들을 깊은 복사로 반환했습니다. 0부터 ^0까지 새로운 배열을 만드는 과정으로 볼 수 있죠. 그렇기에, 둘은 다른 메모리 주소를 가리키고 있습니다. 깊은 복사를 할 수 있는 몇 가지 방법이 있는데, 하나씩 보죠.

// C# 8.0 Range 사용
int[] copyArray = numArray[0..^0];

// Clone() 메서드. 반드시 캐스팅 필요.
int[] copyArray = (int[])numArray.Clone();

// LINQ 메서드 사용
int[] copyArray = numArray.Select(num => num).ToArray();

// LINQ 표현식 사용
int[] copyArray = (from n in numArray select n).ToArray();
 

뭐가 제일 간단한진 답이 나오죠. 보통 Clone()을 자주 사용하셨을텐데, 이건 object 반환이라 캐스팅을 해줘야 한다는 번거로움이 있죠. 저는 그냥 Range 쓰겠습니다.

Range range = 1..5;
var sub = array[range];

Index index = ^1;
array[index] = 999;
 

참고로 이러한 연산자들은 Range와 Index 타입의 인스턴스를 반환하기에, 그냥 이렇게 변수에 따로 담아다가 재사용할 수도 있습니다. 다만 형식이 readonly struct라, 시작 값 변경 등 필드 수정은 불가능해요. 그냥 새로 대입해야 합니다.

string str = "This is a sample string";
Console.WriteLine(str[1..7]);
 

출력 결과: his is

 

string 컨테이너도 이걸 지원합니다. SubString() 메서드를 쓰지 않고 Range로 바로 받을 수 있어요.

string str = "This is a sample string";
string sub = str[..7];
Console.WriteLine(sub);
 

출력 결과: This is

 

참고로 시작 인덱스나 종료 인덱스를 생략하면 처음 또는 끝을 의미합니다. 시작 인덱스가 사라지면 0이, 끝 인덱스가 사라지면 ^0이 자동으로 대입되는 것과 같은 효과를 냅니다.

 

즉, 아래 둘은 같은 코드입니다.

string sub = str[..];
string sub = str[0..^0];
 

 

 

반응형