🦀 Rust
[Rust 입문] 7. 기본 컬렉션 타입
date
Mar 23, 2023
slug
rust-collection
author
status
Public
tags
Rust
독학
summary
Rust의 기본 컬렉션 타입 (벡터, 스트링)들에 대한 내용입니다.
type
Post
thumbnail
category
🦀 Rust
updatedAt
Mar 23, 2023 11:20 AM
C++의 STL에 포함된 데이터 컬렉션 타입처럼 rust도 언어에서 제공하는 자료구조가 있다.
참고로 컬렉션은 동적으로 값들을 저장하므로 힙에 저장된다.
벡터
값을 메모리에 차례대로 저장하는 배열과 유사한 자료구조이다. 보통 벡터 매크로를 이용해서 생성하거나 벡터의 new 함수를 호출해서 벡터를 생성한다.
C++의 벡터와 유사하게 제네릭을 이용해서 저장되는 데이터의 타입을 정한다.
let mut vec1: Vec<i32> = Vec::new(); let vec2 = vec![1, 2, 3]; vec1.push(123); vec1.push(456);
벡터를 mutable하게 선언하지 않으면 값을 추가할 수 없다.
벡터는 정의된 스코프를 벗어나면 drop된다. (삭제된다.)
벡터 요소값 읽기
fn main() { let mut vec1: Vec<i32> = Vec::new(); let vec2 = vec![1, 2, 3]; vec1.push(123); vec1.push(456); // 대괄호 메소드를 이용한 벡터 아이템 접근 (panic 발생가능) let v1 = vec1[1]; let v2 = vec2[2]; println!("v1 : {}, v2 : {}", v1, v2); // get 메소드를 이용한 벡터 아이템 접근 let v3 = vec1.get(1); let v4 = vec2.get(2); if let Some(v) = v3 { println!("v3 : {}", v); } if let Some(v) = v4 { println!("v4 : {}", v); } }
벡터 값 순회
원소 값을 변경하지 않을 때 벡터의 값을 순회할 때엔 아래와 같이 한다. 밑의 for문의 i는 참조자 타입이다.
fn main() { let vec = vec![1, 2, 3]; for i in &vec { println!("i : {}", i); } }
그래서 if문으로 정수와 비교할 때 타입 때문에 오류가 날 수 있다. 이런 경우
* 연산자를 이용해 참조자가 가리키는 원본 값에 접근하도록 하여 비교하거나 정수를 참조자 타입으로 변경해야 한다.fn main() { let v = vec![1, 2, 3, 4, 5]; for i in &v { if *i > 2 { // 또는 i > &2 println!("{}", i); } } }
참조자와 벡터
벡터의 요소는 메모리의 특정 값을 가리킨다. 그래서 이 값을 가변 참조자로 빌려와서 참조자에
* 연산자를 이용해 값을 변경하면 벡터의 원소 값도 변하게 된다.fn main() { let mut vec = vec![1, 2, 3]; let borrow = &mut vec[1]; println!("borrow : {}", borrow); *borrow = 10; for i in &vec { println!("i : {}", i); } }
단 기존 벡터의 원소에 대한 참조자를 만들어 놓고 벡터에 대대 push와 같은 작업을 하면 오류가 발생한다.
fn main() { let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); for i in &v { println!("i : {}", i); } println!("first : {}", first); }
논리적인 이유는 벡터에서 원소를 추가할 때 새로운 힙 영역의 메모리를 할당하여 기존 벡터값을 복사할 수도 있는데 이런 경우 참조자가 가리키는 값이 의미가 없어진다.
문법적인 이유는 값에 대해 불변 참조자가 존재할 때 가변 참조자를 만들 수 없다. 라는 이유 때문인데
first 라는 벡터에 대한 불변 참조자를 만들었는데 push 메소드 내에서 벡터에 대한 가변 참조자를 만들기 때문에 위 코드는 동작하지 않는다.벡터 값을 순회하면서 값을 변경
벡터 값을 순회하면서 값을 변경하려면 가변 참조자로 반복해야 한다. 아래는 벡터 내의 3 이하의 수를 3으로 바꿔준다.
fn main() { let mut v = vec![1, 2, 3, 4, 5]; for i in &mut v { if *i <= 2 { *i = 3 } } for i in &v { println!("i : {}", i); } }
스트링
rust의 스트링은 크게 두가지가 있다.
&str: 스트링 슬라이스라고 부르며 바이너리에 저장된 스트링 리터럴의 참조자를 의미한다.
String: rust 표준 라이브러리에 구현된 타입이며 가변적이다.
여기서 말하는 스트링은 표준 라이브러리의 스트링을 의미한다.
스트링 생성하기
스트링을 생성하는 방법은 크게 두가지가 있다. to_string을 이용하거나 String의 from 함수를 사용한다.
fn main() { let itos = 123.to_string(); let stos = "스트링 리터럴".to_string(); let ftos = 3.14.to_string(); let btos = false.to_string(); println!("{}, {}, {}, {}", itos, stos, ftos, btos); }
to_string 메소드는 Display라는 트레잇이 구현된 타입엔 모두 사용이 가능하다. (트레잇은 추후에 다룸)
fn main() { let stos = String::from("스트링 리터럴"); println!("{}", stos); }
어느 것을 사용하던지 상관 없다.
스트링 변경하기
스트링의 데이터는 힙에 할당되어 있다. 그래서 유동적으로 변경할 수 있다.
fn main() { let mut stos = String::from("hello"); stos.push('~'); stos.push_str("!!!!!!!!!"); println!("{}", stos); }
+ 연산자를 사용하는 방법도 있다. 이 연산자는 스트링 간에 사용할 때에는 add 라는 함수(메소드)를 호출하는 것이 된다.이 메소드는 왼쪽 값의 소유권을 받아 새로운 값으로 리턴하므로 기존에 대입한 왼쪽 값은 사용이 불가능해진다.
오른쪽 값에는 참조자가 들어가므로 오른쪽 값은 상관 없다.
fn main() { let left = String::from("hello"); let right = String::from(" world"); let sum = left + &right; println!("{}, {}", right, sum); }
이런 점 때문에 문자열에 대해서
+ 연산자를 여러개 사용하는 점이 불편해진다. 가장 왼쪽의 문자열을 제외하고 모두 참조자 꼴로 넣어야 한다.fn main() { let a = String::from("The"); let b = String::from("Worst"); let c = String::from("Band"); let d = String::from("In"); let e = String::from("The"); let f = String::from("World"); let space = String::from(" "); let sum = a + &space + &b + &space + &c + &space + &d + &space + &e + &space + &f; println!("{}", sum); }
그래서 소유권도 뺏지 않고, 원하는 포맷대로 문자열을 만들어 주는
format 매크로를 사용하는 것도 좋다.fn main() { let a = String::from("The"); let b = String::from("Worst"); let c = String::from("Band"); let d = String::from("In"); let e = String::from("The"); let f = String::from("World"); let sum = format!("{} {} {} {} {} {}", a, b, c, d, e, f); println!("{}", sum); }
문자열의 인덱싱
rust의 문자를 저장하는 방식때문에 문자열은 인덱싱을 지원하지 않는다. 하나의 “문자" 가 몇 바이트인지 문자마다 다르기 때문이다.
만약 문자가 아스키 코드뿐만이 아니라 한글이나 한자, 이모지로 이루어졌을 때 다른 언어들처럼 문자를 1바이트나 2바이트 단위로 접근한다면 아무런 의미 없는 값이 올 수 있기 때문이다.
그래서 문자열의 chars 메소드나 bytes 메소드로 본인이 의도한 대로 스트링에 접근해야 한다.
fn main() { let a = String::from("english, 한국어 혼용 👍"); for b in a.chars() { // 실제 의미가 있는 문자대로 추출함 println!("{}", b); } for b in a.bytes() { // 바이트 단위로 추출함 println!("{}", b); } }