반응형

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



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

 

Tour of Rust

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



2022. 10. 1  최초작성

2023. 4. 23  최종작성



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

https://webnautes.tistory.com/2110



객체 지향 프로그래밍(Object-Oriented Programming, OOP)

캡슐화 (encapsulation) - 객체라 불리는 단일 타입의 개념적 단위에 데이터와 메서드를 묶어줍니다. 

추상화 (abstraction) - 데이터와 메서드를 숨겨 객체의 상세 구현 사항을 알기 어렵게 합니다. 

다형성 (polymorphism) - 하나의 객체가 여러 가지 타입을 가질 수 있음을 의미합니다.

상속 (inheritance) - 다른 객체로부터 데이터와 메서드를 상속 받습니다. 



Rust는 객체 지향 프로그래밍 언어가 아닙니다. 데이터와 메서드의 상속이 불가능하기 때문입니다.

구조체는 부모 구조체로부터 데이터와 메서드를 상속받을 수 없습니다.



메소드 캡슐화

Rust는 메소드와 연결된 구조체라는 객체 개념을 지원합니다.

모든 메소드의 첫번째 파라미터는 메소드 호출과 연관된 인스턴스에 대한 참조여야 합니다 

인스턴스에 대한 참조에는 다음 두가지가 있습니다.

  • &self - 인스턴스에 대한 변경 불가능한 참조.
  • &mut self - 인스턴스에 대한 변경 가능한 참조.

 

인스턴스에 대한 변경 불가능한 참조 &self를 사용하는 예제 코드입니다. 

// 구조체 SeaCreature를 정의합니다. 
struct SeaCreature {
    // String 타입의 필드 name을 포함하고 있습니다.
    name: String,
}

// impl 키워드를 사용하여 구조체 SeaCreature에 연결되는 메서드 get_name을 정의합니다. 
impl SeaCreature {


    // &self - 인스턴스에 대한 변경 불가능한 참조입니다.
    fn get_name(&self) -> &str {


        // name을 리턴합니다. 변경 불가능한 참조 &self를 사용하기 때문에 get_name 메서드에서 self.name의 값을 변경할 수 없습니다. 
        &self.name
    }
}

fn main() {


    // SeaCreture 구조체 객체를 생성합니다.
    let creature = SeaCreature {


        // 구조체의 필드 name의 값을 초기화 합니다.
        name: String::from("rust"),
    };

    // get_name 메소드를 사용하여 구조체의 필드 name의 값을 출력합니다.
    println!("{}", creature.get_name());

}



실행 결과

rust



추상화

Rust는 객체의 내부 동작을 숨길 수 있습니다.

기본적으로, 멤버 변수와 메서드는 그들이 속한 모듈에서만 접근 가능하지만 pub 키워드를 구조체의 멤버 변수와 메소드 앞에 붙여주면 모듈 밖에서도 접근이 가능해집니다. 

 

스크린샷처럼 config.rs와 main.rs 파일을 작성합니다.



main.rs

mod config;

fn main() {


    // config 모듈에 있는 SeaCreature 구조체의 인스턴스를 생성합니다.
    let mut creature = config::SeaCreature{ name: "default".to_string()};
    

    // SeaCreature 구조체의 set_name 메서드를 호출하여 name 필드의 값을 설정합니다.
    creature.set_name("whale");


    // SeaCreature 구조체의 get_name 메서드를 호출하여 name 필드의 값을 가져와 출력합니다.
    println!("{}", creature.get_name());
}



config.rs

// 구조체 SeaCreature를 정의합니다. 모듈 외부에서 접근할 수 있도록 pub 키워드를 추가해야 합니다. 
pub struct SeaCreature {
    pub name: String,
}


// 구조체 SeaCreature의 필드에 접근하는 두 개의 메서드를 정의합니다.
// 각 메서드에 pub 키워드를 추가해줘야 외부에서 해당 메서드를 사용할 수 있습니다.
impl SeaCreature {


    // SeaCreature 구조체의 name 필드 값을 리턴하는 메서드입니다.
    pub fn get_name(&self) -> &str {
        &self.name
    }


    // SeaCreature 구조체의 name 필드 값을 변경하는 메서드입니다. 
    // &mut self 참조를 사용하기 때문에 self.name의 값을 변경할 수 있습니다.
    pub fn set_name(&mut self, name: &str){

        
        self.name = name.to_string();
    }
}

 

실행하면 config 모듈에 정의된 SeaCreature 구조체의 set_name 메서드를 사용하여 name 필드에 값을 대입하고 get_name 메서드를 사용하여 name 필드의 값을 가져와 화면에 출력해줍니다. 

 

실행 결과

whale



다형성

Rust는 trait으로 다형성을 지원합니다. 

 

trait를 사용하여 make_name 메소드 원형을 정의한 후, 

trait NameMaker {

    // make_name 메서드 원형을 정의합니다.
    fn make_name(&self);
}



impl 키워드를 사용하여 trait에 정의한 메소드를 실제로 구현합니다.   실제 데이터 타입입이 무엇인지 알지 못하더라도 그 trait 데이터 타입을 통해 간접적으로 구조체와 상호작용할 수 있습니다. 

impl NameMaker for SeaCreature {
    // SeaCreature 구조체를 위한 trait 메서드를 실제로 구현합니다.
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}

impl NameMaker for Number {

    // Number 구조체를 위한 trait 메서드를 실제로 구현합니다.
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}



아래 코드에는 &str 타입을 리턴하는 SeaCreature 구조체의 get_name 메서드와 i32 타입을 리턴하는 Number 구조체의 get_name 메서드가 있습니다. 

trait를 사용하여 NameMaker 메서드를 정의한 후,  SeaCreature 구조체와 Number 구조체에 대한 trait 메서드를 각각 구현합니다. 

// String 타입의 name 필드를 포함하는 SeaCreature 구조체를 정의합니다. 
struct SeaCreature {
    name: String
}


// SeaCreature 구조체의 name 필드를 리턴하는 메서드를 정의합니다. 리턴 타입은 &str입니다.
impl SeaCreature {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}


// i32 타입의 name 필드를 포함하는 Number 구조체를 정의합니다.
struct Number {
    name: i32
}


// Number 구조체의 name 필드를 리턴하는 메서드를 정의합니다. 리턴 타입은 i32입니다.
impl Number {
    pub fn get_name(&self) -> i32 {
        self.name
    }
}


// trait를 사용하여 make_name 메서드의 원형을 정의합니다.
trait NameMaker {
    fn make_name(&self);
}


// SeaCreature 구조체를 위한 make_name 메서드를 실제로 구현합니다. 
impl NameMaker for SeaCreature {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}


// Number 구조체를 위한 make_name 메서드를 실제로 구현합니다.
impl NameMaker for Number {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}


fn main() {


    // SeaCreature 구조체 객체를 생성하며 name 필드의 값을 초기화합니다.
    let creature = SeaCreature {
        name: String::from("Ferris")
    };


    // make_name 메서드를 호출합니다.
    creature.make_name();



    // Number 구조체 객체를 생성하며 name 필드의 값을 초기화합니다.
    let number = Number {
        name: 123
    };


    // make_name 메서드를 호출합니다.
    number.make_name();
}



실행 결과

Ferris

123



Trait에 구현된 메소드

trait에 메소드를 구현해 넣을 수 있습니다.

메서드가 구조체 내부의 멤버 변수에 직접 접근할 수는 없지만, trait 구현체들 사이에서 메서드를 공유할 때 유용하게 쓰입니다.

 

아래 코드에서 make_many_name 메서드를 공유해서 사용하고 있습니다. 

 

// SeaCreature 구조체를 정의합니다.
struct SeaCreature {
    name: String
}


// SeaCreature 구조체를 위한 get_name 메서드를 정의합니다.
impl SeaCreature {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}


// Number 구조체를 정의합니다.
struct Number {
    name: i32
}


// Number 구조체를 위한 get_name 메서드를 정의합니다.
impl Number {
    pub fn get_name(&self) -> i32 {
        self.name
    }
}


// trait를 정의합니다.
trait NameMaker {
   
    // 구조체 별로 구현이 존재하는 메서드입니다.
    fn make_name(&self);


    // trait에만 정의된 메서드입니다.
    fn make_many_name(&self){
        self.make_name();
        self.make_name();
        self.make_name();
    }

}


// SeaCreature 구조체를 위한 trait 메서드인 make_name을 구현합니다.
impl NameMaker for SeaCreature {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}


// Number 구조체를 위한 trait 메서드인 make_name을 구현합니다.
impl NameMaker for Number {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}

fn main() {


    // SeaCreature 구조체 인스턴스를 생성합니다.
    let creature = SeaCreature {
        name: String::from("Ferris")
    };


    // make_many_name 메서드를 호출합니다.
    creature.make_many_name();



    // Number 구조체 인스턴스를 생성합니다.
    let number = Number {
        name: 123
    };


    // make_many_name 메서드를 호출합니다.
    number.make_many_name();
}



실행 결과

Ferris

Ferris

Ferris

123

123

123




Trait 상속

trait은 다른 trait의 메서드를 상속 받을 수 있습니다.

struct SeaCreature {
    pub name: String
}

impl SeaCreature {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

trait NoiseMaker {
    fn make_name(&self);
}

// trait NoiseMaker를 상속받았기 때문에 trait Noisemaker에 정의된 make_name 메서드를 호출하는 메서드를 정의할 수 있습니다.
trait LoudNoiseMaker: NoiseMaker {
    fn make_many_name(&self) {
        self.make_name();
        self.make_name();
        self.make_name();
    }
}

impl NoiseMaker for SeaCreature {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}

impl LoudNoiseMaker for SeaCreature {}

fn main() {


    // SeaCreature 구조체 인스턴스를 생성합니다.
    let creature = SeaCreature {
        name: String::from("Ferris")
    };


    // make_many_name 메서드를 호출합니다. 
    creature.make_many_name();
}



실행 결과

Ferris

Ferris

Ferris



동적 vs 정적 디스패치

메소드는 다음의 두 가지 방식으로 실행됩니다:

 

정적 디스패치 (static dispatch) - 인스턴스의 타입을 알고 있는 경우로 어떤 함수를 호출해야 하는지 알고 있습니다.

동적 디스패치 (dynamic dispatch) - 인스턴스의 타입을 모르는 경우로  어떤 함수를 호출할지 런타임에 결정됩니다. 

 

trait 자료형인 &dyn MyTrait은 동적 디스패치를 통해 객체의 인스턴스들을 간접적으로 작동시킬 수 있게 해줍니다.

동적 디스패치를 사용할 경우, Rust에서는 trait 자료형 앞에 dyn을 붙일 것을 권고합니다.

 

동적 디스패치는 함수 호출을 위한 포인터 추적을 하기 때문에 조금 느릴 수 있습니다.

 

struct SeaCreature {
    pub name: String
}

impl SeaCreature {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

struct Number {
    name: i32
}

impl Number {
    pub fn get_name(&self) -> i32 {
        self.name
    }
}

trait NoiseMaker {
    fn make_name(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}

impl NoiseMaker for Number {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}


// 정적 디스패치 - 인스턴스의 타입을 아는 경우로 타입별로 메서드를 정의해야 합니다. 

// SeaCreature 구조체를 위한 make_name 메서드를 호출합니다.
fn static_make_name1(maker: &SeaCreature) {
    maker.make_name();
}


// Number 구조체를 위한 make_name 메서드를 호출합니다.
fn static_make_name2(maker: &Number) {
    maker.make_name();
}

// 동적 디스패치 - 인스턴스의 타입을 모르는 경우로 하나의 메서드만 정의하면 됩니다.
// dyn 키워드를 추가해줘야 합니다. 

// 실행될때 SeaCreature 구조체 또는 Number 구조체를 위한 make_name 메서드를 호출합니다.
fn dynamic_make_name(maker: &dyn NoiseMaker) {
    maker.make_name();
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris")
    };

    let number = Number{
        name: 100
    };


    // 정적 디스패치 메서드를 호출합니다.
    static_make_name1(&creature);
    static_make_name2(&number);


    // 동적 디스패치 메서드를 호출합니다.
    dynamic_make_name(&creature);
    dynamic_make_name(&number);
}



Generic 메서드

Rust의 generic은 trait과 함께 작동합니다. 매개변수 타입 T를 정의할 때 해당 인자가 어떤 trait을 구현해야 하는지 나열함으로써 인자에 어떤 자료형을 쓸 수 있는지 제한할 수 있습니다.

 

아래 예제에서 T 타입은 Foo trait로 정의해야 합니다:

 

fn my_function<T>(foo: T)

where

    T:Foo

{

    ...

}



generic을 이용하면 컴파일 시 자료형과 크기를 알 수 있는 정적 자료형의 함수가 만들어지며, 따라서 정적 디스패치와 함께 크기가 정해진 값으로 저장할 수 있게 됩니다.

struct SeaCreature {
    pub name: String
}

impl SeaCreature {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

struct Number {
    name: i32
}

impl Number {
    pub fn get_name(&self) -> i32 {
        self.name
    }
}

trait NoiseMaker {
    fn make_name(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}

impl NoiseMaker for Number {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}


// 제네릭 메서드 generic_make_name을 정의합니다.
// T를 위한 타입은 trait NoiseMaker를 사용합니다.
fn generic_make_name<T>(creature: &T)
where
    T: NoiseMaker,
{
    // 컴파일 타임에 실제 자료형을 알게 됩니다
    creature.make_name();
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris")
    };

    let number = Number{
        name: 100
    };


    // 제네릭 메서드를 호출합니다.
    generic_make_name(&creature);
    generic_make_name(&number);
}




실행결과

Ferris

100




Generic 메서드 줄여쓰기

 

trait과 함께 사용한 generic은 다음과 같이 짧게 줄여쓸 수 있습니다:

 

fn my_function(foo: impl Foo) {

    ...

}



원래 코드는 다음과 같습니다.

 

fn my_function<T>(foo: T)

where

    T:Foo

{

    ...

}



Generic 함수를 줄인 코드입니다.

 

struct SeaCreature {
    pub name: String
}

impl SeaCreature {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

struct Number {
    name: i32
}

impl Number {
    pub fn get_name(&self) -> i32 {
        self.name
    }
}

trait NoiseMaker {
    fn make_name(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}

impl NoiseMaker for Number {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}


// 제네릭 메서드를 간단히 줄인 버전입니다.
fn generic_make_name (creature:impl NoiseMaker)
{
    // 컴파일 타임에 실제 자료형을 알게 됩니다
    creature.make_name();
}

fn main() {
    let creature = SeaCreature {
        name: String::from("Ferris")
    };

    let number = Number{
        name: 100
    };


    // 제네릭 메서드를 호출합니다.
    generic_make_name(creature);
    generic_make_name(number);
}



Box

Box는 stack에 있는 데이터를 heap으로 옮겨주는 데이터 구조입니다.

Box는 smart pointer로도 알려진 구조체이며 heap에 있는 데이터를 가리키는 포인터를 들고 있습니다.

 

Box는 크기가 알려져 있는 구조체이므로 (왜냐하면 포인터만 들고 있으므로) field의 크기를 알아야 하는 구조체에 뭔가의 참조를 저장할 때 종종 사용됩니다.

 

Box::new(Foo { ... })

 

struct SeaCreature {
    pub name: String,
}

impl SeaCreature {
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

trait NoiseMaker {
    fn make_name(&self);
}

impl NoiseMaker for SeaCreature {
    fn make_name(&self) {
        println!("{}", &self.get_name());
    }
}

struct Ocean {
    animals: Vec<Box<dyn NoiseMaker>>,
}

fn main() {
    let ferris = SeaCreature {
        name: String::from("Ferris")
    };

    let sarah = SeaCreature {
        name: String::from("Sarah")
    };

    let ocean = Ocean {

        // 벡터에 ferris 객체와 sarah 객체를 저장합니다.
        animals: vec![Box::new(ferris), Box::new(sarah)],
    };


    // 벡터에 저장된 객체의 make_name 메서드를 호출합니다.
    for a in ocean.animals.iter() {
        a.make_name();
    }
}



실행결과

Ferris

Sarah



간단하게 정리해본 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