Page

관련 포스팅

2023년 9월 16일 토요일

Week02 - Rust 기초 문법 (Slice)

이번 포스팅에서는 slice에 대해서 다뤄보도록 하겠다.

pub fn run() {
    let a = [3, 2, 9, -1, 8];
    println!("{}", max(&a));

    let v = vec![3, 2, 7, 6, 8];
    println!("{}", max(&v[1..4]));
    println!("{}", max(&v[1..=4]));
    println!("{}", max2(&v[1..=4]));

    if let Some(x) = find_even(&v) {
        println!("{x}");
    } else {
        println!("no even numbers");
    }

    // example of use of an iterator
    let v2 = a.into_iter().collect::<Vec<_>>();
    println!("{v2:?}");
}

코드 분석

이 코드는 배열과 벡터에서 최대값을 찾는 함수와, 벡터에서 첫 번째 짝수를 찾는 함수를 예시로 보여주고 있다.

  1. max(&a): 배열 a에서 최대값을 찾아서 출력한다.
    1. 여기서 a대신 &a를 넣는 이유는 함수 내에서 배열의 소유권을 넘기지 않고 참조만 전달하기 위해서이다. 만약 a만 넣으면, 배열 a의 소유권이 max 함수로 넘어가고, 함수가 끝난 뒤에는 a를 더 이상 사용할 수 없게 되기 때문이다.
    2. &a를 넣어서 참조를 전달하면, max 함수는 a 배열에 대한 읽기 권한만 얻으므로 원본 배열에는 아무런 영향을 미치지 않는다. 그래서 max 함수가 끝난 뒤에도 여전히 a를 사용할 수 있다.
  2. max(&v[1..4]): 벡터 v의 인덱스 1에서 3까지 범위에서 최대값을 찾아서 출력한다.
  3. max(&v[1..=4]): 벡터 v의 인덱스 1에서 4까지 범위에서 최대값을 찾아서 출력한다.
  4. max2(&v[1..=4]): 동일한 작업을 하는데, max2라는 다른 함수를 사용한다.
  5. find_even(&v): 벡터 v에서 첫 번째 짝수를 찾는다. 찾으면 그 값을 출력하고, 없으면 "no even numbers"를 출력한다.
  6. let v2 = a.into_iter().collect::<Vec<_>>();: 배열 a를 벡터 v2로 변환한다.

백터와 배열

let a = [3, 2, 9, -1, 8];
let v = vec![3, 2, 7, 6, 8];

a는 배열(array)이고, v는 벡터(vector)다. 둘은 Rust에서 제공하는 서로 다른 컬렉션 타입이다.

  1. 크기 고정 vs 동적

    • 배열 a는 크기가 고정되어 있다. 즉, 5개의 정수를 저장할 수 있고, 이 크기는 변경할 수 없다.
    • 벡터 v는 동적이다. 원하는 만큼 요소를 추가하거나 제거할 수 있다.
  2. 타입

    • 배열 a[i32; 5] 타입을 가진다. 크기 정보도 타입에 포함된다.
      • [i32; 5] 타입은 Rust에서 배열을 표현하는 방식 중 하나다. 이 타입은 i32 타입의 원소를 정확히 5개 가질 수 있는 배열을 나타낸다.

      • i32는 32비트 정수를 나타내는 타입이다.

      • 5는 배열의 크기나 길이를 나타낸다. 이 배열은 정확히 5개의 i32 원소를 가질 수 있다.

      • 예를 들어, let a = [1, 2, 3, 4, 5]; 같은 코드에서 a[i32; 5] 타입을 가질 것이다.

      • 이렇게 타입 정보에 배열의 크기까지 명시되는 것은 배열이 고정된 크기를 가진다는 것을 명확히 보여준다.

    • 벡터 vVec<i32> 타입을 가진다. 크기 정보는 타입에 포함되지 않는다.
      • 크기가 동적으로 변할 수 있는 i32 타입 배열을 나타낸다.
  3. 표준 라이브러리

    • 배열은 Rust의 내장 타입이다.
      • "배열은 Rust의 내장 타입이다"라는 문장은, 배열이 Rust 언어 자체에 내장되어 있는 기본 타입이라는 의미다. 즉, 특별한 라이브러리를 추가하지 않아도 사용할 수 있다.
    • 벡터는 표준 라이브러리에서 제공된다.
      • "벡터는 표준 라이브러리에서 제공된다"는, 벡터는 Rust의 표준 라이브러리(std)에 정의되어 있는 타입이라는 것을 의미한다. 표준 라이브러리는 Rust 설치와 함께 제공되긴 하지만, 기본 언어 구조에는 포함되어 있지 않다.
  4. 유연성

    • 배열은 크기가 고정되어 있어 유연성이 떨어진다.
    • 벡터는 push, pop 같은 메서드를 통해 동적으로 크기를 변경할 수 있다.

그래서, 어떤 상황에서는 고정된 크기의 배열이 적합하고, 어떤 상황에서는 유연한 크기의 벡터가 적합하다.

if let Some(x) = find_even(&v)

if let 문은 Rust에서 특정 패턴에 맞는 값이 있을 때만 블록 안의 코드를 실행하게 해준다. Option<T> 타입의 값(여기서는 find_even(&v)의 반환값)이 Some(x)일 경우에만 if let 블록 내의 코드가 실행된다.

  • Option<T>는 Rust에서 어떤 값이 있을 수도, 없을 수도 있을 때 사용하는 타입이다. 이게 무슨 말이냐면, 예를 들어 사과 상자에서 빨간 사과를 찾으려고 할 때, 빨간 사과가 있을 수도 있고 없을 수도 있다. 빨간 사과가 있다면 그걸 가져오고, 없다면 빈 손으로 돌아온다.

    • Some(사과)는 "사과를 찾았다!"라는 뜻이고,

    • None은 "사과가 없어서 빈 손이다"라는 뜻이다.

    • 이제 if let Some(x) = find_even(&v) 이 코드를 생각해보자. 이 코드는 "상자 안에 빨간 사과가 있으면 그걸 x에 넣어줘" 같은 뜻이다. find_even(&v)가 짝수를 찾으면 Some(짝수)를 반환하고, 그 값이 x에 들어가게 된다. 그리고 if let 뒤에 있는 코드가 실행된다. 만약 짝수를 못 찾으면, None이 반환되고, if let 뒤에 있는 코드는 실행되지 않는다.

    • 간단하게 말하면, if let은 "만약 이런 값이 있다면, 그 값을 가져와서 이 일을 해줘"라는 뜻이다.

find_even(&v) 함수는 아마도 벡터 v에서 첫 번째 짝수를 찾아 Some(짝수)를 반환하거나, 짝수가 없을 경우 None을 반환할 것이다.

  • Some(x)가 반환된다면, x는 그 짝수 값이고, if let 블록 내의 코드가 실행된다.
  • None이 반환된다면, if let 블록 내의 코드는 실행되지 않는다.

간단한 예를 들면,

  • find_even(&v)Some(2)를 반환한다면, x2가 되고 블록 내의 코드가 실행된다.
  • find_even(&v)None을 반환한다면, 블록 내의 코드는 실행되지 않는다.

이렇게 if let을 사용하면 match 문을 사용하는 것보다 간결하게 특정 패턴에 대해서만 코드를 실행할 수 있다.

동작

가령 v라는 배열이 [1, 3, 4, 7]이라고 하자. 이 배열에서 짝수를 찾는 find_even(&v) 함수를 실행하면, 여기서 짝수는 4이다.

그래서 find_even(&v)의 반환값은 Some(4)가 된다.

이제 if let Some(x) = find_even(&v)을 실행하면 이렇게 동작한다:

find_even(&v)를 실행해서 Some(4)를 가져온다. Some(4)에서 4를 꺼내서 x에 저장한다. 그리고 if let 블록 내의 코드가 실행된다. 여기서 x는 4이다. 만약 v 배열이 [1, 3, 5, 7]이라면, 짝수가 없다. 이 경우 find_even(&v)는 None을 반환한다.

그러면 if let Some(x) = find_even(&v)이 동작하는 과정은:

find_even(&v)를 실행해서 None을 가져온다. 값이 None이므로, if let 블록 내의 코드는 실행되지 않는다.

Some(4)에서 4를 왜, 그리고 어떻게 꺼내는가?

Some(4)에서 4를 꺼내는 과정은 Rust의 if let 문법을 통해 이루어진다. 이 문법은 Option<T> 타입의 값이 Some인 경우에만 코드 블록을 실행하고, 그 Some 안에 들어있는 값을 변수에 바인딩한다.

코드에서 if let Some(x) = find_even(&v)를 보면:

  • find_even(&v)의 반환값이 Some(4)라고 가정하자.
  • if let Some(x) = Some(4)이 실행된다.
  • 여기서 Some(x) 패턴은 Some(4)와 일치하므로, x4가 바인딩된다.
  • 이후 if let 블록 내의 코드가 실행되고, 여기서 x4의 값을 가진다.

if let 문법은 패턴 매칭을 통해 Some 안의 값을 꺼내서 변수에 저장하는 역할을 한다. 이렇게 꺼낸 값은 if let 블록 내에서 사용할 수 있다.

// &[T] is a fat pointer: ptr to beginning + length
fn max(s: &[i32]) -> i32 {
    let mut max = s[0];

    for i in 1..s.len() {
        if s[i] > max {
            max = s[i];
        }
    }
    max
}

fn max2(s: &[i32]) -> i32 {
    let mut max = s[0];

    for x in s {
        // x has type &i32
        if *x > max {
            max = *x;
        }
    }
    max
}

이 두 함수 maxmax2는 배열 또는 벡터의 참조(&[i32])를 인자로 받아서 그 중에서 가장 큰 값을 찾는 역할을 한다. 하지만 이 두 함수는 내부적으로 다른 방법을 사용해 값을 찾는다.

max 함수

  • max 함수는 s[0]을 초기 max 값으로 설정한다.
  • 1..s.len() 범위로 for 루프를 돌며, 배열의 각 원소(s[i])와 현재 max 값을 비교한다.
  • 만약 배열의 원소가 max보다 크다면, max 값을 그 원소로 업데이트한다.
  • 루프가 끝나면 가장 큰 값을 반환한다.

max2 함수

  • max2 함수는 역시 s[0]을 초기 max 값으로 설정한다.
  • 이 함수는 배열의 각 원소에 대해 for x in s 루프를 돌린다. 여기서 x&i32 타입이다.
  • *x > max 비교를 통해 원소 값과 max 값을 비교한다.
  • max가 원소보다 작다면, max 값을 그 원소로 업데이트한다.
  • 루프가 끝나면 가장 큰 값을 반환한다.

차이점

  1. max 함수는 인덱스를 사용해 배열을 순회한다.
  2. max2 함수는 for x in s 구문을 사용해 배열을 순회한다. 이 방법은 좀 더 "Rust스럽다"고 할 수 있다. <U>여기서 x는 참조(&i32)이기 때문에, 실제 값을 사용하기 위해 *x로 역참조한다.</U>

두 함수는 동일한 작업을 수행하지만, max2 함수가 좀 더 Rust의 특성에 맞는 방식으로 배열을 순회한다.

fn find_even(s: &[i32]) -> Option<i32> {
    for x in s {
        if *x % 2 == 0 {
            return Some(*x);
        }
    }
    None
}

find_even 함수는 정수의 배열 또는 벡터의 참조(&[i32])를 입력으로 받아서 그 중에 짝수가 있으면 그 짝수를 반환한다. 반환 타입은 Option<i32>인데, 짝수를 찾으면 Some(짝수)를 반환하고, 없으면 None을 반환한다.

  • 함수는 for x in s 루프를 돌면서 배열의 각 원소(x)를 확인한다. 이때 x&i32 타입이다.
  • if *x % 2 == 0 조건문을 통해 해당 원소가 짝수인지 확인한다. 짝수면 Some(*x)를 반환한다. 여기서 *x는 역참조를 통해 원래 값(i32)을 가져온다.
  • 배열에 짝수가 없다면 루프가 끝날 때까지 Some(*x)를 반환하지 않고, 마지막에 None을 반환한다.

간단하게 요약하면, 이 함수는 주어진 배열 또는 벡터에서 첫 번째로 발견되는 짝수를 반환하거나, 짝수가 없으면 None을 반환한다.

댓글 없음:

댓글 쓰기

관련 포스팅