Rust 정리 (3)
2021.03.20
Generic
fn main() {
  let numbers = vec![34, 50, 25, 100, 65];
  let mut largest = numbers[0];
  for number in numbers {
    if number > largest {
      largest = number;
    }
  }
  println!("Largest : {}", largest);
}
이 후 또 다른 정수 배열에 대해 가장 큰 수를 찾는다면, for 문이 다시 한번 사용될 것이다.
fn largest(list: &[i32]) -> &i32 {
  let mut largest = &list[0];
  for item in list {
    if item > largest {
      largest = item;
    }
  }
  largest
}
이런식으로 함수를 만든다면, 정수 배열에 대해서 largest 함수만 호출하면 될 것이다.
하지만, char 배열에 대해서는 또 다시 함수를 생성해야되는 것일까? 이를 Generic으로 해결한다.
fn largest<T>(list: &[T]) -> &T {
  // ... same
}
하지만 위 코드를 컴파일해보려 하면 오류가 난다. (binary operation '>' cannot be applied to type '&T')
이를 해결하기 위해 T에 적절한 trait을 지정해주어야한다.
얘를 작동하게 만들려면?
정답은...
T: PartialOrd + Copy
Method 정의시
struct Point<T> {
  x: T,
  y: T,
}
impl<T> Point<T> {
  fn x(&self) -> &T {
    &self.x
  }
}
impl Point<i32> {
  // i32 타입일 경우에만 method 정의 가능
}
fn main() {
  let p = Point { x: 5, y: 10};
  println!("p.x = {}", p.x());
}
Trait ≈ interface
Golang에서의 interface와 같이 생각하면 된다.
pub trait Summary {
  fn summarize(&self) -> String;
}
- Type에 Trait을 implement하려면?
pub struct NewsArticle {
  pub headline: String,
  pub location: String,
  pub author: String,
  pub content: String,
}
impl Summary for NewsArticle {
  fn summarize(&self) -> String {
    format!("{}, by {} ({})", self.headline, self.author, self.location)
  }
}
pub struct Tweet {
  pub username: String,
  pub content: String,
  pub reply: bool,
  pub retweet: bool,
}
impl Summary for Tweet {
  fn summarize(&self) -> String {
    format!("{}: {}", self.username, self.content)
  }
}
impl [[TraitName]] for [[Type]]
- Parameter에 Trait 지정 (Parameter에 해당 Trait이 있어야한다.)
// (1)
pub fn notify<T: Summary + Display>(item: &T) {}
// (2)
pub fn notify(item: &(impl Summary + Display)) {}
+를 쓰면서 trait을 지정하다보면, parameter 부분의 가독성이 매우 떨어질 것이다. 이를 위해 where 을 사용할 수 있다.
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone, U: Clone + Debug {
      // ...
}
+return type에 implement trait을 지정할 수 있다.
Lifetimes
Lifetime은 다른 언어에서의 변수 scope 라고 간단히 생각할 수 있다.
러스트는 원본 레퍼런스가 사라질 가능성이 있는 경우 컴파일 에러를 일으킨다.(dangling reference)
컴파일 시기에 이러한 에러를 잡기 위한 해답으로 Rust는 Lifetime을 제시한다.
// error : `x` does not live long enough
{
  let r; // lifetime : 'a
  {
    let x = 5; // lifetime : 'b
    r = &x;
  } // 'b end : (1)
  println!("r: {}", r); // (1)에서 만료된 x를 참조하려 함
} // 'a end : (2)
위 코드가 에러가 나는 이유는, (1) 때문이다.
Rust 컴파일러의 borrow checker 는 모든 borrw 행위들이 유효한지 확인한다. Lifetime의 크기를 비교해서 짧은 lifetime 내부의 변수를 외부에서 참조하고 있다면 오류를 일으킨다.
// compile error
fn longest(x: &str, y: &str) -> &str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}
fn main() {
  let string1 = String::from("abcd");
  let string2 = "xyz";
  let result = longest(string1.as_str(), string2);
  println!("The longest string is {}", result);
}
위와 같은 코드가 있을 경우, longest에서는 x가 return될지, y가 return될지 모르는 상황이다.
함수 입장에서는 x와 y의 lifetime이 return 값과 어떤 연관이 있을지도 모른다.
'a 와 같은 형식으로 lifetime을 표시할 수 있는데, lifetime에 변형을 일으키지 않는다.
단지 컴파일할 때, 프로그램 실행간 일어날 수 있는 에러를 사전 파악하기 위함이라고 보면 되겠다.
longest함수는 다음과 같이 수정해야한다.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  // ... same
}
x와 y가 'a 의 lifetime 만큼은 존재해야한다는 것을 의미하게 되었다.
Lifetime의 궁극적인 목적은, 다양한 parameter들과 함수의 return 값의 연결로 인해, memory-safe 한 연산을 보장하는 것이다.
- Struct
struct ImportantExcerpt<'a> {
  part: &'a str,
}
구조체에 reference 가 담겨있다면, 해당 구조체와 reference되는 변수는 'a 만큼은 살아있어야함을 표시한다.
Lifetime Elision Rule
(1) 각각의 parameter는 고유한 lifetime 을 갖는다.
(2) parameter가 1개면, 해당 lifetime이 return value에도 적용된다.
(3) parameter 중에
&self나&mut self일 경우,self의 lifetime이 return value에 적용된다.위의 세개의 규칙으로 lifetime이 지정되지 않는 값이 있을 경우에, compiler가 error가 있음을 이야기해준다.
- method
lifetime이 struct 타입을 구성하는 요소이기 때문에, method 정의시에 lifetime을 반드시 작성해주어야한다.
impl<'a> ImportantExcerpt<'a> {
  fn level(&self) -> i32 {
    3
  }
}
- static
'static 의 의미는 reference가 프로그램이 실행되는 전체 시간 동안 살아있을 수 있음을 의미한다.
모든 string literal들은 'static lifetime을 갖는다.
Test 작성하기
- #[test]를 표시하여 test 함수임을 나타낸다.
#[cfg(test)]
mod tests {
  #[test]
  fn exploration() {
    assert_eq!(2+2, 4);
  }
  
  #[test]
  fn another() {
    panic!("Make this test fail")
  }
}
cargo test
- #[cfg(test)]:- cargo test일 때만 실행하라는 의미- cargo build같은 경우에는 실행하지 않으므로, compile time 단축- cfg : configuration 
 
- assert!: true이면 PASS, false 이면 FAIL- assert_eq!: LEFT == RIGHT 이면 PASS
- assert_ne!: LEFT != RIGHT 이면 PASS
 
pub fn greeting(name: &str) -> String {
  String::from("Hello!")
}
#[cfg(test)]
mod tests {
  use super::*;
  
  #[test]
  fn greeting_contains_name() {
    let result = greeting("Carol");
    assert!(
      result.contains("Carol"),
      "Greeting did not contain name, value was `{}`", // 에러메시지를 확인하기 좋음.
      result
    );
  }
}
- #[should_panic]: panic이 일어나야 PASS
pub struct Guess {
  value: i32,
}
impl Guess {
  pub fn new(value: i32) -> Guess {
    if value < 1 {
      panic!("Guess value must be greater than or equal to 1, got {}.", value);
    } else if value > 100 {
      panic!("Guess value must be less than or equal to 100, got {}.", value);
    }
    
    Guess { value }
  }
}
#[cfg(test)]
mod tests {
  use super::*;
  #[test]
  #[should_panic(expected = "Guess value must be less than or equal to 100")]
  fn greater_than_100() {
    Guess::new(200);
  }
}
expected에 들어갈 것은 error message의 substring이면 된다.message 내용이 완전히 달라도 오류가 난다.
- Result<T, E>: panic하는 대신- Err를 return 한다.
#[cfg(test)]
mod tests {
  #[test]
  fn it_works() -> Result<(), String> {
    if 2+2 == 4 {
      Ok(())
    } else {
      Err(String::from("two plus two does not equal four"))
    }
  }
}
Test Control
- cargo test는 모든 테스트를 실행한다.
- 테스트는 parallel 하게 진행되므로, 파일을 생성하고 지우는 경우가 겹치다보면 의도치 않은 오류가 발생할 수 있다.- thread를 1개를 제한하려면, cargo test -- --test-threads=1와 같이 사용해야한다.
 
- thread를 1개를 제한하려면, 
- Test 코드에서 println!을 사용하더라도, PASS 했을 때 그 결과를 볼 수 없다.- 보고 싶다면, cargo test -- --show-output
 
- 보고 싶다면, 
- 모든 테스트를 실행하는 것은 많은 시간이 걸릴 수도 있으므로, 어떤 특정 분야의 함수만 실행하고 싶을 수 있다. 이 때는 함수의 이름 일부분을 전달하면서 테스트할 함수를 제한 할 수 있다.- cargo test {{subset_of_function_name}}
 
- 테스트 함수 중 굉장히 오랜 시간이 걸리는 함수가 있어서, 왠만하면 안 돌리는 테스트 코드를 만들 수 있다.- #[ignore]로- cargo test가 무시하고 지나갈 수 있다.
- cargo test -- --ignored로 ignore 한 테스트만 실행시킬 수 있다.
 
Test 조직화
- unit test : 좀 더 작은 분야, private interface 테스트
- integration test : 좀 더 큰 분야, public interface만 사용
1. unit test
src 폴더 안에 각 기능을 담고있는 파일 내부에 존재할 것이다.
각 파일마다 tests module 을 만들고(cfg(test) 명시),  test 함수가 담겨있으면 된다.
private 함수가 직접적으로 테스트 되는 것에 대해 여러가지 의견이 있지만, Rust에서는 private 함수를 테스트 할 수 있다.
pub fn add_two(a: i32) -> i32 {
  internal_adder(a,2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
  a + b
}
#[cfg(test)]
mod tests {
  use super::*;
  #[test]
  fn internal() {
    assert_eq!(4, internal_adder(2,2)); // *
  }
}
private한 internal_adder 함수를 테스트할 수 있음을 볼 수 있다.
2. integration test
src 폴더와 같은 위치에 tests 폴더를 생성한다. cargo가 이 폴더에 integration test 파일이 존재하는 것을 안다.
cargo는 tests 폴더 안에 있는 파일들을 각각의 crate로 취급하여 컴파일 한다.
// Filename : tests/lorem.rs
use adder; // (1)
#[test] // (2)
fn it_adds_two() {
  assert_eq!(4, adder::add_two(2)); // (3)
}
(1) tests 폴더 내부에 있는 파일은 개별 crate 이므로 use adder 를 통해 사용할 수 있는 scope로 가져온다.
(2) #[cfg(test)] 가 필요하지 않다.(tests 폴더 자체가 cargo test 할 때 참조되기 때문)
(3) public 한 함수에 대하여 test 한다.
cargo test --test lorem와 같이 실행하면 lorem.rs 파일에 대한 Integration test만 진행할 수 있다.
- setup이 필요할 때
tests 내부에 있는 *.rs 파일들은 각각의 crate로 취급되므로, test에 포함시키고 싶지 않다면, 폴더를 만들고, 그 안에 파일을 만들면 된다.
// Filename : tests/common/mod.rs
pub fn setup() {
  // setup code
}
// Filename: tests/lorem.rs
use adder;
mod common;
#[test]
fn it_adds_two() {
  common::setup();
  assert_eq!(4, adder::add_two(2));
}
