&str typelet world: &str = "world";
&str 타입은 C나 C++의 char* (또는 const char*)와 유사한 개념입니다. &str은 문자열 슬라이스를 나타내며, 이는 컴파일 타임에 크기가 결정되지 않은 문자열 데이터에 대한 참조입니다. 이는 불변 참조로, 가리키는 데이터를 변경할 수 없습니다. Rust에서 문자열을 다룰 때, String 타입과 &str 타입 두 가지를 주로 사용하는데, String은 힙에 할당된 변경 가능한 문자열을 나타내고, &str은 보통 문자열 리터럴이나 String으로부터 빌린 불변 참조를 나타냅니다.constconst NUMBER: i32 = 3;
const를 사용할 때 타입을 명시적으로 지정해야 하는 이유는, Rust 컴파일러가 const 값의 타입을 추론할 수 없기 때문입니다. const는 컴파일 시간 상수이며, 그 값은 프로그램의 실행 전에 결정되어야 합니다. 이러한 상수는 변수와 달리 런타임에 변경될 수 없으며, 그들의 타입은 컴파일 시간에 반드시 알려져 있어야 합니다.let 키워드를 사용하면 Rust 컴파일러는 주어진 값으로부터 변수의 타입을 추론할 수 있습니다. 하지만, const 상수에 대해서는 이러한 타입 추론이 허용되지 않습니다. const 상수는 명시적으로 타입을 지정해야 하며, 이는 컴파일러가 해당 상수의 타입을 정확히 알 수 있도록 하기 위함입니다.const THING: u32 = 0xABAD1DEA;
THING 상수는 u32 타입으로 명시적으로 선언되었습니다. 이는 const 상수가 프로그램 전체에서 일관된 타입을 가지고 사용될 수 있도록 보장합니다. 타입을 명시적으로 선언함으로써, 컴파일러는 상수의 사용을 더 정확히 검사할 수 있고, 타입 관련 오류를 미리 방지할 수 있습니다(Rust Docs).returnRust에서 함수가 값을 반환하는 방법은 몇 가지가 있습니다. 가장 기본적인 방법은 return 키워드를 사용하는 것입니다. 하지만 Rust에서는 return 키워드를 생략하고 마지막 표현식의 결과를 자동으로 반환하는 것이 일반적입니다. 여기에는 몇 가지 중요한 규칙과 스타일이 있습니다.
return 키워드 사용하기return 키워드는 함수에서 명시적으로 값을 반환할 때 사용됩니다. 이는 특히 함수의 중간에서 반환해야 할 때 유용합니다. 예를 들어:
fn add_one(x: i32) -> i32 {
return x + 1;
}
또는 함수의 중간에서 조건에 따라 반환해야 하는 경우:
fn check_number(x: i32) -> i32 {
if x > 5 {
return x;
}
x + 1
}
Rust에서는 함수의 마지막 표현식이 자동으로 반환 값이 됩니다. 이 때 return 키워드와 세미콜론(;)을 생략해야 합니다. 이 방식은 Rust에서 권장되는 스타일입니다. 예를 들어:
fn add_one(x: i32) -> i32 {
x + 1
}
이 경우, x + 1이 마지막 표현식이므로 이 함수는 x + 1의 결과를 반환합니다.
함수가 값을 반환할 때, 함수 선언에 반환 타입을 명시해야 합니다. 반환 값이 없는 경우, 반환 타입은 ()로 표시되며, 이는 "unit type"이라고 불립니다. 예를 들어:
fn do_nothing() -> () {
// 아무것도 반환하지 않음
}
또는 더 간단하게 반환 타입을 생략할 수 있습니다:
fn do_nothing() {
// 아무것도 반환하지 않음
}
Result와 Option 타입Rust에서는 에러 처리와 선택적인 값 반환을 위해 Result와 Option 타입을 사용할 수 있습니다. 이들은 Rust의 표준 라이브러리에 정의된 열거형(enum)입니다.
Option<T>는 값이 있거나 없을 수 있는 경우에 사용됩니다.Result<T, E>는 연산이 성공적으로 완료될 수도 있고, 에러가 발생할 수도 있는 경우에 사용됩니다.예를 들어, 어떤 함수가 실패할 수 있는 경우 Result 타입을 반환 타입으로 사용할 수 있습니다:
fn divide(numerator: f64, denominator: f64) -> Result<f64, &'static str> {
if denominator == 0.0 {
return Err("분모가 0입니다.");
}
Ok(numerator / denominator)
}
이러한 방식으로 Rust에서는 다양한 상황에서 유연하게 값을 반환하고 에러를 처리할 수 있습니다.
arrayRust에서 배열을 사용하는 방법은 다음과 같습니다. 배열은 동일한 타입의 여러 값을 연속적으로 저장할 수 있는 컬렉션입니다. 배열의 크기는 컴파일 시에 결정되며, 실행 중에는 변경할 수 없습니다.
배열을 선언하고 초기화하는 기본 문법은 다음과 같습니다:
let arr: [타입; 크기] = [값1, 값2, 값3, ...];
예를 들어, i32 타입의 5개 요소를 가진 배열을 선언하고 초기화하는 방법은 다음과 같습니다:
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
모든 요소가 동일한 값을 가질 경우, 다음과 같이 초기화할 수 있습니다:
let same_values = [3; 5]; // [3, 3, 3, 3, 3]
배열의 특정 요소에 접근하려면 인덱스를 사용합니다. 인덱스는 0부터 시작합니다:
let first = numbers[0]; // 배열의 첫 번째 요소
let second = numbers[1]; // 배열의 두 번째 요소
Rust에서 배열은 고정된 크기를 가지기 때문에, 더 유연한 컬렉션 타입이 필요한 경우 Vec<T> 타입(벡터)을 사용할 수 있습니다. 벡터는 실행 시간에 크기를 변경할 수 있습니다.
배열에 대한 더 자세한 정보는 Rust의 공식 문서에서 확인할 수 있습니다(Rust Documentation).
reference comparing with C// immutable i32
let a: i32 = 10;
println!("{}", a);
// -> 10
println!("{:p}", a);
// -> 0x16afceab4
// immutable reference to i32
let b: i32 = 5;
let c: &i32 = &b;
println!("{}", b);
// -> 5
println!("{:p}", &b);
// -> 0x16b2429a4
println!("{:p}", c);
// -> 0x16b2429a4
println!("{:p}", &c);
// -> 0x16b2429a8
// immutable i32 in c
const int a = 10;
printf("%d\n", a);
// -> 10
printf("%p\n", &a);
// -> 0x16f67b21c
// imuttable reference to i32 in c
const int b = 5;
const int *c = &b;
printf("%d\n", b);
// -> 5
printf("%p\n", &b);
// -> 0x16af83218
printf("%p\n", c);
// -> 0x16af83218
printf("%p\n", &c);
// -> 0x16af83210
dyn keyword[dyn] 키워드는 Rust에서 동적 디스패치(dynamic dispatch)를 사용하여 특정 트레잇(trait)을 구현하는 타입의 인스턴스를 가리킬 때 사용됩니다. 동적 디스패치는 컴파일 시간이 아닌 런타임에 메서드 호출이 어떤 구현을 사용할지 결정하는 방식입니다. 이는 Rust의 타입 시스템과 다형성을 활용하는 방법 중 하나입니다.
dyn Trait을 사용하면, 컴파일러에게 "이 변수는 Trait를 구현하는 어떤 타입의 값이든 가질 수 있다"고 알립니다. 이는 여러 다른 타입들이 하나의 트레잇을 공유할 수 있음을 의미하며, 이러한 타입들 중 어느 하나를 런타임에 선택하여 사용할 수 있습니다. dyn Trait 타입의 변수는 실제로 어떤 구체적인 타입의 값이 저장되어 있는지는 알 수 없지만, 그 타입이 Trait 트레잇을 구현한다는 것만 알고 있습니다.
예를 들어, Box<dyn Error>는 Error 트레잇을 구현하는 어떤 타입의 값이든 담을 수 있는 박스(Box)입니다. 이는 다양한 에러 타입들을 동일한 방식으로 처리할 수 있게 해주며, Rust의 강력한 에러 처리 기능 중 하나입니다.
동적 디스패치의 장점은 유연성입니다. 다양한 타입들을 하나의 트레잇 바운드로 추상화하여 처리할 수 있습니다. 단점은 런타임에 어떤 메서드를 호출할지 결정해야 하므로, 정적 디스패치(static dispatch)에 비해 약간의 성능 오버헤드가 발생할 수 있다는 점입니다. Rust는 기본적으로 정적 디스패치를 선호하지만, dyn Trait을 사용하여 필요에 따라 동적 디스패치를 선택할 수 있습니다.
반대로 정적 디스패치(static dispatch)는 Rust에서 컴파일 시간에 메서드 호출이 어떤 구현을 사용할지 결정하는 방식입니다. 이는 주로 제네릭과 트레잇 바운드(trait bounds)를 사용하여 구현됩니다. 정적 디스패치는 런타임 비용 없이 다형성을 제공하며, 인라인화(inlining)와 같은 컴파일러 최적화를 가능하게 합니다.
map_err #[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn map_err<F, O: FnOnce(E) -> F>(self, op: O) -> Result<T, F> {
match self {
Ok(t) => Ok(t),
Err(e) => Err(op(e)),
}
}
이 부분을 보고있는데 지금 그러면 op라는게 from_parseint 이 함수가 되는거고 그 함수O의 형태는 FnOnce(E) -> F이니까 E는 ParseIntError가 되는거고 F는 ParsePosNonzeroError가 되는거겠지? 그리고 또한 self는 이 함수를 부른 parse에서 나온 result 객체를 말하는거고, 그게 OK이면 OK그대로 보내주는 대신 Err이였으면 그 함수를 호출해서 Err를 감싸서 보내준다 이말이네??
네, 정확히 맞습니다. 여기서 설명하신 [map_err] 메서드의 구현체를 보면, 이 메서드는 Result<T, E> 타입의 self를 받아서, 에러(E)를 다른 타입(F)으로 변환하는 함수 op를 인자로 받습니다. op는 FnOnce(E) -> F 특성을 만족하는 함수나 클로저여야 합니다. 여기서 E는 원래의 에러 타입이고, F는 변환된 에러 타입입니다.
self는 map_err 메서드를 호출하는 Result<T, E> 타입의 인스턴스입니다. 이는 Ok(T) 또는 Err(E) 값을 가질 수 있습니다.op는 에러를 변환하는 함수나 클로저입니다. 이 예제에서는 ParsePosNonzeroError::from_parseint가 op에 해당합니다.map_err 메서드의 동작은 다음과 같습니다:
self가 Ok(t)인 경우, 즉 성공한 경우, Ok(t)를 그대로 반환합니다. 여기서 t는 성공 값입니다.self가 Err(e)인 경우, 즉 에러가 발생한 경우, op 함수(여기서는 ParsePosNonzeroError::from_parseint)를 호출하여 e를 새로운 에러 타입 F로 변환합니다. 그리고 변환된 에러 Err(op(e))를 반환합니다.이 과정에서 E는 원래의 에러 타입인 ParseIntError가 되고, F는 변환된 에러 타입인 ParsePosNonzeroError가 됩니다. 따라서, map_err를 사용하여 Result<T, ParseIntError> 타입의 값을 Result<T, ParsePosNonzeroError> 타입으로 변환할 수 있습니다. 이는 에러 처리를 더 유연하게 만들어주며, 다양한 에러 타입을 통합하거나 변환하는 데 유용합니다.
아 근데 궁금한건 op(e)의 return 이 결국 Err타입인데 왜 Err를 감싸서 return 하는거야? 그냥 Err(e) => op(e) 하면 안되는 이유
op(e)의 반환 값 자체는 [Err]타입이 아니라, op 함수나 클로저에 의해 변환된 새로운 에러 타입 F의 값입니다. [map_err] 메서드의 목적은 Result<T, E> 타입의 Err 값을 취해 그 에러를 다른 타입 F로 변환하는 것이며, 이 변환된 값은 여전히 Result 타입의 Err 분기에 포함되어야 합니다.
즉, op(e) 호출의 결과는 단순히 새로운 에러 값이고, 이를 Result 타입의 Err 분기로 포장해야 합니다. 그래서 Err(op(e))와 같이 쓰는 것입니다. 여기서 Err는 Result 타입을 생성하는 생성자입니다.
Err(e) => op(e)와 같이 작성하면, Rust 컴파일러는 op(e)의 결과를 직접 Result 타입의 Err 분기로 인식하지 못합니다. op(e)는 단지 F 타입의 값일 뿐이기 때문입니다. 따라서, Err(op(e))와 같이 명시적으로 Err 생성자를 사용하여 Result 타입의 새로운 Err 값을 명확하게 생성해야 합니다.
간단히 말해, op(e)는 새로운 에러 타입 F의 값이고, Err(op(e))는 이 새로운 에러 값을 Result 타입의 Err 분기로 포장하는 것입니다. 이렇게 함으로써, map_err 메서드는 원래의 에러 타입 E를 새로운 에러 타입 F로 변환한 Result<T, F> 타입을 반환할 수 있습니다.
네, trait 정의에서 파라미터에 mut를 명시하지 않아도 구현체에서 mut self를 사용할 수 있습니다. Rust에서 trait 메서드의 시그니처는 해당 메서드가 호출될 때 필요한 소유권, 빌림, 또는 변경 가능한 빌림(mutability)의 요구사항을 정의합니다. 그러나 구현체에서는 이러한 요구사항을 충족시키는 한에서, 메서드 내에서 변수를 변경 가능하게 만들기 위해 mut 키워드를 사용할 수 있습니다.
pub trait TraitExample {
fn ICanMakeFunctionInTrait(self) -> Self {
self
}
}
fn compare_license_types(software: impl Licensed, software_two: impl Licensed) -> bool {
software.licensing_info() == software_two.licensing_info()
}
fn compare_license_types<T: Licensed, U: Licensed>(software: T, software_two: U) -> bool {
software.licensing_info() == software_two.licensing_info()
}
뭐 둘다 작동한다.
pub struct ReportCard<T> {
pub grade: T,
pub student_name: String,
pub student_age: u8,
}
impl<T: std::fmt::Display> ReportCard<T> {
fn print(&self) -> String {
format!(
"{} ({}) - achieved a grade of {}",
&self.student_name, &self.student_age, &self.grade
)
}
}
그러니까 정리하자면 format에 들어가야하는 parameter들은 모두 display tarit을 만족시켜줘야하는데 지금 내가 T라고 임의로 generic하게 받아왔으니까 그걸 restrict를 해줘서 format parameter로 들어갈 수 있도록 해주는거라는거지?
추가질문 : 그러면 나머지 String, 또는 u8은 Display trait을 가지고있나?
네, 정확합니다. format! 매크로에 들어가는 파라미터들은 모두 [std::fmt::Display] 트레잇을 만족해야 합니다. 이는 Rust에서 {} 포맷 지정자를 사용할 때, 해당 값이 사용자에게 친숙한 형태로 출력될 수 있도록 보장하기 위함입니다. 제네릭 타입 [T]에 대해 T: std::fmt::Display 제약을 추가함으로써, T 타입의 값이 format! 매크로의 파라미터로 사용될 수 있게 됩니다.
네, Rust의 표준 라이브러리에서 제공하는 String 타입과 기본 숫자 타입들(여기서는 u8을 포함한 모든 정수 및 부동 소수점 타입)은 std::fmt::Display 트레잇을 구현하고 있습니다. 이는 이러한 타입들이 {} 포맷 지정자를 사용하여 format!, println! 등의 매크로에서 직접 사용될 수 있음을 의미합니다.
String 타입: String은 Rust의 문자열 타입으로, 사용자에게 친숙한 형태로 출력될 수 있도록 Display 트레잇을 구현합니다. 따라서 String 타입의 값은 {} 포맷 지정자를 사용하여 직접 출력할 수 있습니다.u8 타입: u8은 8비트 부호 없는 정수 타입으로, Display 트레잇을 구현합니다. 이는 u8 타입의 값도 {} 포맷 지정자를 사용하여 직접 출력할 수 있음을 의미합니다. 실제로 Rust의 모든 기본 숫자 타입들은 Display 트레잇을 구현하여, 숫자 값을 사용자에게 친숙한 형태로 출력할 수 있습니다.이러한 구현 덕분에, String, u8, 그리고 다른 기본 타입들은 추가적인 작업 없이도 format!, println! 등에서 직접 사용될 수 있습니다.
next() 호출시 cannot borrow as mutable 에러Rust에서 iterator를 사용할 때 [next()]메소드를 호출하면 이터레이터의 내부 상태가 변경됩니다. 이 메소드는 이터레이터가 다음 요소로 진행하면서, 현재 위치를 업데이트합니다. 따라서, 이터레이터는 내부 상태를 변경할 수 있어야 하며, 이를 위해 mutable하게 선언되어야 합니다.
간단히 말해서, [next()]메소드는 이터레이터의 내부 상태를 변경하기 때문에, 이터레이터 변수는 mutable해야 합니다. 이는 Rust의 소유권 및 빌림 규칙에 따라, 데이터의 안전한 동시 수정을 방지하기 위한 설계입니다.
collect() 의 유연성Rust의 표준 라이브러리는 다양한 컬렉션 타입들에 대해 [FromIterator] 트레잇을 구현하고 있습니다. 여기서는 두 가지 경우, Result<Vec<i32>, DivisionError>와 Vec<Result<i32, DivisionError>>에 대해 FromIterator의 구현을 살펴보겠습니다.
Result<Vec<i32>, DivisionError>이 타입에 대한 FromIterator 구현은 Iterator의 각 요소가 Result<T, E> 타입일 때, 모든 요소가 Ok인 경우에만 Ok(Vec<T>)를 반환하고, 하나라도 Err인 경우 첫 번째 Err를 반환하는 방식으로 작동합니다. 이 구현은 Result 타입에 대해 제공됩니다.
impl<T, E, V> FromIterator<Result<T, E>> for Result<V, E>
where
V: FromIterator<T>,
{
fn from_iter<I: IntoIterator<Item = Result<T, E>>>(iter: I) -> Self {
let mut iter = iter.into_iter();
let mut v: V = match iter.next() {
Some(Ok(value)) => {
let mut v = V::from_iter(std::iter::once(value));
for value in iter {
match value {
Ok(value) => v.extend(std::iter::once(value)),
Err(e) => return Err(e),
}
}
v
}
Some(Err(e)) => return Err(e),
None => return Ok(V::from_iter(std::iter::empty())),
};
Ok(v)
}
}
Vec<Result<i32, DivisionError>>이 타입에 대한 FromIterator 구현은 간단합니다. Iterator의 각 요소를 순회하면서, 각 요소를 벡터에 추가합니다. 이 구현은 Vec<T> 타입에 대해 제공됩니다.
impl<T> FromIterator<T> for Vec<T> {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let mut c = Vec::new();
c.extend(iter);
c
}
}
이 경우, Iterator의 각 요소가 이미 Result<i32, DivisionError> 타입이므로, 각 요소를 그대로 벡터에 추가하기만 하면 됩니다.
이 두 구현을 통해, collect() 메서드는 Iterator의 요소들을 적절한 타입의 컬렉션으로 변환할 수 있습니다. 첫 번째 경우에서는 모든 요소가 성공(Ok)해야 하며, 하나라도 실패(Err)하는 경우 전체 결과가 실패로 처리됩니다. 두 번째 경우에서는 각 요소의 성공 또는 실패가 개별적으로 처리되어, 최종적으로 성공과 실패가 혼합된 벡터가 생성됩니다.
(0..=num) 표현식네, 맞습니다. Rust에서 (1..=num) 표현식은 RangeInclusive 타입의 이터레이터를 반환합니다. 이 이터레이터는 지정된 범위 내의 모든 값을 순차적으로 생성할 수 있습니다. 1..=num의 경우, 이는 [1]부터 [num]까지(양 끝을 포함하여)의 모든 정수를 순차적으로 생성하는 이터레이터입니다.
이터레이터는 Rust의 표준 라이브러리에 정의된 Iterator 트레잇(trait)을 구현하는 모든 타입입니다. Iterator 트레잇은 .next() 메서드를 통해 이터레이션의 다음 요소를 반환하는 기능을 제공합니다. 이터레이터는 for 루프, map, filter, [fold]등 다양한 메서드와 함께 사용될 수 있어, Rust에서 데이터 컬렉션을 다루는 강력한 도구입니다.
RangeInclusive 이터레이터는 범위의 시작점과 끝점을 포함하는 이터레이터로, 이를 통해 범위 내의 모든 값을 탐색하거나, 이터레이터를 사용하는 다양한 메서드를 통해 복잡한 연산을 간결하게 표현할 수 있습니다.
match 에서는 변수와의 비교 불가능// 원하는대로 동작안함
fn count_iterator(map: &HashMap<String, Progress>, value: Progress) -> usize {
// map is a hashmap with String keys and Progress values.
// map = { "variables1": Complete, "from_str": None, ... }
let mut count: usize = 0;
for (key, val) in map.iter() {
match val {
value => {
count += 1;
}
_ => {}
}
}
count
}
Rust에서는 패턴 매칭을 사용할 때, 리터럴 값이나 불변 참조 등과 직접 비교할 수 있지만, 변수와의 비교는 if 표현식이나 == 연산자를 사용해야 합니다. match 구문 내에서 변수와의 직접 비교를 시도할 때는 match가 기대하는 패턴 매칭의 성격상 변수의 값과 매칭되는 것이 아니라, 변수 이름을 새로운 바인딩으로 취급하기 때문에 의도한 대로 동작하지 않습니다.
Refrence Counting(RC)Rc, the Reference Counted Smart Pointer - The Rust Programming Language
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
이렇게 하면 a가 Box::new로 move해버리기 때문에 소유권이 Box::new로 가버리게 됨.
→ 따라서 이제 a는 사용못함
→ 그렇기 때문에 가능한 방법은 a를 레퍼런스로 보내면 된다.
만약 레퍼런스로 하게된다면…
그럼 정확하게 정리를 하자면 연결 리스트에서 헤드인 a를 레퍼런스로 넘겨줘도 거기에 속해있는 Box들의 소유권은 결국 b로 넘어가게되어서 거기안에서만 사용이 가능하고 그렇기 때문에 라이프타임을 지정해줘도 box의 라이프타임은 따로 계산되기때문에 c로 &a를 넘겨준다해도 작동이 안할것이다.
따라서 애초에 처음부터 refernce coounter를 사용해서 레퍼런스를 여러곳에서 사용할 수 있도록 하는게 해결 방법이다.
이게 맞냐
네, 정리하신 내용이 맞습니다. 연결 리스트의 예에서 [a]를 참조로 넘기더라도, Box 내부의 데이터에 대한 소유권은 b로 이전되며, 이로 인해 [a]를 다시 c에 사용하려고 할 때 문제가 발생합니다. Rust에서는 한 번에 하나의 변수만이 데이터의 소유권을 가질 수 있으며, Box<T>는 소유권을 가지는 타입입니다. 따라서, Box를 사용한 연결 리스트에서는 [a]의 소유권이 b로 이전되면, [a]는 더 이상 유효하지 않게 되어 c에서 사용할 수 없습니다.
이 문제를 해결하기 위한 방법 중 하나는 참조 카운터인 Rc<T>를 사용하는 것입니다. Rc<T>를 사용하면 데이터에 대한 소유권을 여러 변수가 공유할 수 있으며, 이를 통해 여러 위치에서 동일한 데이터에 접근할 수 있게 됩니다. Rc<T>는 참조 카운트를 사용하여, 데이터에 대한 참조가 더 이상 존재하지 않을 때 데이터를 자동으로 정리합니다. 이 방법을 사용하면, 연결 리스트의 각 노드를 여러 변수가 공유할 수 있으며, 소유권 문제 없이 데이터에 접근할 수 있습니다.
따라서, 여러분이 정리하신 것처럼, 연결 리스트에서 노드를 여러 위치에서 안전하게 공유하고자 할 때는 Rc<T>를 사용하는 것이 적절한 해결책입니다.
순환 참조 문제는 두 개 이상의 Rc<T> 인스턴스가 서로를 직접적이거나 간접적으로 가리키는 경우 발생합니다. 이러한 상황에서는 참조 카운트가 0이 되지 않아, Rc<T>가 가리키는 값이 절대로 드롭되지 않게 됩니다. 이는 메모리 누수로 이어질 수 있습니다.
순환 참조를 해결하기 위한 일반적인 방법은 Weak<T> 포인터를 사용하는 것입니다. Weak<T>는 Rc<T>와 유사하지만, 참조 카운트를 증가시키지 않습니다. 따라서, 순환 참조의 일부로 Weak<T>를 사용하면 순환 참조가 발생하지 않습니다.
그러나 질문에서 요청하신 것처럼, 순환 참조 문제를 [drop]함수로 해결하는 예시를 들어보겠습니다. 이 방법은 순환 참조를 끊기 위해 명시적으로 한 쪽의 Rc<T> 인스턴스를 드롭하는 것입니다. 이 예시는 실제로는 권장되지 않는 방법입니다. 순환 참조는 구조적으로 해결하는 것이 바람직합니다.
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
value: i32,
next: RefCell<Option<Rc<Node>>>,
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
value,
next: RefCell::new(None),
})
}
fn point_to_next(node: &Rc<Node>, next: &Rc<Node>) {
*node.next.borrow_mut() = Some(Rc::clone(next));
}
}
fn main() {
let node1 = Node::new(1);
let node2 = Node::new(2);
// 순환 참조 생성
Node::point_to_next(&node1, &node2);
Node::point_to_next(&node2, &node1);
// 순환 참조 확인
println!("node1 value: {}", node1.value);
println!("node2 value: {}", node2.value);
// 순환 참조 해결을 위해 명시적으로 한 노드 드롭
std::mem::drop(node1);
// 프로그램 종료 시 node2는 자동으로 드롭됩니다.
}
이 예시에서 node1과 node2는 서로를 가리키는 순환 참조를 형성합니다. 이 상태에서는 두 Node 인스턴스 모두 절대로 드롭되지 않습니다. std::mem::drop(node1);을 호출함으로써 node1을 명시적으로 드롭하고, 순환 참조를 끊습니다. 이렇게 하면 node2도 프로그램 종료 시 정상적으로 드롭될 수 있습니다.
Weak<T>를 사용하는 것이 일반적인 해결책입니다. drop을 사용하는 방법은 특정 상황에서만 임시적으로 사용할 수 있는 방법입니다.Arc 이용방법use std::sync::Arc;
use std::thread;
let five = Arc::new(5);
for _ in 0..10 {
let five = Arc::clone(&five);
thread::spawn(move || {
println!("{five:?}");
});
}
Deref trait 이란??#[lang = "deref"]
#[doc(alias = "*")]
#[doc(alias = "&*")]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "Deref"]
pub trait Deref {
/// The resulting type after dereferencing.
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "deref_target"]
#[lang = "deref_target"]
type Target: ?Sized;
/// Dereferences the value.
#[must_use]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_diagnostic_item = "deref_method"]
fn deref(&self) -> &Self::Target;
}
Deref 트레잇의 deref 메소드가 자동으로 호출되어 Cow가 감싸고 있는 데이터에 대한 참조를 제공합니다.
이 부분이 이해가 안가 특히 자동으로 호출되는 메커니즘을 이해하고싶어
Deref 트레잇의 deref 메소드가 "자동으로 호출된다"는 개념은 Rust의 deref coercion 메커니즘을 통해 이해할 수 있습니다. 이 메커니즘은 Rust 컴파일러가 특정 상황에서 자동으로 Deref 트레잇의 deref 메소드를 호출하여 타입을 자동으로 변환하는 것을 의미합니다. 이는 컴파일러가 코드에서 명시적으로 타입을 변환하지 않아도 되도록 해, 코드를 더 간결하게 만들어 줍니다.
예를 들어, Rc<String> 타입의 변수 r에 대해 String 타입의 find 메소드를 호출하고 싶을 때, (*r).find('?') 대신에 간단히 r.find('?')로 작성할 수 있습니다. 이 경우, Rust 컴파일러는 Rc<T>가 Deref<Target=T>를 구현하고 있음을 인식하고, &Rc<String>을 &String으로 자동 변환합니다. 이 변환 과정은 deref 메소드를 자동으로 호출함으로써 이루어집니다.
Deref coercion은 다음과 같은 규칙에 따라 작동합니다:
deref 호출을 삽입하여 불일치를 해결하려고 시도합니다.DerefMut 트레잇을 통해)에도 적용됩니다.따라서, Cow<&str>와 같은 타입이 Deref를 구현하고 있을 때, 이 타입의 변수를 &str처럼 직접 참조하는 것이 가능합니다. 이는 Rust의 deref coercion 메커니즘이 자동으로 deref 메소드를 호출하여, Cow<&str>를 &str로 변환하기 때문입니다.
호출하는 부분은 결국 우리 눈으로 볼 수 없는거네?
네, 정확합니다. Deref 트레잇의 deref 메소드가 자동으로 호출되는 과정은 실제로 우리 눈에 보이지 않습니다. Rust의 컴파일러가 처리하는 내부 메커니즘으로, 컴파일 타임에 발생합니다. 이 메커니즘은 Rust의 타입 시스템과 소유권 모델의 일부로, 코드의 가독성과 유지 보수성을 향상시키기 위해 설계되었습니다.
Deref coercion은 코드에서 직접적인 타입 변환을 수행하지 않아도 되게 해주므로, 코드를 더 간결하고 읽기 쉽게 만들어 줍니다. 예를 들어, String을 가리키는 스마트 포인터에서 직접 문자열 메소드를 호출할 수 있게 해줍니다. 이는 Rust 컴파일러가 자동으로 필요한 deref 호출을 삽입하기 때문에 가능한 것입니다.
이러한 자동 호출 메커니즘은 Rust 프로그래머에게는 투명하게 작동합니다. 즉, 프로그래머는 Deref 트레잇이 적절하게 구현되어 있다는 것을 알고 있으면, 해당 타입을 더 기본적인 타입처럼 사용할 수 있다는 점을 이해하기만 하면 됩니다. 실제 deref 호출은 컴파일러가 알아서 처리하므로, 프로그래머는 이러한 변환 과정에 대해 신경 쓸 필요가 없습니다.
Cow 소?#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "Cow")]
pub enum Cow<'a, B: ?Sized + 'a>
where
B: ToOwned,
{
/// Borrowed data.
#[stable(feature = "rust1", since = "1.0.0")]
Borrowed(#[stable(feature = "rust1", since = "1.0.0")] &'a B),
/// Owned data.
#[stable(feature = "rust1", since = "1.0.0")]
Owned(#[stable(feature = "rust1", since = "1.0.0")] <B as ToOwned>::Owned),
}
Cow는 변형할 필요가 있을 때만 데이터를 복제하는 '복제-쓰기 시' 스마트 포인터입니다. 이는 메모리 사용량을 최적화하고, 필요할 때까지 데이터의 복제를 지연시킬 수 있게 해줍니다.(lazy copy) Cow는 두 가지 변형을 가집니다: Borrowed와 Owned입니다.
Cow는 Deref 트레잇을 구현하기 때문에, Cow가 감싸고 있는 데이터에 대해 직접 메소드를 호출할 수 있습니다. 만약 데이터를 수정하고 싶다면, to_mut 메소드를 사용하여 소유된 데이터에 대한 가변 참조를 얻을 수 있습니다. 필요하다면 이 과정에서 데이터의 복제가 발생할 수 있습니다.
cow는 다른 smartpointer 들과 같이 이렇게 Deref를 상속해오기 때문에 자동으로 derefernce를 가능하게 해준다.
impl<B: ?Sized + ToOwned> Deref for Cow<'_, B>
where
B::Owned: Borrow<B>,
{
type Target = B;
fn deref(&self) -> &B {
match *self {
Borrowed(borrowed) => borrowed,
Owned(ref owned) => owned.borrow(),
}
}
}
[Cow]는 '복사 시 쓰기(Copy On Write)'의 약자로, 데이터를 효율적으로 관리하기 위한 스마트 포인터입니다. [Cow]는 두 가지 상태를 가질 수 있습니다: [Borrowed]와 [Owned]
Cow::Owned(String::from("example"))), 복사 과정 없이 [Owned] 상태가 됩니다. 이 상태에서의 수정은 추가적인 복사 없이 이루어집니다.이러한 [Cow]의 동작 방식은 Rust의 소유권과 타입 시스템을 활용하여 데이터를 효율적으로 관리하도록 설계된 좋은 예시입니다
/// let slice = [0, 1, 2];
/// let mut input = Cow::from(&slice[..]);
/// abs_all(&mut input);
///
/// // Clone occurs because `input` needs to be mutated.
/// let slice = [-1, 0, 1];
/// let mut input = Cow::from(&slice[..]);
/// abs_all(&mut input);
///
/// // No clone occurs because `input` is already owned.
/// let mut input = Cow::from(vec![-1, 0, 1]);
/// abs_all(&mut input);
보니까 Cow::from으로 이렇게 가져오는거같음
#[stable(feature = "cow_from_vec", since = "1.8.0")]
impl<'a, T: Clone> From<&'a [T]> for Cow<'a, [T]> {
/// Creates a [`Borrowed`] variant of [`Cow`]
/// from a slice.
///
/// This conversion does not allocate or clone the data.
///
/// [`Borrowed`]: crate::borrow::Cow::Borrowed
fn from(s: &'a [T]) -> Cow<'a, [T]> {
Cow::Borrowed(s)
}
}
#[stable(feature = "cow_from_vec", since = "1.8.0")]
impl<'a, T: Clone> From<Vec<T>> for Cow<'a, [T]> {
/// Creates an [`Owned`] variant of [`Cow`]
/// from an owned instance of [`Vec`].
///
/// This conversion does not allocate or clone the data.
///
/// [`Owned`]: crate::borrow::Cow::Owned
fn from(v: Vec<T>) -> Cow<'a, [T]> {
Cow::Owned(v)
}
}
#[stable(feature = "cow_from_vec_ref", since = "1.28.0")]
impl<'a, T: Clone> From<&'a Vec<T>> for Cow<'a, [T]> {
/// Creates a [`Borrowed`] variant of [`Cow`]
/// from a reference to [`Vec`].
///
/// This conversion does not allocate or clone the data.
///
/// [`Borrowed`]: crate::borrow::Cow::Borrowed
fn from(v: &'a Vec<T>) -> Cow<'a, [T]> {
Cow::Borrowed(v.as_slice())
}
}
오버로딩해둔 from 들을 볼 수 있다.
mutex 에 대해서impl<T> Mutex<T> {
/// Creates a new mutex in an unlocked state ready for use.
///
/// # Examples
///
/// ```
/// use std::sync::Mutex;
///
/// let mutex = Mutex::new(0);
/// ```
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_stable(feature = "const_locks", since = "1.63.0")]
#[inline]
pub const fn new(t: T) -> Mutex<T> {
Mutex { inner: sys::Mutex::new(), poison: poison::Flag::new(), data: UnsafeCell::new(t) }
}
}
[Arc]자체는 멀티스레드 환경에서 데이터를 안전하게 공유하기 위한 목적으로 설계되었지만, 그것만으로는 데이터 경쟁(race conditions)을 방지할 수 없습니다. [Arc]의 주요 목적은 소유권을 공유하며, 데이터가 더 이상 사용되지 않을 때 안전하게 메모리를 해제하는 것입니다. 그러나 [Arc]는 여러 스레드가 동시에 내부 데이터에 접근하여 수정할 수 있게 해주지 않습니다.
데이터 경쟁은 두 개 이상의 스레드가 동시에 같은 메모리 위치에 접근하려고 하고, 그 중 적어도 하나의 접근이 쓰기 작업일 때 발생합니다. 이러한 상황에서는 데이터의 무결성이 손상될 수 있습니다. 예를 들어, 두 스레드가 동시에 같은 변수를 수정하려고 할 때, 최종 결과는 스레드의 실행 순서에 따라 달라질 수 있으며, 이는 예측 불가능한 결과를 초래합니다.
[Arc]는 참조 카운팅을 통해 데이터의 소유권을 안전하게 공유할 수 있도록 해주지만, 동시에 여러 스레드가 데이터를 수정하는 것을 제어하거나 동기화하지는 않습니다. 따라서, [Arc]만 사용하여 공유 데이터에 접근하면 여전히 데이터 경쟁의 위험이 있습니다.
이 문제를 해결하기 위해 [Mutex]를 사용합니다. [Mutex]는 상호 배제(mutual exclusion)를 제공하여, 한 번에 하나의 스레드만이 데이터에 접근할 수 있도록 합니다. 스레드가 [Mutex]를 통해 데이터에 접근하려면 먼저 락(lock)을 획득해야 하며, 락을 소유한 스레드만이 데이터를 읽거나 수정할 수 있습니다. 다른 스레드는 락이 해제될 때까지 대기해야 합니다.
따라서, 멀티스레드 환경에서 데이터 경쟁을 방지하고 데이터의 일관성을 유지하기 위해서는 [Arc]와 [Mutex]를 함께 사용해야 합니다. [Arc]는 데이터의 안전한 공유를 가능하게 하고, [Mutex]는 데이터에 대한 동시 접근을 제어하여 데이터 경쟁을 방지합니다. 이 조합은 멀티스레드 프로그램에서 안전하고 일관된 데이터 접근을 보장하는 데 필수적입니다.
#[macro_use]#[macro_use] 속성은 Rust에서 매크로를 현재 스코프로 가져오는 데 사용됩니다. 특히, 모듈 내에 정의된 매크로를 해당 모듈 바깥에서도 사용할 수 있게 해줍니다. 이 속성이 없으면, 매크로는 그것이 정의된 모듈 내에서만 사용할 수 있습니다.
#[macro_use] 속성은 모듈 전체에 적용되거나, 특정 외부 크레이트에 대해 사용될 때 extern crate 선언과 함께 사용될 수도 있습니다. 이 경우, 해당 크레이트에서 정의된 모든 매크로를 현재 스코프로 가져옵니다
The Little Book of Rust Macros
as_ref as_mut ?[AsRef]와 [AsMut] 트레잇은 Rust에서 타입 간의 참조 변환을 용이하게 하기 위해 사용됩니다. 이들은 특히 제네릭 함수나 구조체에서 유연성을 제공하며, 타입 안전성을 유지하면서 다양한 타입의 참조를 허용하고자 할 때 유용합니다.
[AsRef]는 참조-대-참조 변환을 위해 사용되며, 주로 비용이 많이 들지 않는 변환에 사용됩니다. 이 트레잇은 &T 타입으로의 변환을 가능하게 하여, 함수가 다양한 타입의 참조를 받아들일 수 있도록 합니다.
Trait Bound로 설정하기: 함수나 메서드의 제네릭 타입 파라미터에 AsRef<T>를 트레잇 바운드로 설정함으로써, 해당 함수는 [T] 타입으로 변환할 수 있는 어떤 타입의 참조도 받을 수 있게 됩니다.
fn print_length<T: AsRef<str>>(s: T) {
let s_ref: &str = s.as_ref();
println!("Length: {}", s_ref.len());
}
let s = "Hello, world!";
let string = "Hello, world!".to_string();
// &str과 String 모두 AsRef<str>을 구현하므로, 둘 다 이 함수에 전달될 수 있습니다.
print_length(s);
print_length(string);
AsMut 트레잇AsMut은 가변 참조 간의 변환을 위해 사용되며, AsRef의 가변 버전입니다. 이 트레잇은 데이터를 수정할 필요가 있을 때 유용하며, &mut T 타입으로의 변환을 가능하게 합니다.
Trait Bound로 설정하기: AsMut<T>를 트레잇 바운드로 사용함으로써, 함수는 T 타입으로 변환할 수 있는 어떤 타입의 가변 참조도 받을 수 있습니다.
fn clear_string<T: AsMut<String>>(s: &mut T) {
s.as_mut().clear();
}
let mut s = String::from("Hello, world!");
clear_string(&mut s);
// 이 시점에서 s는 빈 문자열입니다.
AsRef와 AsMut을 사용하여 변환된 참조는 원본 타입의 참조와 동일한 생명주기를 가집니다. 이는 Rust의 빌림 규칙과 생명주기 추론 시스템 덕분에 가능합니다. 변환 과정에서 원본 데이터의 소유권이나 가변성이 변경되지 않으며, 단지 참조의 형태만 변환됩니다. 따라서, 이러한 변환은 성능 저하 없이 안전하게 수행될 수 있습니다.
AsRef와 AsMut을 사용하는 예시는 다음과 같습니다:
fn modify_vec<T: AsMut<Vec<u32>>>(v: &mut T) {
v.as_mut().push(42);
}
let mut vec = vec![1, 2, 3];
modify_vec(&mut vec);
// 이 시점에서 vec는 [1, 2, 3, 42]입니다.
이 예시에서 modify_vec 함수는 T 타입이 Vec<u32>로 변환될 수 있는 어떤 타입의 가변 참조도 받을 수 있습니다. AsMut 트레잇을 사용함으로써, 이 함수는 Vec<u32>에 직접 접근하여 값을 수정할 수 있습니다. 이는 AsMut 트레잇이 제공하는 유연성의 예시입니다. AsMut을 사용하면, 제네릭 함수가 다양한 타입의 가변 참조를 받아들이고, 그 참조를 통해 내부 데이터를 수정할 수 있게 됩니다.
AsRef와 AsMut의 특징AsRef와 AsMut은 이상적으로 자기 자신에 대해 반사적(reflexive)이어야 합니다. 즉, 모든 타입 T에 대해, T는 자기 자신을 &T 또는 &mut T로 변환할 수 있어야 합니다. 이는 Rust의 타입 시스템에서 자동 참조 및 역참조(auto-dereferencing)를 가능하게 합니다.AsRef<str>을 사용하는 함수는 String 또는 &str을 인자로 받을 수 있습니다.AsRef와 AsMut 변환은 대부분의 경우 비용이 많이 들지 않습니다. 이 변환들은 주로 참조의 형태를 바꾸는 것이므로, 데이터의 복사본을 만들지 않습니다.Document 구조체와 제네릭 함수제공된 예시 코드에서, Document 구조체는 Vec<u8>에 대한 AsMut 구현을 제공합니다. 이를 통해 Document 인스턴스를 가변 바이트 슬라이스(&mut [u8])로 변환할 수 있습니다. 이 변환은 caesar와 null_terminate 함수에서 Document 인스턴스를 직접 수정할 수 있게 합니다.
fn caesar<T: AsMut<[u8]>>(data: &mut T, key: u8) {
for byte in data.as_mut() {
*byte = byte.wrapping_add(key);
}
}
fn null_terminate<T: AsMut<Vec<u8>>>(data: &mut T) {
fn doit(data: &mut Vec<u8>) {
if data.last() != Some(&0) {
data.push(0);
}
}
doit(data.as_mut());
}
이 예시에서 caesar 함수는 어떤 타입 T에 대해서도 작동할 수 있으며, 이 타입은 가변 바이트 슬라이스로 변환될 수 있어야 합니다. null_terminate 함수는 유사하게 Vec<u8>으로 변환될 수 있는 어떤 타입의 가변 참조도 받습니다. 이러한 유연성은 AsMut 트레잇 덕분에 가능합니다.
AsRef와 AsMut 트레잇은 Rust에서 타입 간의 참조 변환을 용이하게 하며, 제네릭 프로그래밍에서 유연성과 성능을 동시에 제공합니다. 이 트레잇들을 사용함으로써, 다양한 타입의 참조를 받아들이는 함수나 메서드를 작성할 수 있으며, 이는 Rust의 타입 시스템의 강력함을 잘 보여줍니다.