23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
6차 스터디는 6월 24일 진행 했습니다. 최신 언어들이 멀티 패러다임의 특성을 가지는 것 처럼 Rust도 멀티 패러다임을 지원합니다.
그 중에서도 함수형 언어의 페러다임을 많이 차용한 것 같습니다.
함수형 언어의 특징 중에서 빠지면 안되는 클로저와 반복자에 대한 내용을 김진님께서 발표해 주셨습니다.
GC를 지원하지 않는 언어는 힙 메모리에서 할당을 받게 되면, 반드시 해제를 해줘야 메모리 누수가 발생하지 않습니다.
C++에서도 이러한 메모리 누수를 방지하기 위해, 생성자에서 할당을 하고 소멸자에서 해제를 하여 라이프타임이 끝나면 자동으로 메모리를 해제 할 수 있는 방법을 RAII라는 기법으로 사용하고 있습니다.
RAII를 사용하여 구현된 스마트 포인터로 unique_ptr, shared_ptr등이 있는데, Rust에도 마찬가지로 스마트 포인터가 있습니다.
15장에선 해당 스마트 포인터에 대한 내용을 다루고 있습니다.
13장, 15장에 대한 간단한 정리
러스트는 함수형 프로그래밍의 특성을 활용할 수 있는 기능을 제공합니다.
클로저와 반복자는 이러한 특성 중 중요한 요소로, 코드를 더 간결하고 표현력 있게 만들어줍니다.
이번 글에서는 클로저, 반복자, I/O 프로젝트 개선, 루프와 반복자의 성능 비교에 대해 자세히 알아보겠습니다.
클로저(Closure)는 자신의 환경을 캡처할 수 있는 익명 함수입니다.
클로저는 함수처럼 호출할 수 있지만, 함수와 달리 클로저는 자신의 스코프 내에 있는 변수에 접근할 수 있습니다.
클로저는 fn 키워드 대신 ||를 사용하여 정의합니다.
let x = 5;
let add_x = |y| y + x;
println!("Result: {}", add_x(10)); // Result: 15위 예제에서 클로저 add_x는 x 변수를 캡처하여 y에 더합니다. 클로저는 x의 값을 기억하고 있으며,
이를 통해 add_x를 호출할 때마다 x와 y를 더한 값을 반환합니다.
클로저는 세 가지 유형이 있습니다: Fn, FnMut, FnOnce. 각각의 차이는 클로저가 환경을 캡처하는 방식에 있습니다.
Fn: 클로저가 환경을 불변으로 캡처합니다.
FnMut: 클로저가 환경을 가변으로 캡처합니다.
FnOnce: 클로저가 환경을 소유권으로 캡처합니다.
let x = vec![1, 2, 3];
// Fn 클로저
let equal_to_x = move |z| z == x;
let y = vec![1, 2, 3];
assert!(equal_to_x(y));위 예제에서 move 키워드는 클로저가 환경을 소유권으로 캡처하도록 합니다.
이를 통해 클로저가 캡처한 값을 다른 곳에서 사용할 수 없게 만듭니다.
반복자(Iterator)는 일련의 아이템을 처리하는 데 사용됩니다.
반복자는 Iterator 트레이트를 구현하며, next 메서드를 통해 각 아이템에 접근할 수 있습니다.
let v = vec![1, 2, 3];
let mut v_iter = v.iter();
assert_eq!(v_iter.next(), Some(&1));
assert_eq!(v_iter.next(), Some(&2));
assert_eq!(v_iter.next(), Some(&3));
assert_eq!(v_iter.next(), None);위 예제에서 v.iter()는 벡터 v의 불변 참조자 반복자를 생성합니다.
next 메서드는 각 아이템에 순차적으로 접근하며, 아이템이 없으면 None을 반환합니다.
반복자 어댑터는 반복자를 변환하여 새로운 반복자를 생성합니다.
예를 들어, map 메서드는 각 아이템에 대해 클로저를 적용하여 새로운 값을 생성합니다.
let v: Vec<i32> = vec![1, 2, 3].iter().map(|x| x + 1).collect();
assert_eq!(v, vec![2, 3, 4]);위 예제에서 map 메서드는 각 아이템에 x + 1 클로저를 적용하여 새로운 벡터를 생성합니다.
클로저와 반복자를 사용하여 I/O 프로젝트를 개선할 수 있습니다.
예를 들어, 파일에서 읽은 데이터를 처리하는 프로그램을 작성해보겠습니다.
use std::fs::File;
use std::io::{self, BufRead, BufReader};
fn read_lines(filename: &str) -> io::Result<Vec<String>> {
let file = File::open(filename)?;
let reader = BufReader::new(file);
reader.lines().collect()
}
fn main() -> io::Result<()> {
let lines = read_lines("input.txt")?;
for line in lines {
println!("{}", line);
}
Ok(())
}위 예제에서 read_lines 함수는 파일을 열고, BufReader를 사용하여 각 라인을 읽습니다.
lines 메서드는 반복자를 반환하며, 이를 collect 메서드로 벡터로 변환합니다.
fn process_lines(lines: Vec<String>) -> Vec<String> {
lines.into_iter().map(|line| line.to_uppercase()).collect()
}
fn main() -> io::Result<()> {
let lines = read_lines("input.txt")?;
let processed_lines = process_lines(lines);
for line in processed_lines {
println!("{}", line);
}
Ok(())
}위 예제에서 process_lines 함수는 클로저와 반복자를 사용하여 각 라인을 대문자로 변환합니다.
into_iter 메서드는 소유권을 가진 반복자를 생성하며, map 메서드는 각 아이템에 클로저를 적용합니다.
루프와 반복자의 성능을 비교해보겠습니다. 일반적으로 반복자는 최적화된 코드로 컴파일되기 때문에 루프와 유사한 성능을 보입니다.
let v = vec![1, 2, 3, 4, 5];
let mut sum = 0;
for i in &v {
sum += i;
}
println!("Sum: {}", sum);let v = vec![1, 2, 3, 4, 5];
let sum: i32 = v.iter().sum();
println!("Sum: {}", sum);위 두 예제는 동일한 결과를 출력하지만, 반복자를 사용한 예제는 더 간결하고 읽기 쉬운 코드입니다.
컴파일러는 반복자를 최적화하여 루프와 유사한 성능을 제공합니다.
클로저와 반복자는 러스트의 함수형 프로그래밍 특성을 잘 보여주는 예제입니다.
클로저는 환경을 캡처하여 유연한 익명 함수를 제공하고, 반복자는 일련의 아이템을 처리하는 간결한 방법을 제공합니다.
클로저와 반복자를 사용하면 코드의 가독성과 유지보수성을 높일 수 있으며, 최적화된 성능을 유지할 수 있습니다.
러스트의 스마트 포인터는 데이터와 메모리 관리에서 중요한 역할을 합니다. 기본 포인터와 달리 스마트 포인터는 추가적인 메모리 관리 기능을 제공합니다.
이번 글에서는 Box<T>, Deref 트레이트, Drop 트레이트, Rc<T>, RefCell<T>, 그리고 순환 참조에 대해 자세히 알아보겠습니다.
Box<T>를 사용하여 힙에 있는 데이터 가리키기Box<T>는 데이터를 힙에 저장하고, 힙에 저장된 데이터를 스택 상에서 가리킬 수 있게 합니다.
이는 주로 크기가 커서 스택에 올릴 수 없는 데이터를 힙에 저장할 때 사용됩니다.
Box<T>는 컴파일 타임에 크기를 알 수 없는 타입을 다루거나, 재귀적인 데이터 구조를 저장할 때 유용합니다.
Box<T> 사용 예제fn main() {
let b = Box::new(5);
println!("b = {}", b);
}위 예제에서 Box::new(5)는 힙에 5를 저장하고, b는 힙에 저장된 데이터를 가리키는 스마트 포인터입니다.
b를 통해 힙에 저장된 5에 접근할 수 있습니다.
재귀적 데이터 구조를 정의할 때도 Box<T>를 사용할 수 있습니다. 예를 들어, 링크드 리스트를 정의할 때 Box<T>를 사용합니다.
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}위 예제에서 List는 재귀적으로 정의된 열거형입니다.
Cons 변형은 값을 저장하고, 다음 요소를 가리키는 Box<List>를 갖습니다. Nil 변형은 리스트의 끝을 나타냅니다.
이 방식으로 크기가 고정되지 않은 데이터 구조를 효율적으로 저장할 수 있습니다.
Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기Deref 트레이트를 구현하면 스마트 포인터를 일반 참조자처럼 사용할 수 있습니다.
Deref 트레이트는 deref 메서드를 정의하여 스마트 포인터가 가리키는 값을 반환합니다.
이는 스마트 포인터를 더 직관적이고 사용하기 쉽게 만들어줍니다.
Deref 트레이트 구현use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}위 예제에서 MyBox는 Deref 트레이트를 구현하여 스마트 포인터가 가리키는 값을 반환합니다.
* 연산자를 사용하여 MyBox가 가리키는 값에 접근할 수 있습니다.
이는 함수나 메서드가 일반 참조자를 기대할 때도 스마트 포인터를 전달할 수 있게 해줍니다.
Drop 트레이트로 메모리 정리 코드 실행하기Drop 트레이트를 구현하면 스마트 포인터가 스코프를 벗어날 때 커스텀 메모리 정리 코드를 실행할 수 있습니다.
Drop 트레이트는 drop 메서드를 정의하여 스마트 포인터가 삭제될 때 호출됩니다. 이를 통해 메모리 누수나 리소스 누출을 방지할 수 있습니다.
Drop 트레이트 구현struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}위 예제에서 CustomSmartPointer는 Drop 트레이트를 구현하여 스마트 포인터가 스코프를 벗어날 때 커스텀 정리 코드를 실행합니다.
프로그램이 종료될 때 drop 메서드가 호출되어 데이터를 정리합니다. 이를 통해 리소스를 안전하게 해제할 수 있습니다.
Rc<T>, 참조 카운트 스마트 포인터Rc<T>는 참조 카운트(reference count) 스마트 포인터로, 데이터를 여러 소유자로 공유할 수 있게 합니다.
Rc<T>는 단일 스레드에서만 사용 가능하며, 주로 읽기 전용 데이터의 공유에 사용됩니다.
Rc<T>는 데이터를 가리키는 포인터의 개수를 추적하여 마지막 포인터가 제거될 때 데이터를 해제합니다.
Rc<T> 사용 예제use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}위 예제에서 Rc::new를 사용하여 데이터를 여러 소유자로 공유합니다.
Rc::clone 메서드를 사용하여 참조 카운트를 증가시킵니다.
이를 통해 데이터의 소유자를 여러 개 가질 수 있으며, 마지막 참조자가 스코프를 벗어날 때 데이터가 해제됩니다.
RefCell<T>와 내부 가변성 패턴RefCell<T>는 런타임에 불변성을 검증하는 스마트 포인터입니다.
RefCell<T>를 사용하면 데이터가 불변이어도 내부적으로 데이터를 변경할 수 있습니다.
이는 내부 가변성 패턴을 통해 이루어지며, RefCell<T>는 불변성을 컴파일 타임이 아닌 런타임에 체크합니다.
RefCell<T> 사용 예제use std::cell::RefCell;
#[derive(Debug)]
struct List {
value: i32,
next: RefCell<Option<Box<List>>>,
}
fn main() {
let list = List {
value: 5,
next: RefCell::new(None),
};
list.next.replace(Some(Box::new(List {
value: 10,
next: RefCell::new(None),
})));
println!("{:?}", list);
}위 예제에서 RefCell::new를 사용하여 내부 가변성을 구현합니다.
replace 메서드를 사용하여 내부 데이터를 변경할 수 있습니다.
이는 주로 외부에서는 불변이지만 내부적으로는 가변적인 데이터를 다룰 때 유용합니다.
순환 참조는 두 개 이상의 Rc<T>가 서로를 가리키는 경우 발생합니다.
순환 참조가 발생하면 참조 카운트가 0이 되지 않아 메모리가 해제되지 않는 메모리 누수가 발생할 수 있습니다. 이를 방지하기 위해 약한 참조 Weak<T>를 사용합니다.
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct List {
value: i32,
next: RefCell<Option<Rc<List>>>,
}
fn main() {
let a = Rc::new(List {
value: 5,
next: RefCell::new(None),
});
let b = Rc::new(List {
value: 10,
next: RefCell::new(Some(Rc::clone(&a))),
});
a.next.replace(Some(Rc::clone(&b)));
println!("a next = {:?}", a.next.borrow());
println!("b next = {:?}", b.next.borrow());
}위 예제에서 a와 b는 서로를 가리키는 순환 참조를 만듭니다. 이를 해결하려면 Weak<T>를 사용하여 약한 참조를 만들어 순환 참조를 방지할 수 있습니다.
use std::rc::{Rc, Weak};
use std::
cell::RefCell;
#[derive(Debug)]
struct List {
value: i32,
next: RefCell<Option<Weak<List>>>,
}
fn main() {
let a = Rc::new(List {
value: 5,
next: RefCell::new(None),
});
let b = Rc::new(List {
value: 10,
next: RefCell::new(Some(Rc::downgrade(&a))),
});
a.next.replace(Some(Rc::downgrade(&b)));
println!("a next = {:?}", a.next.borrow().upgrade());
println!("b next = {:?}", b.next.borrow().upgrade());
}위 예제에서 Rc::downgrade를 사용하여 약한 참조를 생성합니다.
Weak<T>는 참조 카운트를 증가시키지 않으며, upgrade 메서드를 통해 Rc<T>로 업그레이드할 수 있습니다.
이를 통해 순환 참조를 방지하고, 메모리 누수를 피할 수 있습니다.
러스트의 스마트 포인터는 메모리 관리와 데이터 소유권을 효율적으로 다룰 수 있게 해줍니다.
Box<T>를 사용하여 힙에 데이터를 저장하고, Deref 트레이트로 스마트 포인터를 일반 참조자처럼 사용할 수 있습니다.
Drop 트레이트를 통해 커스텀 메모리 정리 코드를 실행하고, Rc<T>로 데이터를 여러 소유자로 공유할 수 있습니다.
RefCell<T>를 사용하면 내부 가변성을 구현할 수 있으며, 순환 참조를 방지하기 위해 Weak<T>를 사용할 수 있습니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.