Rust 프로그래밍

Rust 배열과 for 루프, 함수 - C 코드와 비교 (2)

plas 2020. 7. 15. 13:03

다음의 C 코드를 Rust로 바꾸어 보겠습니다. 배열과 for 루프, 그리고 함수에 배열을 넘기는 것이 포함됩니다.

void analyze_slice(int arr[], int len)
{
    printf("arr[0] = %d, 길이: %d\n", arr[0], len);
}
void main()
{
    int xs[] = {1, 2, 3, 4, 5};
    int ys[500] = {0};
    
    printf("xs 배열의 크기: {}", sizeof(xs)/sizeof(int));

    // 배열을 대여 - 함수 호출
    analyze_slice(xs, 5);

    for (int x=0; x < 500; x++) {
        ys[x] = x;
    }
    // 배열의 슬라이스를 대여
    analyze_slice(&ys[5], 3);    
}

배열을 2개 선언하고 선언 초기화와 for 루프를 이용한 초기화 예를 보여줍니다. 그리고 analyze_slice라는 함수에서는 주소를 포인터로 받아 첫 번째 칸의 값을 출력합니다. 여기서 두 번째 함수 호출에서는 &ys[5]을 보냈는데, 이것은 함수이 여섯 번째 칸의 주소를 나타냅니다. 그럼 받는 쪽에서는 그 위치부터의 배열을 arr로 받게 됩니다. 또한 C 언어에서는 배열을 포인터로 넘기므로 크기를 별도의 매개변수로 보내야 합니다.

그럼 먼저 배열의 선언부와 초기화 부분을 살펴보도록 하겠습니다.

    // 고정크기 배열 (let xs:[i32; 5]의 생략)
    let xs = [1, 2, 3, 4, 5];

    // 모든 값을 0으로 초기화
    let mut ys: [i32; 500] = [0; 500];

첫 줄은 고정크기 배열의 선언을 보여줍니다. C와 마찬가지로 배열의 초기값을 나열하면 크기가 자동으로 설정됩니다. 그리고 숫자의 경우 디폴트로 i32 타입으로 설정됩니다.

두 번째 선언은 배열을 500칸 선언하며 모두 0으로 초기화하는 것을 보여줍니다. 이것은 C 코드에서의 int ys[500]={0};에 대응합니다. 또한 ys는 값을 나중에 바꿀 수 있어야 하므로 앞에 mut라고 써주었습니다.

fn analyze_slice(slice: &[i32]) {
    println!("[0] = {}, 길이: {}", slice[0], slice.len());
}
...
   analyze_slice(&xs);

다음은 함수에 배열을 매개변수로 보내는 방법입니다. 배열을 참조로 보내서 해당 주소의 값을 읽게 하기 위해 &[i32]를 썼습니다. 이것은 C 언어에서 (int slice[])과 같은 의미가 됩니다. 단, 러스트에서는 배열의 크기를 배열 자체가 알고 있으므로 C 처럼 별도로 길이를 매개변수로 보낼 필요가 없습니다. 러스트 함수에서는 slice 참조를 대여해서 받았다가 함수가 끝나면 돌려주게 됩니다. 

다음으로 ys 배열의 초기값을 설정하는 for 루프를 살펴보겠습니다. 

    for x in 0..500 {
        ys[x] = i32::try_from(x).unwrap();
    }
/*  for (int x=0; x < 500; x++) {
        ys[x] = x;
    }
*/

여기서 C와는 상당히 다른 구문이 등장합니다. for x in 0..500은 파이썬에서 보았던 구문입니다. x 변수가 0부터 499까지의 값을 차례로 반복하게 됩니다. 다음 줄에서는 C처럼 간단히 ys[x] = x; 라고 쓸 수가 없습니다. 러스트의 복잡한 타입의 관계를 알아야 되는데, 여기서 for 루프 인덱스는 usize라는 타입을 가지게 되고 ys[x]는 i32 타입입니다. 이 둘간에 자동 변환은 물론이고 그냥 i32.from(x)라고 써서 변환하는 것도 안 됩니다. 왜냐하면 overflow가 생길 여지가 있기 때문입니다. 그래서 try_from과 unwrap을 써서 형변환을 해주고 있습니다. 게다가 use로 try_from을 임포트해 주어야 합니다. 

그럼 마지막으로 ys 배열의 5번째부터를 배열로 넘기는 것을 살펴보겠습니다. 이것은 C 언어에서는 스트링의 일부분이나 배열의 일부를 나타내기 위해 많이 사용합니다. 러스트에서도 이것을 슬라이스라는 개념으로 중요하게 업그레이드했습니다. 배열의 일부를 자유롭게 표현할 수있게 됩니다.

analyze_slice(&ys[5 .. 8]);

그리고 그 슬라이스를 대여해서 배열에게 전달합니다. 슬라이스는 다음과 같은 그림으로 표현될 수 있습니다.

이 그림은 문자열에 대한 슬라이스를 보여주지만 개념은 같습니다. 배열 s에 대해 중간부터 길이 5의 슬라이스를 나타내는 world라는 변수가 생긴 것이지요. 이 때 world는 참조를 대여한 것이라고 볼 수 있습니다.

위의 ys 배열 슬라이스 참조도 마찬가지입니다. 5..8로 두번째 칸부터 세 개를 슬라이스로 함수에 참조를 대여합니다. 그럼 함수 안에서는 그것이 배열로 온 것과 같습니다. 길이는 3으로 설정되어 있겠지요.

이제 전체를 하나의 코드로 모아서 살펴보면 다음과 같습니다. 이 코드를 rust playground에서 실행해 보세요. 

use std::convert::TryFrom;

// 배열을 가져와서 첫 원소와 길이를 출력
fn analyze_slice(slice: &[i32]) {
    println!("[0] = {}, 길이: {}", slice[0], slice.len());
}

fn main() {
    // 고정크기 배열 (let xs:[i32; 5]의 생략)
    let xs = [1, 2, 3, 4, 5];

    // 모든 값을 0으로 초기화
    let mut ys: [i32; 500] = [0; 500];

    // 배열의 크기 출력
    println!("배열의 크기: {}", xs.len());

    // 배열을 대여 - 함수 호출
    analyze_slice(&xs);

    for x in 0..500 {
        ys[x] = i32::try_from(x).unwrap();
    }
    // 배열의 슬라이스를 대여
    analyze_slice(&ys[5 .. 8]);
}