반응형

간단하게 정리해본 Rust 강좌입니다. 

다음 문서를 기반으로 작성했습니다.



Tour of Rust

https://tourofrust.com/00_ko.html 



2022. 9. 11  최초작성

2023. 3. 28   최종 작성




Rust 개발 환경 만드는 방법은 아래 포스트를 참고하세요.

https://webnautes.tistory.com/2110



소유권

데이터 타입을 인스턴스화 하여 변수에 할당(binding)하면 메모리 리소스가 생성됩니다.  여기서 변수를 리소스의 소유자(owner)라고 합니다.  

 

#[derive(Debug)]  // 구조체를 출력하기 위해 필요합니다.


// 구조체를 정의합니다.
struct Foo {  
    x: i32,
}

fn main() {

    // 구조체를 인스턴스화 하고 변수에 할당하여 메모리 리소스를 생성합니다.
    let foo = Foo { x: 42 };

    // 변수 foo가 소유자입니다.
    println!("{:?}", foo)
}

 




범위 기반 리소스 관리

Rust는 범위(scope)가 끝나는 곳에서 리소스를 소멸하고 할당 해제합니다.



다음 코드처럼 블럭 내에서 변수 y에 값을 할당하면 블럭 내부에서는 변수 y에 접근할 수 있습니다.

 

fn main() {

    { // 블럭 시작
        let y = 10;

        println!("{}", y); // 변수 y에 접근 가능합니다.
    } // 블럭 끝
}

 

실행 결과 10이 출력됩니다. 



하지만 변수 y가 블럭을 벗어나는 순간 리소스가 할당 해제되어 존재하지 않게 되기 때문에 블럭 밖에서 변수 y에 접근하려고 하면 에러가 발생합니다. 

 

fn main() {

    { // 블럭 시작
        let y = 10;

        println!("{}", y); // 변수 y에 접근 가능합니다.
    } // 블럭 끝

    println!("{}", y); // 블럭이 끝나는 순간 변수 y가 할당 해제되었기 때문에 변수 y에 접근할 수 없습니다.
}

 




할당 해제는 계층적으로 진행

 

구조체가 할당 해제 될때 자신이 먼저 할당 해제되고 이후에 자신의 자식들이 할당해제됩니다. 

 

struct Bar {
    x: i32,
}

struct Foo {
    bar: Bar,
}


impl Drop for Bar {  // 구조체 Bar를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Bar!");
    }
}


impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}


fn main() {


    // 구조체 Foo의 자식으로 Bar 구조체를 선언합니다.
    let foo = Foo { bar: Bar { x: 42 } };

    println!("{}", foo.bar.x);
}



부모 구조체 Foo가 먼저 소멸되고 자식 구조체 Bar가 나중에 소멸되는 것을 볼 수 있습니다.

 




소유권 이전

소유자가 함수의 인자로 전달되면, 소유권은 그 함수의 파라미터로 이동됩니다.

소유권이 이동 된후에는 함수에 전달했던 변수는 더 이상 사용할 수 없습니다.

 

struct Foo {
    x: i32,
}

impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}


fn do_something(f: Foo) {
    println!("{}", f.x);

    println!("do_something end");


    // f가 여기서 drop 됩니다

}

fn main() {
   
    let foo = Foo { x: 42 };

    // 변수 foo의 소유권이 do_something 함수로 이동합니다.
    do_something(foo);
    

    // foo는 더 이상 사용할 수 없습니다
    println!("main end");
}



do_something 함수가 종료되면서 Foo 구조체가 할당해제 된것을 볼 수 있습니다. 

그렇기 때문에 main에서 변수 foo를 사용하면 안됩니다.

 



do_something 함수 종료후 main에서 foo를 사용하려고 하면 다음과 같은 에러가 발생합니다.

이미 foo는 할당 해제된 상태이기 때문입니다. 

 

struct Foo {
    x: i32,
}

impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}


fn do_something(f: Foo) {
    println!("{}", f.x);

    println!("do_something end");

    // f가 여기서 drop 됩니다
}

fn main() {
   
    let foo = Foo { x: 42 };

    // foo의 소유권이 do_something 함수로 이동합니다.
    do_something(foo);

    // foo는 더 이상 사용할 수 없는데 사용하려고 시도해봅니다.
    println!("{}", foo.x);

    println!("main end");
}






소유권 리턴하기

소유권을 함수에서 자신을 호출한 곳으로 리턴할 수 있습니다.

 

struct Foo {
    x: i32,
}

impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}


fn do_something(f: Foo) -> Foo {
    println!("{}", f.x);

    println!("do_something end");


    // Foo의 소유권이 리턴됩니다.
    return f;
}

fn main() {
   
    let foo = Foo { x: 42 };

    // foo의 소유권이 do_something 함수로 넘겨주었다가 bar로 다시 돌려받습니다.
    let bar = do_something(foo);


    // 소유권을 넘겨받았기 때문에 bar를 사용할 수 있습니다.
    println!("{}", bar.x);

    println!("main end");
}

 

do_something에서 소유권을 넘겨받았기 때문에 main 함수에서도 bar를 사용하여 Foo 구조체 객체에 접근할 수 있음을 볼 수 있습니다.

 



do_something에 소유권을 넘겨주었던 foo를 사용하려고 하면 에러가 발생합니다.

 

struct Foo {
    x: i32,
}

impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}


fn do_something(f: Foo) -> Foo {
    println!("{}", f.x);

    println!("do_something end");


    // Foo의 소유권이 리턴됩니다.
    return f;
}

fn main() {
   
    let foo = Foo { x: 42 };

    // foo의 소유권이 do_something 함수로 넘겨주었다가 bar로 다시 돌려받습니다.
    let bar = do_something(foo);


    // 소유권을 넘겨주었기 때문에 에러가 발생합니다.
    println!("{}", foo.x);

    println!("main end");
}

 

소유권을 do_something에 넘겨준 변수 foo를 접근하는 부분에서 다음과 같은 에러가 발생합니다.

error[E0382]: borrow of moved value: `foo`



참조로 소유권 대여하기

& 연산자를 통해 참조로 리소스에 대한 소유권을 대여할 수 있습니다.

 

struct Foo {
    x: i32,
}

impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}



// &를 사용하면 참조로 소유권을 대여받습니다.
fn do_something(f: &Foo){
    println!("{}", f.x);

    println!("do_something end");
}

fn main() {
   
    let foo = Foo { x: 42 };

    // 참조(&)를 사용하여 foo의 소유권을 do_something 함수에 대여합니다.
    do_something(&foo);


    // 소유권을 넘겨준게 아니라 대여해준것이기 때문에 main 함수에서도 변수 foo를 사용할 수 있습니다.
    println!("{}", foo.x);

    println!("main end");
}

 

Foo에 대한 소유권을 do_something 함수에 대여한 것이기 때문에 do_something을 벗어나는 순간 Foo 구조체에 대한 소멸자가 호출되지 않습니다.  do_something 함수 종료후에도 main에서 Foo를 접근할 수 있습니다. 왜냐하면 Foo 구조체에 대한 소유권을 do_something에 넘겨준게 아니라 참조를 사용하여 대여한 것이기 때문입니다.

 

do_something에서 값을 바꾼다면 main함수에 반영될까?



Foo는 main 함수 종료후 할당 해제됩니다.

 




참조로 변경 가능한 소유권 대여하기

 

&mut 연산자를 통해 리소스에 대한 mutable한 접근 권한도 대여할 수 있습니다.

 

do_something 함수에 변수를 파라미터로 전달하여 do_something에서 변수 변경하면 변경된 값이 main에서도 사용할 수 있는 예제입니다.

참고 https://www.snoyman.com/blog/2020/05/no-mutable-parameters-in-rust/  

 

struct Foo {
    x: i32,
}

impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}



// &mut 를 사용했기 때문에 변수 값을 변경할 수 있도록 소유권을 대여받습니다.
fn do_something(f: &mut Foo){
    

    // 값을 변경할 수 있습니다.
    f.x = 11;

    println!("in do_something : {}", f.x);

    println!("do_something end");
}

fn main() {
   
    let mut foo = Foo { x: 42 };

    println!("in main : {}", foo.x);


    // &mut를 사용하여 변수값을 변경가능하도록 소유권을 대여합니다.
    do_something(&mut foo);


    // do_something 함수 내에서 변경된 값이 반영된 것을 볼 수 있습니다.
    println!("in main : {}", foo.x);

    println!("main end");
}
 

실행결과

 

in main : 42

in do_something : 11

do_something end

in main : 11

main end

Dropping Foo!




역참조

 

&mut를 사용하여 참조한 후,  * 연산자로 소유자의 값을 변경할 수 있습니다.

 

fn main() {
    let mut foo = 42;


    let f = &mut foo; // 값을 변경할 수 있는 참조로 빌려줌
    // println!("{}", foo); // 에러 발생, cannot borrow `foo` as immutable because it is also borrowed as mutable, 이 부분이 이해안갑니다. 변경가능한 참조로 빌려줘서 인거 같습니다. 
 
    let bar = *f; // 소유자의 값의 복사본을 가져옵니다.

    *f = 13; // 역참조로 참조의 소유자의 값을 변경합니다.


    println!("foo {}", foo); //위 코드 이후엔 에러가 안납니다.
   
    println!("bar {}", bar);
}




대여한 데이터 전달하기

 

Rust의 참조 규칙은 다음과 같이 요약될 수 있습니다:

 

  • Rust는 단 하나의 mutable한 참조 또는 여러개의 non-mutable 참조만 허용하며, 둘 다는 안됨.
  • 참조는 그 소유자보다 더 오래 살 수 없음.
  • 이는 보통 함수로 참조를 넘겨줄 때에는 문제가 되지 않습니다.



첫 번째 참조 규칙은 데이터 경합을 방지합니다. 데이터 경합은 데이터를 읽는 행위가 동시에 데이터를 쓰는 이의 존재로 인해 동기화가 어긋날 가능성이 있을 때 일어납니다. 이는 멀티쓰레드 프로그래밍에서 종종 발생합니다.

두 번째 참조 규칙은 존재하지 않는 데이터를 참조하는 것을 방지합니다 



struct Foo {
    x: i32,
}

impl Drop for Foo {  // 구조체 Foo를 위한 소멸자입니다.
    fn drop(&mut self) {
        println!("Dropping Foo!");
    }
}

fn do_something(f: &mut Foo) {
    f.x += 1;

    println!("do_something : {}", f.x);

    println!("do_something end");
    // mutable 참조 f에 대한 소멸자는 호출되지 않지만 리소스가 소멸됩니다.
}

fn main() {
    let mut foo = Foo { x: 42 };

    println!("main : {}", foo.x);

    do_something(&mut foo);



    //  mutable 참조가 do_something 함수 내에서 자원해제되므로,
    // 하나 더 생성할 수 있습니다.   
    do_something(&mut foo);

   
    // foo.x를 출력하면 두번 호출된 do_something에서 더한 값이 반영되어 있습니다.
    println!("main: {}", foo.x);


    println!("main end")

    // foo는 여기서 자원 해제되므로 소멸자가 호출됩니다.
}






참조의 참조

 

참조를 참조에 사용할 수 있습니다.

 

struct Foo {
    x: i32,
}

fn do_something(a: &Foo) -> &i32 {

    // mut를 사용한 참조가 아니었기 때문에 a.x의 값을 변경할 수 없습니다.

    return &a.x;
}

fn main() {
    let mut foo = Foo { x: 42 };

    // foo.x의 소유권을 변경가능한 참조로 빌립니다.
    let x = &mut foo.x;
    println!("x = {}", x);

    //역참조로 값을 변경합니다.
    *x = 13;

    // foo의 소유권을 참조로 빌려주게되면 x는 더이상 사용할 수 없게 됩니다.
    let y = do_something(&foo);

    // 아래 주석을 해제하면 x는 사용할 수 없다는 에러가 납니다.
    //println!("x = {}", x);
   
    // 소유권을 y를 사용하여 참조로 돌려받았기 때문에 접근이 가능합니다.
    println!("y = {}", y);

    // foo도 접근 가능합니다.
    println!("foo.x = {}", foo.x);
}




생명주기

 

Rust 코드에서 컴파일러는 모든 변수의 lifetime을 관리하여 참조가 소유자보다 더 오래 존재하지 못하도록 합니다.

 

fn main() {
    let a;

    {
        let b = 10;

        // 변수 b의 라이프타임이 블럭 내에서 끝나기 때문에 에러가 발생합니다.
        // borrowed value does not live long enough
        a = &b;
    }

    println!("{}", a);
   
}

 



명시적인 생명주기

함수에서는  매개변수와 리턴 값이 서로 같은 lifetime을 공유하는지 식별할 수 있도록 심볼로 표시하여 명시적으로 생명주기를 지정할 수 있습니다. lifetime 지정자는  '로 시작합니다. (예: 'a, 'b, 'c)

 

lifetime 지정자는 컴파일러가 스스로 함수 매개변수들의 lifetime을 판별하지 못하는 경우, 이를 명시적으로 지정할 수 있게 도와줍니다.




다음 코드는 missing lifetime specifier 에러가 발생합니다. 

 

struct Foo {
    x: i32,
}

fn do_something(foo_a : &Foo, foo_b: &Foo) -> (&Foo, &Foo) {
    println!("{}", foo_a.x);
    println!("{}", foo_b.x);


    // foo_a와 foo_b는 참조로 전달받았기 때문에 함수 내에서 소멸되는 변수라서 참조로 리턴하면 안됩니다.
    return (foo_a, foo_b);
}

fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 12 };

    let (foo_a, foo_b) = do_something(&foo_a, &foo_b);

    println!("foo_a.x {}, foo_b.x {}", foo_a.x, foo_b.x);
}
 

 



 lifetime 에러를 해결하기 위해 생명주기를 명시하여 파라미터와 리턴값의 라이프 타임이 똑같다는 것을 Rust에 알려주면 해결됩니다. 

 

struct Foo {
    x: i32,
}

// foo_a, foo_b와 리턴 값은 동일한 lifetime을 공유함을 명시하기 위해 변수앞에 '를 붙여줍니다.
fn do_something<'a, 'b>(foo_a : &'a Foo, foo_b: &'b Foo) -> (&'a Foo, &'b Foo) {
    println!("{}", foo_a.x);
    println!("{}", foo_b.x);

    return (foo_a, foo_b);
}


fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 12 };

    let (foo_a, foo_b) = do_something(&foo_a, &foo_b);

    println!("foo_a.x {}, foo_b.x {}", foo_a.x, foo_b.x);
}

 

실행결과

 

42

12

foo_a.x 42, foo_b.x 12



정적인 생명주기

 

static 변수는 컴파일 타임에 생성되어 프로그램의 시작부터 끝까지 존재하는 메모리 리소스입니다. 이들은 명시적으로 데이터 타입을 지정해 주어야 합니다.

 

static lifetime은 프로그램이 끝날 때까지 유지되는 메모리 리소스로 'static이라는 특별한 lifetime 지정자를 갖습니다.

만약 static lifetime을 가진 리소스가 참조를 포함하는 경우, 그들도 모두 'static이어야 합니다



static PI: f64 = 3.1415;

fn main() {

    // static 변수에 값을 대입합니다.
    static mut SECRET: &'static str = "swordfish";

    // 문자열 값은 'static lifetime을 갖습니다
    let msg: &'static str = "Hello World!";

    let p: &'static f64 = &PI;
    println!("{} {}", msg, p);

    unsafe // unsafe 키워드를 사용하여 static 변수의 값을 변경할 수 있습니다. 정수로 변경하면 에러가 발생합니다.
    {
        // SECRET의 값을 변경할 수 있습니다.  변경된 값도 'static이기 때문입니다
        SECRET = "abracadabra";
        println!("{}", SECRET);
    }
}

 

실행결과

 

Hello World! 3.1415

abracadabra




구조체의 데이터 타입의 생명주기

 

구조체에 포함된 데이터 타입의 구성원들도 lifetime을 지정할 수 있습니다.

참조 데이터 타입이 참조가 가리키는 소유자보다 오래 살아남지 못하도록 합니다.

 

아래 코드는 missing lifetime specifier 에러가 발생합니다.

 

struct Foo{
    i: &i32
}

fn main() {

    let x = 42;
    let foo = Foo {
        i: &x
    };
   
    println!("{}",foo.i);
}

 




lifetime을 지정하여 에러를 해결할 수 있습니다. 

struct Foo<'a> {
    i:&'a i32
}

fn main() {

    let x = 42;
    let foo = Foo {
        i: &x
    };
   
    println!("{}",foo.i);
}




간단하게 정리해본 Rust 강좌 1 : 변수, 상수, 함수, 튜플, 배열, 데이터타입, println

https://webnautes.tistory.com/2194

 

간단하게 정리해본 Rust 강좌 2 : if-else, loop, while, for, match, struct, method

https://webnautes.tistory.com/2195

 

간단하게 정리해본 Rust 강좌 3 : 열거형, 제네릭 데이터 타입, Option, Result, unwrap, vector

https://webnautes.tistory.com/2196

 

간단하게 정리해본 Rust 강좌 4 : 소유권, 참조, 역참조, 생명주기

https://webnautes.tistory.com/2197

 

간단하게 정리해본 Rust 강좌 5 : 문자열, utf-8

https://webnautes.tistory.com/2198

 

간단하게 정리해본 Rust 강좌 6 : 모듈

https://webnautes.tistory.com/2199

 

간단하게 정리해본 Rust 강좌 7 : &self, &mut self, trait, 동적 디스패치, 정적 디스패치, Generic 메서드, Box

https://webnautes.tistory.com/2200

 

간단하게 정리해본 Rust 강좌 8 : 참조자, 댕글링 참조, 원시 포인터

https://webnautes.tistory.com/2203

 

간단하게 정리해본 Rust 강좌 9 : Box, 재귀적 데이터타입, Deref 트레잇

https://webnautes.tistory.com/2202



반응형

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts