IT/Rust 기초 완전 정복

Rust 벡터와 문자열 타입 사용법 (9)

지식 발전소 2024. 4. 24. 12:39
728x90
반응형

1

벡터(Vector)란 무엇인가?

이번에는 러스트에서 벡터(Vector) 자료구조를 사용하는 방법에 대해 알아보도록 하겠습니다. 왜 벡터를 사용해야 할까요? 벡터는 동적 배열(Dynamic Array)이라고 볼 수 있습니다. 즉, 실행 중에 크기를 변경할 수 있는 배열인 것입니다.

많은 프로그래밍 언어에서 배열의 크기는 컴파일 시점에 결정됩니다. 하지만 러스트의 벡터는 실행 중에 요소를 추가하거나 제거할 수 있어 매우 유연합니다. 이를 통해 메모리를 효율적으로 사용할 수 있습니다.

벡터를 만들려면 먼저 Vec<T> 형식을 이해해야 합니다. 여기서 T는 제네릭 타입 파라미터로, 벡터에 저장될 데이터 타입을 지정합니다. 예를 들어 Vec<i32>는 정수 벡터, Vec<String>은 문자열 벡터를 의미합니다.

let v: Vec<i32> = Vec::new(); // 빈 정수 벡터 생성

여기서 주목할 점은 러스트 벡터는 std::vec::Vec<T>라는 열거형 구조체라는 것입니다. 즉, Vec<T>는 구조체의 한 종류라고 볼 수 있습니다.

벡터 생성과 값 추가하기

이제 벡터를 생성하고 값을 추가하는 방법을 알아보겠습니다. Vec::new()로 빈 벡터를 만들 수 있습니다.

let mut v = Vec::new(); // 비어있는 벡터 v를 생성한다.

그리고 push 메서드로 벡터에 값을 추가할 수 있습니다.

v.push(1); // v는 이제 [1]이 된다.
v.push(2); // v는 이제 [1, 2]가 된다.
v.push(3); // v는 이제 [1, 2, 3]이 된다.

마찬가지로 pop 메서드로 마지막 요소를 제거할 수 있습니다.

let last = v.pop(); // last는 Some(3)이 되고, v는 [1, 2]가 된다.

pop 메서드가 반환하는 값은 Option<T> 열거형 타입입니다. 왜냐하면 벡터가 비어있을 경우 아무 값도 반환할 수 없기 때문입니다. Option은 안전하게 값의 존재 유무를 다루는 방법이죠.

벡터의 다양한 메서드

벡터는 요소를 다루는 다양한 메서드들을 제공합니다.

  • len() : 벡터 길이를 반환합니다.
  • get(index) : index에 있는 요소에 접근합니다. 인덱스가 범위를 벗어나면 None을 반환합니다.
  • insert(index, value) : 지정한 index에 value를 삽입합니다.
  • remove(index) : 지정한 index의 요소를 제거하고 그 값을 반환합니다.

예를 들어 다음과 같이 사용할 수 있습니다.

let mut v = vec![1, 2, 3, 4, 5];
println!("길이: {}", v.len()); // 출력: 길이: 5

v.insert(2, 99); // v = [1, 2, 99, 3, 4, 5]
println!("요소 2: {:?}", v.get(2)); // 출력: 요소 2: Some(99)

if let Some(x) = v.get(10) { // 벡터 범위를 벗어난 인덱스 접근
    println!("요소 10: {}", x); 
} else {
    println!("인덱스 10은 범위를 벗어났습니다.");
}

let removed = v.remove(2); // v = [1, 2, 3, 4, 5], removed = 99
println!("제거된 값: {}", removed); // 출력: 제거된 값: 99

벡터 순회

여러분은 어떻게 벡터의 모든 요소를 순회할 수 있을까요? 바로 반복문을 사용하면 됩니다.

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

// 인덱스와 값을 순회
for (index, value) in v.iter().enumerate() {
    println!("인덱스 {}: {}", index, value);
}

// 값만 순회 
for x in &v {
    println!("{}", x);
}

// 가변 참조자로 값 수정하기
for x in &mut v {
    *x += 10; // 각 요소에 10을 더한다.
}
println!("{:?}", v); // 출력: [11, 12, 13, 14, 15]

iter() 메서드로 벡터의 불변 참조자를 얻을 수 있고, iter_mut()로는 가변 참조자를 얻습니다. enumerate()는 인덱스와 값을 쌍으로 반환합니다.

벡터의 효율성

앞서 설명했듯이 벡터는 실행 시간에 크기를 조정할 수 있습니다. 하지만 벡터의 용량(capacity)을 꼭 인지해야 합니다.

예를 들어 vec![1, 2, 3]처럼 생성하면 용량이 3입니다. 이 상태에서 요소를 더 추가하면 벡터는 기존 메모리를 재사용할 수 없게 되므로, 새로운 더 큰 메모리 공간에 모든 요소를 복사해야 합니다.

이런 과정에서 성능 저하가 발생할 수 있습니다. 따라서 용량을 초과하게 될 것 같으면 with_capacity 메서드로 미리 충분한 용량을 확보하는 것이 좋습니다.

// 용량 10인 벡터를 최초부터 생성한다. 
let mut v = Vec::with_capacity(10); 

용량을 초과하는 상황이 자주 발생한다면 VecDeque와 같은 다른 자료구조를 사용하는 것도 고려해볼 수 있습니다.

문자열 타입 소개

이번에는 러스트의 문자열 타입에 대해 알아보겠습니다. 먼저 String과 &str의 차이부터 설명하는 것이 좋겠네요.

  • String은 소유 가능한(Owned) UTF-8 인코딩 문자열로서 힙에 할당됩니다.
  • &str은 문자열 슬라이스로 불리며, 소유권이 없는 문자열 데이터를 참조합니다.

러스트는 이렇게 문자열 타입을 두 종류로 나눕니다. 왜 그럴까요? 메모리 관리와 관련이 있습니다.

// 이 예제에서 my_string은 힙에 할당된 String 타입이고,
let my_string = String::from("hello");

// literals는 바이너리 내부에 저장된 불변 &str입니다.
let literals = "hello"; 

String은 힙에 할당되므로 크기가 런타임에 결정되고, 소유권 규칙에 따라 관리됩니다. 반면 &str은 정적 데이터로서 프로그램의 어디에선가 문자열 데이터를 참조하고 있습니다.

이러한 구분이 있음으로써 러스트는 메모리 안전성을 보장하고, 런타임 오버헤드를 최소화할 수 있습니다.

문자열 연산과 메서드들

다양한 문자열 연산과 메서드들도 살펴보겠습니다.

// 문자열 합치기
let s1 = String::from("hello, ");
let s2 = String::from("world");
let s3 = s1 + &s2; // "hello, world"

// push_str 메서드로 문자열 합치기 
let mut s = String::from("foo");
s.push_str("bar"); // s는 이제 "foobar"가 된다. 

// + 연산자로 char 값 합치기 
let mut s = String::from("x");
s.push('y'); // `char` 타입을 추가한다.
assert_eq!(s, "xy"); // s는 "xy"가 된다.

// to_string() 메서드로 다른 타입에서 String으로 변환하기
let i = 3;
let s = i.to_string(); // s는 "3"이 된다.

// 문자열 인덱싱(indexing)은 허용되지 않지만 슬라이스로 부분 문자열을 얻을 수 있다.
let s = String::from("hello");
let sub = &s[0..2]; // sub는 "he"가 된다.

문자열 타입은 내부적으로 Vec<u8>로 표현되므로 벡터와 매우 유사한 메서드들을 제공합니다. 다만 일부 다른 점도 있습니다.

먼저 인덱싱은 허용되지 않는다는 점입니다. 이는 UTF-8 인코딩을 사용하는 문자열이 가변 길이이기 때문입니다. 대신 []와 .. 연산자로 부분 문자열 슬라이스를 얻을 수 있습니다.

또한 + 연산자로 문자열을 합칠 수 있지만, push_str() 메서드가 더 효율적입니다. 이는 +가 새로운 String을 생성하기 때문입니다.

문자열 포매팅과 유니코드 처리

문자열 포매팅과 유니코드 처리도 중요합니다. 러스트는 표준 라이브러리에서 다양한 기능을 지원합니다.

// 형식 지정 매크로
let x = 5;
let y = 10;
println!("{} + {} = {}", x, y, x + y); // 출력: "5 + 10 = 15" 

// 유니코드 문자열에 포함된 스칼라 값 접근
for c in "नमस्ते".chars() {
    println!("{}", c);
}

// 문자열에서 바이트 값에 직접 접근
for b in "hello \u{0bc6}".bytes() {
    print!("{} ", b);
} // 출력: 104 101 108 108 111 208 189 198

// 유니코드 스칼라 값에서 문자열로 변환 
let hello = std::char::from_u32(0x1f308).unwrap();
assert_eq!(hello, "🌈"); // 올바른 유니코드 스칼라 값이므로 문자열 변환 가능

println!과 같은 형식 지정 매크로를 사용하면 편리하게 문자열을 출력할 수 있습니다. 또한 유니코드 문자열도 다룰 수 있습니다.

chars() 메서드로 문자열을 유니코드 스칼라 값 단위로 순회할 수 있고, bytes()는 바이트 단위로 순회합니다. 그리고 from_u32()와 같은 메서드로 유니코드값에서 char나 문자열로 변환할 수 있습니다.

문자열 관련 크레이트

표준 라이브러리 외에도 많은 써드파티 크레이트가 문자열 처리를 돕고 있습니다. 몇 가지만 소개하겠습니다.

  • regex: 정규 표현식 라이브러리로서 강력한 패턴 매칭 기능을 제공합니다.
  • encoding_rs: 다양한 문자 인코딩 방식을 지원합니다.
  • url: URL 문자열 파싱과 조작 기능을 제공합니다.
  • base64: BASE64 인코딩/디코딩 기능을 제공합니다.
  • unicode-segmentation: 유니코드 문자열에 대한 세분화 알고리즘을 제공합니다.

이들 크레이트에 대해서는 필요에 따라 공식 문서를 참고하면 됩니다.

 

 

한 고대 문서 이야기

여기 한 고대 문서가 있습니다. 이 문서는 B.C. 1,500년 부터 A.D 100년까지 약 1,600 여 년 동안 기록되었습니다. 이 문서의 저자는 약 40 명입니다. 이 문서의 고대 사본은 25,000 개가 넘으나, 사본간 오

gospel79.tistory.com

 

유튜브 프리미엄 월 1만원 할인받고 월 4000원에 이용하는 방법

올해 5월부터 월 8000원 정도이던 유튜브 프리미엄 요금이 15000원 정도로 인상됩니다. 각종 OTT 서비스, ChatGPT 같은 서비스들이 늘어나다보니 이런 거 몇 개만 이용하더라도 월 이용요금이 5만원을

stock79.tistory.com

 

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."

728x90
반응형