Rust String, &str, Cow

코너C·2022년 7월 20일
0

[원본 글]

String을 인수로 받는 함수

fn print_me(msg: String) {
    println!("the message is {}", msg);
}

fn main() {
    let msg = "hello world";
    print_me(msg);
}
error[E0308]: mismatched types
print_me(msg);
         ^^^ expected struct `String`, found `&str
  • let msg = "hello world".to_string()으로 변환하면 문제 없으나, clone()을 수행하는 것과 유사
  • print_me()에서 인수를 String보다 &str 사용하는 것이 더 나은 이유
    • 함수 실행 이후에도 ownership을 원래 변수가 가질 수 있으므로 재활용이 용이함
    • 값의 복사가 발생하지 않아 효율적임
    • String 타입은 &str 타입으로 Deref를 통해 변환이 용이함

Deref Coercion 예제

fn print_me(msg: &str) { println!("msg = {}", msg); }

fn main() {
    let string = "hello world";
    print_me(string);

    let owned_string = "hello world".to_string(); 
    // or String::from_str("hello world")
    print_me(&owned_string);

    let counted_string = std::rc::Rc::new("hello world".to_string());
    print_me(&counted_string);

    let atomically_counted_string = std::sync::Arc::new("hello world".to_string());
    print_me(&atomically_counted_string);
}
  • print_me()&str을 인수로 받음
  • owned_string&String에서 자동으로 &str로 변환됨
  • RcArc에서도 마찬가지로 자동 변환됨
  • .to_string()으로 어질러지는 것을 피할 수 있음

struct

struct Person {
    name: &str,
}

fn main() {
    let _person = Person { name: "Herman" };
}
error[E0106]: missing lifetime specifier
name: &str,
      ^ expected named lifetime parameter
  • namePerson보다 오래 남아있지 않도록 방지
  • 이를 위해 lifetime specifier 명시 필요
struct Person {
    name: &'a str,
}

fn main() {
    let _person = Person { name: "Herman" };
}
error[E0261]: use of undeclared lifetime name `'a`
name: &'a str,
       ^^ undeclared lifetime
  • name보다 Person이 더 오래 남아있도록 Person에도 힌트 제공 필요
struct Person<'a> {
    name: &'a str,
}

fn main() {
    let _person = Person { name: "Herman" };
}
  • 이제 greet() 함수 구현
struct Person<'a> {
    name: &'a str,
}

impl Person {
    fn greet(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

fn main() {
    let person = Person { name: "Herman" };
    person.greet();
}
error[E0726]: implicit elided lifetime not allowed here
impl Person {
     ^^^^^^ expected lifetime parameter
  • impl에서도 lifetime 힌트 제공 필요
    • impl Person<'a> 해도 다음 에러 발생
    error[E0261]: use of undeclared lifetime name `'a`
    impl Person<'a> {
                ^^ undeclared lifetime
    • impl<'a> Person<'a> 로 해결
struct Person<'a> {
    name: &'a str,
}

impl<'a> Person<'a> {
    fn greet(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

fn main() {
    let person = Person { name: "Herman" };
    person.greet();
}

struct 내에서의 String&str

  • struct 내에서 String을 사용하는게 나을지, &str을 사용하는게 나을지?

    • 즉, struct 내에서 언제 reference를 사용하는게 나을지?
  • 변수의 ownership을 가질 필요가 없으면 reference 사용

    • struct 밖에서 변수를 사용할 필요가 있는가?

      struct Person {
          name: String,
      }
      
      impl Person {
          fn greet(&self) {
              println!("Hello, my name is {}", self.name);
          }
      }
      
      fn main() {
          let name = "Herman".to_string();
          let person = Person { name: name };
          person.greet();
          println!("My name is {}", name); // move error 발생
      }
    • 데이터 타입이 큰가? 불필요한 메모리 복사 발생 여부

'static에 대한 고려

  • 프로그램 전체 실행동안 유지할 필요가 있을까?
struct Person {
    name: &'static str,
}

impl Person {
    fn greet(&self) {
        println!("Hello, my name is {}", self.name);
    }
}

fn main() {
    let person = Person { name: "Herman" };
    person.greet();
}

struct에서 &str보다 String이 더 나은 경우 대처

  • 다음과 같이 struct 내에서 name 변수를 소유하는게 반드시 필요하다고 가정
    • 하지만 .to_string()으로 변환 필요
struct Person {
    name: String,
}

impl Person {
    fn new(name: &str) -> Person {
        Person {
            name: name.to_string(),
        }
    }
}

fn main() {
    let name = "Herman".to_string();
    let person = Person::new(name.as_ref());
}

into trait

  • &str을 자동으로 String으로 변환
  • &strString 모두 받을 수 있음
struct Person {
    name: String,
}

impl Person {
    fn new<S: Into<String>>(name: S) -> Person {
        Person { name: name.into() }
    }
}

// 위와 같은 구현
// impl Person {
//     fn new<S>(name: S) -> Person where S: Into<String> {
//         Person { name: name.into() }
//     }
// }

fn main() {
    let person = Person::new("Herman");
    let person = Person::new("Herman".to_string());
}

&str 또는 String의 함수 반환

  • (예) 주어진 문자열에서 공백들을 모두 제거하는 함수 예제
    • 문자열 버퍼를 위한 메모리를 할당하고,
    • input으로 주어진 각 문자들을 살펴보면서 공백이 아니면 버퍼에 추가
fn remove_spaces(input: &str) -> String {
   let mut buf = String::with_capacity(input.len());

   for c in input.chars() {
      if c != ' ' {
         buf.push(c);
      }
   }

   buf
}
  • 애초에 input 내에 공백이 전혀 없으면 메모리를 할당하지 말고 주어진 input을 그대로 반환하는게 효율적이지 않을까?
    • 그런데 input이 &str 타입이고 반환값은 String이어서 다른데?
    • 그럼 input도 String으로 하여 타입을 맞춰주면?
      fn remove_spaces(input: String) -> String { ... }
    • 이 함수를 호출하는 쪽에서 input의 ownership이 함수 안으로 move되므로 호출하는 쪽에서 input을 이후 사용하지 못함
    • input이 이미 &str이면 String으로 변환하는 과정을 한번 더 거쳐야 함

Clone-on-write

  • 주어진 input에 공백이 포함되어 있는지 먼저 검사하고,
    • 공백이 있는 경우에만 메모리 할당
    • 공백이 없으면 그대로 반환
  • Cow 타입의 lifetime은 &str과 같음
use std::borrow::Cow;

fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
    if input.contains(' ') {
        let mut buf = String::with_capacity(input.len());

        for c in input.chars() {
            if c != ' ' {
                buf.push(c);
            }
        }

        return Cow::Owned(buf);
    }

    return Cow::Borrowed(input);
}
  • 데이터를 borrowed할지 owned할지 나중에 결정
    • 변수의 새로운 메모리 할당(clone) 발생
      let s = remove_spaces("Herman");    // s는 Cow::Borrowed
      let len = s.len();                  // s를 변경시키지 않는 immutable 함수 호출
                                          // Deref를 통해 자동 변환됨
      let owned: String = s.into_owned(); // 새로운 문자열을 위해 메모리 할당
    • 변수의 메모리 할당 없음
      let s = remove_spaces("Herman Radtke"); // s는 Cow::Owned
      let len = s.len();                      // s를 변경시키지 않는 immutable 함수 호출
                                              // Deref를 통해 자동 변환됨
      let owned: String = s.into_owned();     // 이미 String이어서 새로운 메모리 할당 없음
  • 호출하는 쪽에서는 메모리가 새롭게 할당되는지 신경쓸 필요 없음

Into trait 활용

fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
    if input.contains(' ') {
        let mut buf = String::with_capacity(input.len());
        let v: Vec<char> = input.chars().collect();

        for c in v {
            if c != ' ' {
                buf.push(c);
            }
        }

        return buf.into();  // .into()
    }
    return input.into();    // .into()
}
fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> {
    if input.contains(' ') {
        input
            .chars()
            .filter(|&x| x != ' ')
            .collect::<std::string::String>()
            .into()
    } else {
        input.into()
    }
}

String::with_capacity()를 사용하는 이유

  • 위의 예제 함수 내에서 buf를 위해 String::new()를 사용하지 않고 String::with_capacity() 사용
  • String은 실제로는 Vec
  • String::new()하면 0바이트 벡터를 생성하고, 내용물이 추가될 때 벡터의 용량을 늘리기 위해 메모리 재할당
    • 매우 비효율적
  • 데이터를 벡터에 넣기 전에 시작할 용량을 정확하게 설정하는게 가장 바람직하므로, 가능한 with_capacity()를 사용하는 것을 권장
profile
Rustacean & Pythonian

0개의 댓글