데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      Openlab - 6차 Rust 스터디

      lambda 24.06.30
      442 0 0
      DEVOTEE 요약
      6차 스터디에서는 Rust의 멀티 패러다임적인 특성, 특히 함수형 언어의 클로저와 반복자에 대해 논의하였습니다. Rust는 스마트 포인터를 사용하여 메모리 관리 문제를 해결하며, 15장에서는 Box, Rc, RefCell, 그리고 Deref 트레이트 등을 통해 힙 메모리 관리와 내부 가변성을 자세히 다룹니다. 또한, 순환 참조 방지를 위해 Weak 사용법도 설명되었습니다.
      DEVOTEE 추천 블로그

      6주차 스터디 소개

      6차 스터디는 6월 24일 진행 했습니다. 최신 언어들이 멀티 패러다임의 특성을 가지는 것 처럼 Rust도 멀티 패러다임을 지원합니다.

      그 중에서도 함수형 언어의 페러다임을 많이 차용한 것 같습니다.

      함수형 언어의 특징 중에서 빠지면 안되는 클로저와 반복자에 대한 내용을 김진님께서 발표해 주셨습니다.

      GC를 지원하지 않는 언어는 힙 메모리에서 할당을 받게 되면, 반드시 해제를 해줘야 메모리 누수가 발생하지 않습니다.

      C++에서도 이러한 메모리 누수를 방지하기 위해, 생성자에서 할당을 하고 소멸자에서 해제를 하여 라이프타임이 끝나면 자동으로 메모리를 해제 할 수 있는 방법을 RAII라는 기법으로 사용하고 있습니다.

      RAII를 사용하여 구현된 스마트 포인터로 unique_ptr, shared_ptr등이 있는데, Rust에도 마찬가지로 스마트 포인터가 있습니다.

      15장에선 해당 스마트 포인터에 대한 내용을 다루고 있습니다.



      13장, 15장에 대한 간단한 정리

      13. 함수형 언어의 특성: 반복자와 클로저

      러스트는 함수형 프로그래밍의 특성을 활용할 수 있는 기능을 제공합니다.

      클로저와 반복자는 이러한 특성 중 중요한 요소로, 코드를 더 간결하고 표현력 있게 만들어줍니다.

      이번 글에서는 클로저, 반복자, I/O 프로젝트 개선, 루프와 반복자의 성능 비교에 대해 자세히 알아보겠습니다.

      13.1. 클로저: 자신의 환경을 캡처하는 익명 함수

      클로저(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 키워드는 클로저가 환경을 소유권으로 캡처하도록 합니다.

      이를 통해 클로저가 캡처한 값을 다른 곳에서 사용할 수 없게 만듭니다.

      13.2. 반복자로 일련의 아이템들 처리하기

      반복자(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 클로저를 적용하여 새로운 벡터를 생성합니다.

      13.3. I/O 프로젝트 개선하기

      클로저와 반복자를 사용하여 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 메서드는 각 아이템에 클로저를 적용합니다.

      13.4. 성능 비교하기: 루프 vs. 반복자

      루프와 반복자의 성능을 비교해보겠습니다. 일반적으로 반복자는 최적화된 코드로 컴파일되기 때문에 루프와 유사한 성능을 보입니다.

      루프를 사용한 합계 계산

      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);

      위 두 예제는 동일한 결과를 출력하지만, 반복자를 사용한 예제는 더 간결하고 읽기 쉬운 코드입니다.

      컴파일러는 반복자를 최적화하여 루프와 유사한 성능을 제공합니다.

      결론

      클로저와 반복자는 러스트의 함수형 프로그래밍 특성을 잘 보여주는 예제입니다.

      클로저는 환경을 캡처하여 유연한 익명 함수를 제공하고, 반복자는 일련의 아이템을 처리하는 간결한 방법을 제공합니다.

      클로저와 반복자를 사용하면 코드의 가독성과 유지보수성을 높일 수 있으며, 최적화된 성능을 유지할 수 있습니다.


      15. 스마트 포인터

      러스트의 스마트 포인터는 데이터와 메모리 관리에서 중요한 역할을 합니다. 기본 포인터와 달리 스마트 포인터는 추가적인 메모리 관리 기능을 제공합니다.

      이번 글에서는 Box<T>, Deref 트레이트, Drop 트레이트, Rc<T>, RefCell<T>, 그리고 순환 참조에 대해 자세히 알아보겠습니다.

      15.1. 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 변형은 리스트의 끝을 나타냅니다.

      이 방식으로 크기가 고정되지 않은 데이터 구조를 효율적으로 저장할 수 있습니다.

      15.2. 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가 가리키는 값에 접근할 수 있습니다.

      이는 함수나 메서드가 일반 참조자를 기대할 때도 스마트 포인터를 전달할 수 있게 해줍니다.

      15.3. 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 메서드가 호출되어 데이터를 정리합니다. 이를 통해 리소스를 안전하게 해제할 수 있습니다.

      15.4. 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 메서드를 사용하여 참조 카운트를 증가시킵니다.

      이를 통해 데이터의 소유자를 여러 개 가질 수 있으며, 마지막 참조자가 스코프를 벗어날 때 데이터가 해제됩니다.

      15.5. 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 메서드를 사용하여 내부 데이터를 변경할 수 있습니다.

      이는 주로 외부에서는 불변이지만 내부적으로는 가변적인 데이터를 다룰 때 유용합니다.

      15.6. 순환 참조는 메모리 누수를 발생시킬 수 있습니다.

      순환 참조는 두 개 이상의 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>를 사용할 수 있습니다.

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      lambda 님의 최신 블로그

      더보기

      DEVOTEE 추천 블로그

      동영상 기고하기