본문 바로가기
개발/러스트 (Rust)

Rust 기본, to_string() vs to_owned()

by 레조 2024. 7. 20.

https://rustacean.net/

 

Rust 기본 : to_string() vs to_owned()

 

.to_string()

 - .to_string() 메서드는 ToString 트레이트의 일부입니다.


 - 이 메서드는 보통 Display 트레이트를 구현한 모든 타입에 대해 사용할 수 있습니다. 
   Display 트레이트는 주로 사람이 읽을 수 있는 형태로 객체를 문자열로 변환할 때 사용됩니다.

 

ToString 트레이트는 fmt::Display 트레이트를 구현하는 모든 타입에 자동으로 구현됩니다.

fmt::Display 트레이트를 구현하면 표준 라이브러리에서 아래 코드가 작동하게 된다.

T가 fmt:: Display 트레이트면 ToString 트레이트도 동작한다.

trait ToString {
    fn to_string(&self) -> String;
}

impl<T: fmt::Display> ToString for T {
    fn to_string(&self) -> String {
        use std::fmt::Write;
        let mut buf = String::new();
        let _ = write!(buf, "{}", self);  // `Display` 트레이트를 사용하여 `self`를 `buf`에 쓴다.
        buf
    }
}


 - 문자열 리터럴에 대해 사용할 경우, 내부적으로는 fmt::Write 트레이트를 사용하여 문자열의 내용을 새로운 String에 복사합니다.


.to_owned()

 - .to_owned() 메서드는 ToOwned 트레이트의 일부이며, 이 트레이트는 일반적으로 불변 타입으로부터 가변 타입의 소유 버전을 생성하는 데 사용됩니다.

 - &str에서 String으로의 변환에 자주 사용되며, 이 경우 &str의 데이터를 새로운 String으로 복사합니다.

 - .to_owned()는 .to_string()보다 좀 더 일반적인 용도로 사용되며, 특히 불변 참조(&str)에서 소유된 데이터(String)의 복사본을 만들어야 할 때 유용합니다.

 

 

성능

 - 두 메서드 사이에 큰 차이는 없습니다.

   둘 다 문자열 데이터를 새로운 String으로 복사합니다.

 


 

Display 트레이트 구현

 - 주로 사용자에게 보여줄 수 있는 문자열을 생성할 때 구현합니다. ex) 로깅, 메시지 출력.

use std::fmt;

struct Person {
    name: String,
    age: u32,
}

// Display 트레이트를 구현하여 Person 객체를 어떻게 표시할지 정의
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} is {} years old", self.name, self.age)
    }
}

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
    };

    // Display 트레이트를 사용하여 Person 객체를 출력
    // Display 트레이트를 구현하면 "{}" 를 사용하여 커스텀 타입을 출력할 수 있다.
    println!("{}", person);
}

 

ToOwned 트레이트 구현

 - 주로 불변 참조(&str)에서 소유된 타입(String)으로의 변환에 최적화된 .clone()의 특수 케이스를 구현합니다.

#[derive(Debug)]
struct Data {
    value: i32,
}

// ToOwned 트레이트를 구현하여 &Data에서 Data로 복제를 지원
impl ToOwned for Data {
    type Owned = Data;

    fn to_owned(&self) -> Data {
        Data { value: self.value }
    }
}

fn main() {
    let data_ref = &Data { value: 10 };

    // ToOwned 트레이트를 사용하여 data_ref의 소유된 복사본을 생성
    let data_owned = data_ref.to_owned();

    // 커스텀 타입을 출력하기 위해 디버그 트레이트 "{:?}" 사용한다.
    // struct Data에 #[derive(Debug)] 사용.
    println!("Original: {:?}", data_ref);
    println!("Owned Copy: {:?}", data_owned);
}

 

구조체 Data에 #[derive(Debug)]를 사용하면, Rust 컴파일러는 Data 타입의 인스턴스를 {:?} 형식 지정자와 함께 println! 매크로 등에서 사용할 수 있도록 자동으로 Debug 구현을 생성합니다.

 

참고) ToOwned 트레이트 구현에 .clone() 사용

- #[derive(Clone)] 사용.

#[derive(Debug, Clone)]
struct CustomData {
    content: String,
}

// CustomData에 대해 ToOwned를 직접 구현
impl ToOwned for CustomData {
    type Owned = CustomData;
    fn to_owned(&self) -> CustomData {
        self.clone()  // 여기서 Clone 트레이트의 clone() 메서드를 호출
    }
}

fn main() {
    let data_ref = &CustomData {
        content: "Hello Rust".to_string(),
    };

    let data_owned = data_ref.to_owned();  // &CustomData에서 CustomData로 복제
    println!("{:?}", data_owned);  // 출력: CustomData { content: "Hello Rust" }
}

 


 

Rust 표준 라이브러리에는 Display 트레이트를 구현하는 여러 기본 제공 타입이 있다.

Display 트레이트는 주로 사람이 읽을 수 있는 형식으로 타입을 출력하기 위해 사용한다.

 

기본적인 수치형 타입

정수 타입 (i8, i16, i32, i64, i128, isize 등)

부호 없는 정수 타입 (u8, u16, u32, u64, u128, usize 등)

부동 소수점 타입 (f32, f64)

 

문자열과 문자 타입
char
String
&str (문자열 슬라이스)


복합 데이터 구조
Option<T>: 여기서 T도 Display를 구현해야 한다.
Result<T, E>: T와 E 모두 Display를 구현해야 한다.


기타 유용한 타입
Path와 PathBuf: 파일 시스템 경로를 다루는 타입.
std::net::IpAddr: IP 주소를 나타내는 타입.
std::net::Ipv4Addr와 std::net::Ipv6Addr: IPv4와 IPv6 주소 타입.
std::time::Duration: 시간 간격을 나타내는 타입.

 

이 외에도 많은 표준 라이브러리 타입들이 Display 트레이트를 구현하고 있어, 

.to_string() 메서드를 통해 간편하게 문자열로 변환할 수 있습니다.

 


 


 

어떻게 println!("{}", some_variable); 은 Display Trait fmt()를 호출하는가?

 

println!("{}", some_variable); 매크로는 다음과 같이 확장됩니다.

// println!("{}", some_variable);
{
    use std::io::{self, Write};
    let mut stdout = io::stdout();
    let _ = write!(stdout, "{}\n", format_args!("{}", some_variable));
}

 

1. write! 매크로의 호출

write! 매크로는 출력 스트림(stdout), 포맷 스트링("{}\n"), 그리고 포맷 인자(person)를 사용하여 호출됩니다.
write! 매크로는 내부적으로 std::fmt::write_fmt 함수를 호출하며, 출력 스트림과 fmt::Arguments 인스턴스를 전달합니다.

(format_args!를 사용하여 fmt::Arguments 생성)

 

use std::fmt;
use std::io::{self, Write};

// 사용자 정의 타입
struct Person {
    name: String,
    age: u32,
}

// Person 타입에 대한 Display 트레이트 구현
impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} is {} years old", self.name, self.age)
    }
}

fn main() {
    let person = Person {
        name: "Alice".to_string(),
        age: 30,
    };

    // println!("{}", person); 매크로 확장하면,
    //
    // stdout을 사용하여 출력
    let mut stdout = io::stdout();
    // format_args!를 사용하여 fmt::Arguments를 생성하고 write!를 통해 출력
    let _ = write!(stdout, "{}\n", format_args!("{}", person));
}

 

2. std::fmt::write_fmt 함수의 실행

write_fmt 함수는 두 가지 주요 매개변수를 받습니다. 출력 스트림과 fmt::Arguments.
fmt::Arguments는 format_args! 매크로를 통해 생성되며, 

이 매크로는 포맷 스트링과 포맷 대상(person)을 받아 처리합니다.


write_fmt는 실제로 std::fmt::write 함수를 호출하여 포맷팅 작업을 진행합니다.

// io::Write trait

#[stable(feature = "rust1", since = "1.0.0")]
pub trait Write {

    // ...
    
    #[stable(feature = "rust1", since = "1.0.0")]
    fn write_fmt(&mut self, args: Arguments<'_>) -> Result {
        // We use a specialization for `Sized` types to avoid an indirection
        // through `&mut self`
        trait SpecWriteFmt {
            fn spec_write_fmt(self, args: Arguments<'_>) -> Result;
        }

        impl<W: Write + ?Sized> SpecWriteFmt for &mut W {
            #[inline]
            default fn spec_write_fmt(mut self, args: Arguments<'_>) -> Result {
                if let Some(s) = args.as_statically_known_str() {
                    // 단순 문자열일 경우 최적화를 위해 그대로 출력
                    self.write_str(s)
                } else {
                    // 아닐경우 std::fmt::write 호출
                    write(&mut self, args)
                }
            }
        }

        impl<W: Write> SpecWriteFmt for &mut W {
            #[inline]
            fn spec_write_fmt(self, args: Arguments<'_>) -> Result {
                if let Some(s) = args.as_statically_known_str() {
                    self.write_str(s)
                } else {
                    // 아닐경우 std::fmt::write 호출
                    write(self, args)
                }
            }
        }

        self.spec_write_fmt(args)
    }
    
    // ...
    
}

 

3. std::fmt::write 함수의 실행

std::fmt::write 함수는 포맷팅을 수행하는 핵심 함수로, fmt::Arguments를 해석하고 관련된 포맷 스펙에 따라 적절한 트레이트의 fmt 메서드를 호출합니다.
이 함수는 각 포맷 스펙 ({})에 해당하는 데이터(person)에 대해 Display 트레이트의 fmt 메서드를 호출합니다.

#[stable(feature = "rust1", since = "1.0.0")]
pub fn write(output: &mut dyn Write, args: Arguments<'_>) -> Result {
    let mut formatter = Formatter::new(output);
    let mut idx = 0;

    match args.fmt {
        None => {
            // We can use default formatting parameters for all arguments.
            for (i, arg) in args.args.iter().enumerate() {
                // SAFETY: args.args and args.pieces come from the same Arguments,
                // which guarantees the indexes are always within bounds.
                let piece = unsafe { args.pieces.get_unchecked(i) };
                if !piece.is_empty() {
                    formatter.buf.write_str(*piece)?;
                }

                // SAFETY: There are no formatting parameters and hence no
                // count arguments.
                unsafe {
                    arg.fmt(&mut formatter)?;
                }
                idx += 1;
            }
        }
        Some(fmt) => {
            // Every spec has a corresponding argument that is preceded by
            // a string piece.
            for (i, arg) in fmt.iter().enumerate() {
                // SAFETY: fmt and args.pieces come from the same Arguments,
                // which guarantees the indexes are always within bounds.
                let piece = unsafe { args.pieces.get_unchecked(i) };
                if !piece.is_empty() {
                    formatter.buf.write_str(*piece)?;
                }
                // SAFETY: arg and args.args come from the same Arguments,
                // which guarantees the indexes are always within bounds.
                unsafe { run(&mut formatter, arg, args.args) }?;
                idx += 1;
            }
        }
    }

    // There can be only one trailing string piece left.
    if let Some(piece) = args.pieces.get(idx) {
        formatter.buf.write_str(*piece)?;
    }

    Ok(())
}

 

4. Display 트레이트의 fmt 메서드 호출

unsafe {
    arg.fmt(&mut formatter)?;
}

 

Person 타입에 대한 Display 트레이트의 fmt 메서드는 write! 매크로를 사용하여 실제 포맷팅 문자열을 생성합니다. 

예제에서는 Person의 이름과 나이를 문자열로 포맷팅하고 있습니다.
fmt 메서드는 Formatter 객체(f)를 사용하여 최종 문자열을 구성하고, 이 문자열은 출력 스트림에 기록됩니다.

 

동적 디스패치 (Dynamic Dispatch)
arg.fmt(&mut formatter)?;에서 arg의 실제 타입은 컴파일 시점에는 알려져 있지 않습니다. 

Rust에서 이러한 경우, 트레이트 객체를 통해 메서드를 호출할 때 "동적 디스패치"가 발생합니다. 

동적 디스패치는 프로그램 실행 중에 특정 메서드 호출을 해결하는 방식으로, 

특정 타입의 메서드를 호출할 것인지를 런타임에 결정합니다.

 

이렇게 println!("{}", some_variable); 은 Display Trait fmt()를 호출한다.

 


 

io::Write trait의 write_fmt()

https://github.com/rust-lang/rust/blob/453ceafce32ef8108c604bca5e165ab41d3d6d8c/library/core/src/fmt/mod.rs#L193

 

io::Write trait의 write()

https://github.com/rust-lang/rust/blob/453ceafce32ef8108c604bca5e165ab41d3d6d8c/library/core/src/fmt/mod.rs#L1139

 

커스텀 포멧팅을 만들고 싶다면 더 파라!
https://stackoverflow.com/questions/67852867/rust-calling-fmt-function-directly

 

Rust calling fmt function directly

I'm trying to implement different Display formats depending on arguments. Something like print_json() and print_pretty(). I could of course implement it as a function returning string print_json(&

stackoverflow.com

 

더 파면,

https://github.com/kpreid/all-is-cubes/blob/ce15ab52dc84c59efaa2f0b95b042cf65c201016/all-is-cubes/src/util.rs