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 이면 FAILassert_eq!
: LEFT == RIGHT 이면 PASSassert_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));
}