Rust 문법

전지현·2024년 11월 12일

Rust

목록 보기
16/17
post-thumbnail

rustling 하며 이것저것 같이 공부한 내용 그때마다 기록.
도움 많이 받음



함수의 리턴

Rust에서는 함수의 마지막 표현식에서 return 키워드를 생략할 수 있습니다. 함수의 마지막에 위치한 표현식이 곧 함수의 반환값으로 처리됩니다. 이를 암묵적 반환이라고 합니다.

Rust의 특징 중 하나로, 함수의 마지막 줄에 세미콜론(;)이 없으면 해당 표현식의 값이 함수의 반환값이 됩니다. return 키워드를 쓰는 대신, 마지막 줄에 단순히 값을 적어주는 방식으로 반환할 수 있습니다.

예시 : 암묵적 반환

fn add(a: i32, b: i32) -> i32 {
    a + b  // 마지막 줄의 값이 반환됨
}

위 함수에서는 a + b 표현식이 마지막 줄에 세미콜론 없이 위치해 있으므로 a + b의 값이 반환됩니다.

주의 사항

Rust에서 암묵적 반환을 사용할 때, 마지막 줄에 세미콜론을 붙이지 않아야 합니다. 만약 세미콜론을 붙이면 해당 줄이 단순한 문(statement)으로 처리되기 때문에 함수는 () 타입을 반환하게 됩니다. 예를 들어, 아래 코드는 컴파일 오류가 발생합니다.

fn add(a: i32, b: i32) -> i32 {
    a + b;  // 세미콜론이 있어서 반환값이 아님
}

위 코드에서 a + b;는 값이 아닌 문으로 처리되기 때문에 ()를 반환하게 되어 반환 타입이 맞지 않다는 오류가 발생합니다.

따라서 Rust에서는 함수의 반환값을 지정할 때, 마지막 줄의 값에 세미콜론을 생략하는 방식으로 암묵적 반환을 사용하는 것이 일반적입니다.

반환값이 없는 경우

반환하는 값이 없는 경우에는 -> 뒤에 반환 타입을 명시할 필요는 없습니다. Rust에서는 반환 타입이 없는 함수는 기본적으로 () 타입(유닛 타입)을 반환합니다. 반환 타입을 명시하지 않으면 Rust는 함수가 암묵적으로 ()을 반환한다고 간주합니다.

다만, 반환 타입이 없다는 것을 명시적으로 표시하려면 -> ()를 사용할 수도 있습니다. 예를 들어, 아래 두 함수는 동일하게 동작합니다:

fn no_return_value() {
    println!("This function does not return a value.");
}

// 반환 타입을 명시적으로 표시하는 방법
fn no_return_value_explicit() -> () {
    println!("This function also does not return a value.");
}

Rust에서는 반환 타입이 없을 경우 생략하는 것이 일반적이지만, 명시적으로 -> ()를 추가하는 것이 가독성 측면에서 좋음

세미콜론이 있고 없고 잘 구분할 줄 알아야 함
세미콜론은 표현식이 반환값을 갖지 않도록 만듦.

  • 단순한 명령문을 나타내는 경우 세미콜론이 필요.
  • 함수나 식에서 반환값을 기대할 때 세미콜론을 생략. 세미콜론이 없는 표현식을 값을 반환하는 표현식으로 취급

유닛 테스트

Rust에서 유닛 테스트는 주로 개별 함수나 모듈이 올바르게 동작하는지 확인하기 위해 작성됩니다. 유닛 테스트는 해당 코드가 있는 파일 내에 위치하는 것이 일반적이며, 주로 #[cfg(test)] 애트리뷰트와 mod tests 블록을 사용하여 작성합니다.

유닛 테스트의 특징

  1. 독립적: 유닛 테스트는 개별 함수나 모듈을 독립적으로 검증합니다.
  2. 모듈 내 테스트: #[cfg(test)]mod tests를 사용하여 테스트 코드를 포함하며, 일반적으로 파일의 맨 아래에 위치합니다.
  3. 비공개: #[cfg(test)]로 컴파일러에게 테스트 모듈임을 알리며, 실제 코드에 포함되지 않고 테스트 시에만 컴파일됩니다.
  4. 직접적인 검증: assert_eq!, assert!, assert_ne! 등 매크로를 활용하여 특정 입력에 대해 함수가 예상대로 동작하는지 확인합니다.

예시

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn foo_for_fizz() {
        // This means that calling `foo_if_fizz` with the argument "fizz" should return "foo".
        assert_eq!(foo_if_fizz("fizz"), "foo");
    }

    #[test]
    fn bar_for_fuzz() {
        assert_eq!(foo_if_fizz("fuzz"), "bar");
    }

    #[test]
    fn default_to_baz() {
        assert_eq!(foo_if_fizz("literally anything"), "baz");
    }
}

단계별로 테스트 실행 방법

  1. Cargo 프로젝트 생성:

    cargo new my_project
    cd my_project
  2. 코드 작성: src/main.rs 파일에 코드를 작성하고, #[cfg(test)] 모듈 아래에 테스트 함수들을 추가합니다.

  3. 테스트 실행: 다음 명령어로 프로젝트 내의 모든 테스트를 실행합니다.

    cargo test
  • cargo test테스트 함수만 실행하며, 프로그램의 메인 함수(main)는 실행하지 않습니다.
  • 테스트 결과와 함께 각 테스트의 성공 또는 실패 여부가 출력됩니다.

통합 테스트와의 차이점

Rust에서 통합 테스트는 주로 tests 폴더 안에서 전체 모듈의 상호 작용을 검증하는 데 사용됩니다. 반면, 유닛 테스트는 특정 모듈이나 함수의 동작을 독립적으로 검증하는 데 중점을 둡니다.

따라서 이 코드의 테스트는 함수 단위로 개별 동작을 검증하므로 유닛 테스트에 해당합니다.


유닛 테스트 실행 결과 예시

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn foo_for_fizz() {
        // This means that calling `foo_if_fizz` with the argument "fizz" should return "foo".
        assert_eq!(foo_if_fizz("fizz"), "foo");
    }

    #[test]
    fn bar_for_fuzz() {
        assert_eq!(foo_if_fizz("fuzz"), "bar");
    }

    #[test]
    fn default_to_baz() {
        assert_eq!(foo_if_fizz("literally anything"), "baz");
    }
}


// 터미널 output
PS C:\Users\sure\Desktop\RUST KPI\rustlings\exercises\03_if> cargo test --bin if2
   Compiling exercises v0.0.0 (C:\Users\sure\Desktop\RUST KPI\rustlings)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.55s
     Running unittests exercises/03_if/if2.rs (C:\Users\sure\Desktop\RUST KPI\rustlings\target\debug\deps\if2-bfc90d2acd9c33f5.exe)

running 3 tests
test tests::bar_for_fuzz ... ok
test tests::default_to_baz ... ok
test tests::foo_for_fizz ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

슬라이싱

부분 배열을 얻기 위해 Rust의 슬라이싱 문법 사용.
[start..end] 형식을 이용하여 배열의 특정 범위 참조. start는 포함되지만 end는 포함되지 않는다.

슬라이스는 항상 참조로 처리되어야 한다 슬라이스의 타입 자체가 참조로 정의되어 있음.

슬라이스는 배열의 일부분을 '복사'하는 것이 아니라, 원본 배열을 그대로 참조해 필요한 범위만 접근하는 거임.

fn main() {
    // You can optionally experiment here.
}

#[cfg(test)]
mod tests {
    #[test]
    fn slice_out_of_array() {
        let a = [1, 2, 3, 4, 5];

        // 배열 `a`에서 [2, 3, 4] 부분 배열을 얻기 위해 슬라이스를 생성합니다.
        let nice_slice = &a[1..4];

        assert_eq!([2, 3, 4], nice_slice);
    }
}
  • let nice_slice = &a[1..4]; 부분에서 a[1..4]a 배열의 두 번째 요소(인덱스 1)부터 네 번째 요소(인덱스 3)까지 포함하는 슬라이스를 만듭니다. &를 붙여 슬라이스 참조로 저장합니다.
  • assert_eq!([2, 3, 4], nice_slice);nice_slice[2, 3, 4]와 동일한지 확인합니다.

튜플 요소 접근 시
'튜플명.인덱스숫자' 로 접근 가능

mod tests {
    #[test]
    fn indexing_tuple() {
        let numbers = (1, 2, 3);

        // 튜플의 두 번째 요소에 접근합니다.
        let second = numbers.1;

        assert_eq!(second, 2, "This is not the 2nd number in the tuple!");
    }
}

벡터

새 벡터 만들기

fn array_and_vec() -> ([i32; 4], Vec<i32>) {
    let a = [10, 20, 30, 40]; // Array

    // TODO: Create a vector called `v` which contains the exact same elements as in the array `a`.
    // Use the vector macro.
    // let v = ???;

    // 이런식으로 벡터 타입을 명시할 수 있지만 러스트에서는 값의 타입을 유추해주므로
    // 굳이 타입 명시를 할 필요가 없다. 대신 편의를 위해 제공되는 vec! 매크로를 사용하면 된다!
    let v: Vec<i32> = Vec::new();
    
    // 매크로 사용하여 1,2,3을 저장하고 있는 Vec<i32> 생성
    let v = vec![1, 2, 3]; 

    // a와 같은 원소를 가진 벡터 v 생성
    let v = vec![10, 20, 30, 40];

    (a, v)
}

let v: Vec<i32> = Vec::new();

위처럼 벡터 타입을 명시해서 선언할 수 있지만,
편의상 벡터 매크로 vec! 를 사용하면 된다.

let v = vec![1, 2, 3];

벡터 갱신하기
push 메소드 사용

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

벡터도 스코프 밖으로 벗어났을 때 해제된다.

{
    let v = vec![1, 2, 3, 4];

    // v를 가지고 뭔가 합니다

} // <- v가 스코프 밖으로 벗어났고, 여기서 해제됩니다

이게 뭔지 공부해보자

    input.iter().map(|element| element + 1).collect()
  1. input.iter()

input은 [i32] 타입의 슬라이스.
iter() 메서드를 사용하면 슬라이스의 요소들을 순서대로 반환하는 불변 참조 이터레이터를 생성.
iter()의 역할은 슬라이스에 포함된 각 요소에 반복 작업을 수행할 수 있게 해주는 이터레이터를 만드는 것.
예를 들어, input이 [1, 2, 3]이라면, input.iter()는 각각 1, 2, 3을 가리키는 불변 참조 &i32 이터레이터를 반환.

  1. .map(|element| element + 1)

map()은 이터레이터의 각 요소에 대해 주어진 함수를 적용하는 메서드.
=> map()은 이터레이터의 각 요소에 대해 특정 변환을 수행하고 싶을 때 사용.
반복문을 사용하는 대신, 각 요소를 변환하여 새로운 값을 포함하는 이터레이터를 생성.

.map()은 이터레이터의 모든 요소에 대해 일관된 변환을 적용하여 루프를 사용하지 않고도 원하는 연산을 간결하게 수행할 수 있게 한다.
예를 들어 input.iter().map(|element| element + 1)은 모든 요소에 + 1을 적용하여 input이 [1, 2, 3]일 때, map()은 2, 3, 4로 변환된 이터레이터를 생성

  1. .collect()

collect()는 이터레이터의 모든 요소를 특정 타입의 컬렉션으로 모아줍니다. Rust에서는 Vec, HashMap, HashSet 등 다양한 컬렉션 타입으로 변환이 가능합니다.

왜 .collect()를 쓰는가?

.map()과 같은 연산으로 변환된 값들은 이터레이터로 존재합니다. 이터레이터는 그 자체로 값을 저장하지 않고, 요소를 순회할 수 있는 기능만을 제공합니다.

.collect()를 사용해 이 변환된 이터레이터의 요소들을 실제 컬렉션으로 저장할 수 있습니다. 여기서는 Vec로 수집합니다.

정리
iter()는 슬라이스의 이터레이터를 생성합니다.
map()은 이터레이터의 각 요소에 + 1 연산을 적용하여 새로운 이터레이터를 만듭니다.
collect()는 최종적으로 변환된 이터레이터를 Vec와 같은 컬렉션으로 모아 반환합니다.

이와 같은 이터레이터 체인은 for 루프 없이도 간단히 데이터 변환을 수행할 수 있는 편리한 방법입니다.

for 루프와 비교 예제
.map()과 .collect()를 사용한 코드와 일반 for 루프를 사용한 코드 비교

// Using .map() and .collect()
let input = vec![1, 2, 3];
let output: Vec<i32> = input.iter().map(|element| element + 1).collect();
println!("{:?}", output);  // Output: [2, 3, 4]

// Using a for loop
let input = vec![1, 2, 3];
let mut output = Vec::new();
for element in input.iter() {
    output.push(element + 1);
}
println!("{:?}", output);  // Output: [2, 3, 4]

참조

*는 포인터나 참조의 실제 값을 접근할 때 사용하는 역참조(dereference) 연산자.

Rust에서 &는 참조를 뜻하며, 예를 들어 &vec0은 vec0의 참조(메모리 주소)를 가리킨다. 참조로 값을 얻을 때는 직접 접근하는 대신 *를 사용해 참조가 가리키는 실제 값을 가져온다.


구조체

구조체 정의

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

구조체 인스턴스 생성

let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

여러가지 구조체 선언

struct ColorRegularStruct {  // 이름이 있는 필드로 구성된 구조체
  red: u8,
  green: u8,
  blue: u8,
}

struct ColorTupleStruct(u8, u8, u8);  // 필드 이름이 없는 튜플 구조체.

struct UnitStruct;  // 필드가 없는 구조체. 데이터 없이 이름만 존재.

템플릿

  • 구조체는 데이터를 저장하고 표현하기 위한 틀
  • 템플릿은 구조체를 특정 기본값으로 초기화하여 재사용 가능하게 만든 것.

템플릿 활용 예시

fn create_order_template() -> Order {
    Order {
        name: String::from("Default Name"),
        year: 2024,
        made_by_phone: false,
        made_by_mobile: false,
        made_by_email: true,
        item_number: 0,
        count: 0,
    }
}

  
  let new_order = Order {
    name: String::from("Alice"),
    count: 2,
    ..create_order_template()
};

구조체는 틀만 있는 거고, 구조체에 값까지 할당해 놓은 게 템플릿!!
구조체에 값을 미리 채워서 초기화된 상태로 제공하는 데이터를 "템플릿"이라고 볼 수 있습니다. Rust에서는 구조체 자체는 데이터의 형식만 정의하지만, 템플릿은 특정 구조체의 값을 포함한 인스턴스를 미리 만들어 놓은 것입니다.

구조체

  • 데이터의 틀을 정의합니다.
  • 필드 이름과 데이터 타입을 지정하지만, 값은 포함하지 않습니다.
struct Order {
    name: String,
    year: u32,
    count: u32,
}

템플릿

  • 구조체의 인스턴스에 값을 미리 할당해 둔 것입니다.
  • 특정 작업에서 공통적으로 사용할 기본값을 제공하기 위해 생성됩니다.
fn create_order_template() -> Order {
    Order {
        name: String::from("Default Name"),
        year: 2024,
        count: 0,
    }
}

구조체 사용 시

구조체 선언

struct Order {
    name: String,
    year: u32,
    made_by_phone: bool,
    made_by_mobile: bool,
    made_by_email: bool,
    item_number: u32,
    count: u32,
}

구조체 사용 (인스턴스 생성)

 let order = Order {
    name: String::from("Alice"),
    year: 2024,
    made_by_phone: false,
    made_by_mobile: true,
    made_by_email: true,
    item_number: 42,
    count: 5,
};

템플릿 사용 시

템플릿 선언

// 이 함수는 Order 구조체의 기본값을 정의한 템플릿 역할을 합니다.
  fn create_order_template() -> Order {
    Order {
        name: String::from("Default Name"),
        year: 2024,
        made_by_phone: false,
        made_by_mobile: false,
        made_by_email: true,
        item_number: 0,
        count: 0,
    }
}

템플릿 사용

let order_template = create_order_template();
let custom_order = Order {
    name: String::from("Hacker in Rust"), // 템플릿에서 변경
    count: 1,                             // 템플릿에서 변경
    ..order_template                      // 나머지 필드는 템플릿 값을 복사
};

템플릿은 구조체의 초기값을 미리 정의해 둔 것입니다. 구조체와 템플릿의 차이는, 구조체는 데이터를 정의하는 틀이지만 템플릿은 특정 값을 미리 설정한 구체적인 인스턴스라는 점입니다.

  • 구조체: 데이터의 틀을 정의. 선언 시 구체적인 값은 포함하지 않음.
  • 템플릿: 특정 구조체의 초기값을 설정한 인스턴스. 주로 함수(create_order_template)를 통해 정의.

Rust에서 템플릿은 구조체를 기반으로 초기값이 미리 설정된 인스턴스를 의미하며, 이를 함수로 제공하는 것이 일반적.


템플릿 사용하여 코드 중복 감소

 let template = create_order_template();

let order1 = Order {
    name: String::from("Alice"),
    count: 10,
    ..template // 템플릿에서 나머지 값 복사
};

let order2 = Order {
    name: String::from("Bob"),
    count: 5,
    ..template
};



impl 이란

구조체 안에 함수를 선언할 때
C처럼 구조체 안에 함수를 작성하지 않고
impl 구조체명 으로 선언해서
구조체의 함수를 작성함.

=> impl은 구조체의 데이터를 다룰 메서드를 정의하기 위해 사용됩니다.
메서드를 통해 구조체의 데이터를 읽거나 수정할 수 있고, 연관 함수를 통해 구조체와 관련된 작업을 처리할 수도 있습니다.

=> Rust에서 구조체를 선언하고 사용할 메서드를 정의할 때는 impl 블록 사용.
이때 메서드(구현함수)의 첫 번째 매개변수로 항상 &self를 사용하여 해당 메서드가 호출된 구조체의 인스턴스에 대한 참조(레퍼런스)를 전달한다.


Rust에서 impl 안에는 두 가지 종류의 함수가 들어갈 수 있음
1. 메서드: 구조체 인스턴스(객체)에 연결된 함수.

호출 시 구조체 인스턴스의 데이터를 사용하거나 수정할 수 있습니다.
반드시 첫 번째 매개변수로 self를 받습니다.
=> self를 사용해서 구조체 안에 있는 데이터를 사용해 메서드 구현 (getter, setter)
예) fn area(&self), fn set_width(&mut self, width: f64)

2. 연관 함수: 구조체와 관련 있지만, 특정 인스턴스와 연결되지 않은 함수.

보통 새로운 구조체를 생성하거나 유틸리티 로직을 제공할 때 사용됩니다.
self를 받지 않으므로 구조체의 데이터를 다루지 않습니다.
예) fn new(width: f64, height: f64) -> Self

struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // 메서드: 구조체 인스턴스의 데이터를 사용하는 함수
    fn area(&self) -> f64 {
        self.width * self.height
    }

    // 메서드: 구조체 데이터를 수정하는 함수
    fn set_width(&mut self, width: f64) {
        self.width = width;
    }

    // 연관 함수: 특정 인스턴스 없이 호출 가능한 함수
    fn new(width: f64, height: f64) -> Self {
        Self { width, height }
    }
}

fn main() {
    // 연관 함수를 사용하여 새로운 Rectangle 생성
    let mut rect = Rectangle::new(10.0, 20.0);

    // 메서드를 사용하여 넓이 계산
    println!("Area: {}", rect.area()); // 출력: Area: 200

    // 메서드를 사용하여 너비 수정
    rect.set_width(15.0);
    println!("Updated Area: {}", rect.area()); // 출력: Updated Area: 300
}

enum

enum을 사용하면 관련된 여러 값을 하나의 타입으로 묶을 수 있다.

예시)

#[derive(Debug)]
enum Message {
    // TODO: Define a few types of messages as used below.
    Quit,
    Move {x: i32, y: i32},
    Echo(String),
    ChangeColor(u8, u8, u8),
    Resize(u32, u32),
}

fn main() {
    println!("{:?}", Message::Resize(1000, 4080));
    println!("{:?}", Message::Move {x: 10, y: 20});
    println!("{:?}", Message::Echo("Hello!" .into()));
    println!("{:?}", Message::ChangeColor(255, 0, 0));
    println!("{:?}", Message::Quit);
}

열거형 (enum): 여러 개의 서로 다른 변형을 가질 수 있는 데이터 타입.


#![allow(dead_code)]

#[derive(Debug)]
struct Point {
    x: u64,
    y: u64,
}

#[derive(Debug)]
enum Message {
    // TODO: Define the different variants used below.
    Resize {width: u8, height: u8},
    Move(Point),
    Echo(String),
    ChangeColor(u32, u32, u32),
    Quit,
}

impl Message {
    fn call(&self) {
        println!("{self:?}");
    }
}

fn main() {
    let messages = [
        Message::Resize {
            width: 10,
            height: 30,
        },
        Message::Move(Point { x: 10, y: 15 }),
        Message::Echo(String::from("hello world")),
        Message::ChangeColor(200, 255, 255),
        Message::Quit,
    ];

    for message in &messages {
        message.call();
    }
}

위 코드 실행결과

PS C:\Users\sure\Desktop\RUST KPI\rustlings\exercises\07_structs> cargo run --bin enums2
   Compiling exercises v0.0.0 (C:\Users\sure\Desktop\RUST KPI\rustlings)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
     Running `C:\Users\sure\Desktop\RUST KPI\rustlings\target\debug\enums2.exe`
Resize { width: 10, height: 30 }
Move(Point { x: 10, y: 15 })
Echo("hello world")
ChangeColor(200, 255, 255)
Quit

match

if-else와 같은 분기 처리 수행.
특히, 열거형(enum)의 각 변형(variant)을 처리할 때 매우 유용
값을 가져와서 가능한 모든 경우를 하나씩 확인.

예시)

let number = 3;

match number {
    1 => println!("One"),
    2 => println!("Two"),
    3 => println!("Three"),
    _ => println!("Other"), // 나머지 경우 처리
}

출력: Three

  • number의 값에 따라 분기 처리
  • _는 위에 정의된 패턴에 해당하지 않는 모든 경우를 처리 (else)

if-else와 비교
왜 if-else 대신 match를 사용하는가?
: 패턴 매칭의 강력함

match는 단순히 값 비교뿐 아니라 복잡한 구조를 매칭할 수 있다.
Message::Resize { width, height }처럼 구조체 형태의 데이터를 처리할 때도 매우 직관적

if-else로 작성 시

fn process(&mut self, message: Message) {
    if let Message::Resize { width, height } = message {
        self.resize(width as u64, height as u64);
    } else if let Message::Move(point) = message {
        self.move_position(point);
    } else if let Message::Echo(s) = message {
        self.echo(s);
    } else if let Message::ChangeColor(r, g, b) = message {
        self.change_color(r, g, b);
    } else if let Message::Quit = message {
        self.quit();
    }
}

match로 작성 시

fn process(&mut self, message: Message) {
    match message {
        Message::Resize { width, height } => {
            self.resize(width as u64, height as u64);
        }
        Message::Move(point) => {
            self.move_position(point);
        }
        Message::Echo(s) => {
            self.echo(s);
        }
        Message::ChangeColor(r, g, b) => {
            self.change_color(r, g, b);
        }
        Message::Quit => {
            self.quit();
        }
    }
}

분기 처리의 동작
1. Message::Resize { width, height }
Resize 메시지가 들어오면 resize 메서드를 호출하여 State의 크기를 변경합니다.

  1. Message::Move(point)
    Move 메시지가 들어오면 move_position 메서드를 호출하여 위치를 갱신합니다.

  2. Message::Echo(s)
    Echo 메시지가 들어오면 echo 메서드를 호출하여 메시지를 업데이트합니다.

  3. Message::ChangeColor(r, g, b)
    ChangeColor 메시지가 들어오면 change_color 메서드를 호출하여 색상을 변경합니다.

  4. Message::Quit
    Quit 메시지가 들어오면 quit 메서드를 호출하여 종료 상태를 true로 설정합니다.



문자열 String과 &str의 차이

String과 &str의 차이:

String: 힙에 저장된 소유권을 가진 가변 문자열 타입.
&str: 문자열 데이터에 대한 불변 참조로, 주로 정적 메모리(예: 문자열 리터럴)나 String의 슬라이스를 가리킴.


String은 힙(heap)에 저장되는 소유권을 가진 타입으로 문자열 데이터를 복사하거나, 수정하거나, 동적으로 확장할 수 있음. 반면, &str은 불변(immutable)이고, 복사나 소유권 개념과 관련이 없다. 타입이 &str인 이유 Rust는 문자열 리터럴을 기본적으로 효율적으로 관리하기 위해 &str 타입을 사용한다.

String으로 변환하려면?

.to_string()
String::from()

ex) "blue".to_string() / String::from("blue")


문자열 push_str, push

1. push_str

  • 기능:
    • 다른 문자열 슬라이스(&str)를 현재 문자열의 끝에 추가.
    • 예를 들어, 문자열 hello world!를 추가할 때 사용.
  • 사용법:
    let mut string = String::from("hello");
    string.push_str(" world!");
    println!("{}", string); // 출력: "hello world!"
  • 특징:
    • 한 번에 여러 문자를 추가할 수 있다.
    • 추가할 데이터는 &str 타입이어야 한다.

2. push

  • 기능:
    • 단일 문자(char)를 현재 문자열의 끝에 추가.
    • 예를 들어, 문자열 끝에 하나의 문자를 추가하고 싶을 때 사용.
  • 사용법:
    let mut string = String::from("hello");
    string.push('!');
    println!("{}", string); // 출력: "hello!"
  • 특징:
    • 한 번에 하나의 문자만 추가할 수 있다.
    • 추가할 데이터는 char 타입이어야 한다.

차이점 비교

특징push_strpush
입력 타입&strchar
추가 가능 데이터 크기여러 문자(문자열 슬라이스) 추가 가능단일 문자만 추가 가능
주 용도문장/단어 추가개별 문자 추가

  • push_str는 여러 문자를 한꺼번에 추가할 때 유용하고, push는 단일 문자를 추가할 때 유용하다.

0개의 댓글