에러 핸들링



러스트의 에러는 회복 가능한(recoverable) 에러와 회복 불가능한(unrecoverable) 에러로 나눌 수 있음. 회복 가능한 에러는 프로그램을 바로 죽일 정도는 아닌, 런타임에 충분히 대응할 수 있는 에러를 의미하고, 회복 불가능한 에러는 그렇지 못한 에러임. 자바로 치면 체크드 익셉션을 써야 할 상황과 언체크드 인셉션을 써야 할 상황으로 생각하면 됨.


러스트에 익셉션을 없음. 대신 Result<T, E> 타입과 panic! 매크로가 존재함. Result<T, E> 는 회복 가능한 에러에, panic! 은 회복 불가능한 에러에 사용됨.



panic! 매크로


런타임에 에러가 발생했고 이 에러에 대해 해결할 수 있는 방법이 없는 경우, panic! 매크로를 사용함. 이 매크로는 프로그램 구동 중 에러가 발생했음을 알리고 프로그램을 즉각 종료함.


프로그램을 종료하는 과정에는 unwinding 이 있음. 이는 스택을 비우고 프로그램이 사용했던 메모리를 청소하는 작업임. 기본적으로 panic! 을 만나 프로그램이 종료되면 이 작업이 수행되는데, 이 작업은 내부적으로 상당히 많은 일을 필요로 함. 때문에 이 작업 없이 그냥 프로그램을 즉각 종료하도록 하는 방법이 마련되어 있음. 이렇게 하면 프로그램이 사용하던 메모리는 OS에 의해 청소되어야 함. 바이너리를 가능한 작게 만들고 싶을 경우 이 방법을 사용할 수도 있음. 방법은 Cargo.toml 파일에 다음 코드를 넣는 것.


[profile.release]

panic = 'abort'



패닉을 사용하는 방법은 간단함.


fn main() {
panic!("crash and burn");
}


위 코드는 실행되자마자 종료됨. 그리고 아래 메시지를 남김.


panic! 에 파라미터로 전달한 메시지가 포함됨. ("crash and burn")


그런데 복잡한 모듈 구조와 다양한 외부 라이브러리를 사용하면 위와 같은 메시지로는 에러가 발생한 정확한 위치를 찾기 어려움. 콜 스택 트레이스와 같은 정보가 필요함. 여기에 환경변수 RUST_BACKTRACE 를 사용하면 됨.


RUST_BACKTRACE 는 아래와 같이 사용(윈도우 기준).


fn main() {
let vec = vec![1, 2, 3];

vec[99];
}


위 코드를 그냥 컴파일 하면 아래와 같이 에러가 뜸.



set RUST_BACKTRACE=1 후에 컴파일 시도하면 사용하면 아래와 같이 에러가 뜸.


스택 트레이스의 라인넘버 10 을 보면 main.rs 가 나옴. 여기서부터 에러를 찾아가면 됨.







'Rust' 카테고리의 다른 글

제너릭 타입  (0) 2018.02.27
에러 핸들링(Result)  (0) 2018.02.26
컬렉션(해시맵)  (0) 2018.02.25
컬렉션(스트링)  (0) 2018.02.24
컬렉션(벡터)  (0) 2018.02.24

해시맵



모두에게 익숙한 그 해시맵 타입을 알아보기로 함. HashMap<K, V>(이하 해시맵) 은 std 라이브러리에서 제공하지만, 벡터만큼 사용 빈도가 높다고 여겨지진 않았는지 자동으로 로드되지 않음. use 를 사용해야 함. 해시맵은 제너릭 타입이 적용되며, 요소의 키와 값이 각각 같아야 함.



해시맵 생성


기본적으로 아래와 같이 생성하고 데이터를 넣음.


use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);


collect 메서드를 사용하면 두 벡터를 이용해서 맵으로 합칠 수 있음.


use std::collections::HashMap;

let teams = vec![String::from("Blud"), String::from("Yellow")];
let initial_scores = vec![10, 50];

let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();

teams 의 요소는 키로, initial_scores 의 요소는 값으로 1:1 매핑된 해시맵이 생성됨.


타입을 '<_, _>' 으로 지정했는데, 이는 collect 메서드는 다양한 타입의 맵을 리턴할 수 있고, 호출 결과로 어떤 타입이 올지 모르기 때문. '_' 를 사용하면 러스트는 벡터 안의 데이터 타입을 기반으로 해시맵의 타입을 추론할 수 있음.



해시맵과 오너십


해시맵의 키/값 insert 에도 오너십 규칙이 적용됨.


let key = String::from("color");
let value = String::from("blud");

let mut map = HashMap::new();
map.insert(key, value); // key, value 의 오너십 이동

변수 key, value 는 오너십을 상실함.


어떤 값의 레퍼런스(참조)를 해시맵에 넣는다면, 그 값은 해시맵으로 이동하지 않음. 레퍼런스가 가리키는 값은 해시맵이 유효한 이상 반드시 유효해야 함. 이는 라이프타임에 관한 것인데, 후에 자세히 다룸.



해시맵 값 접근


아래와 같이 get 메서드를 이용해서 해시맵에 저장된 값에 접근할 수 있음.


use std::collections::HashMap;

let key = String::from("color");
let value = String::from("blue");

let mut map = HashMap::new();
map.insert(key, value);

let blue = map.get(String::from("color"));


get 메서드는 Option<&V> 를 리턴함. 위의 변수 blue 는 Some(&String::from("color")) 가 됨. 만약 키에 해당하는 값이 없으면 get 메서드는 None 을 리턴함.


아래의 방법으로 맵을 순회하며 키와 값을 얻을 수 있음.


let mut map = HashMap::new();
map.insert(String::from("Blue"), 10);
map.insert(String::from("Yellow"), 50);

for (key, value) in &map {
println!("{}: {}", key, value);
}



해시맵 값 변경


해시맵은 하나의 키로 하나의 값만 할당할 수 있음. 만약 맵에 넣으려는 키에 해당하는 값이 이미 존재할 경우, 새로운 값으로 덮어쓸 것인지 기존 값을 유지할 것인지 결정해야 함.



값 덮어쓰기


아래와 같이 이미 존재하는 키로 값을 넣으면 그냥 새로운 값으로 덮어쓰고 기존 값은 사라짐.


map.insert(String::from("Blue"), 10);
map.insert(String::from("Blue"), 50);



값 유지하기


해시맵에 entry 메서드가 있음. 파라미터로 키 값을 받아서, 이 키에 해당하는 값이 이미 있는지 체크하고 그 결과를 리턴함. 리턴 타입은 enum 타입 Entry 임. 아래와 같이 사용.


map.insert(String::from("Blue"), 10);
map.entry(String::from("Yellow")).or_insert(50);
map.entry(String::from("Blue")).or_insert(50);


Entry 타입의 메서드 or_insert 는 Entry 의 값이 있을 경우 그 값을 리턴하고, 값이 없을 경우 파라미터로 주어진 값을 맵에 넣음. 위의 코드에서는 키 Yellow 는 값이 없으니 50 을 값으로 넣고, 키 Blue 는 값이 있으니 기존 값 10을 유지함.



기존 값을 기반으로 값 변경하기


기존 값을 그대로 덮어쓰거나 유지하지 않고, 기존 값을 이용해서 새로운 값을 만드는 경우도 있음.


let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}

println!("{:?}", map);


위 코드의 아웃풋은 다음과 같음.


{"wonderful": 1, "world": 2, "hello": 1}


코드의 수행 과정을 다음과 같음.


1. for word in text.split_whitespace() {..} 는 text 의 문자열을 공백 기준으로 잘라낸 결과를 가지고 순회.


2. let count = map.entry(word).or_insert(0); 는 map 에 해당 단어가 키로 존재하는지 체크(entry)하여 or_inert(0) 호출, 즉 값이 이미 있다면 있는 값을 리턴하고, 없다면 0 을 값으로 맵에 넣음.


3. *count += 1; 은 존재하는 값에 1 을 더함.


결국 이미 키가 존재하는 경우 count 를 1 더하고, 없으면 0 으로 초기화 함. or_insert 메서드는 값이 있는 경우 뮤터블 레퍼런스(*mut V) 를 리턴하기 때문에, 역참조(*) 로 값을 변경할 수 있음.



해싱 함수


기본적으로 러스트의 해시맵은 서비스 거부 공격(Denial of Service)을 방어할 수 있는, 암호화된 보안 해싱 함수를 사용함. 이 함수는 가장 빠른 해싱 함수는 아님. 성능을 어느 정도 희생하여 안정성을 취한 형태. 만약 다른 해싱 함수를 사용하길 원한다면, hasher 를 명시하여 원하는 해싱 함수를 지정할 수 있음. 이에 대한 자세한 내용은 나중에 다루기로 함.




'Rust' 카테고리의 다른 글

에러 핸들링(Result)  (0) 2018.02.26
에러 핸들링(panic)  (0) 2018.02.26
컬렉션(스트링)  (0) 2018.02.24
컬렉션(벡터)  (0) 2018.02.24
모듈(다른 모듈 접근)  (0) 2018.02.22

스트링(문자열)


러스트의 스트링은 좀 복잡함. 스트링이 컬렉션에 포함된 이유는, 스트링은 텍스트로 표현되는 바이트의 집합 +이 바이트를 다루는 유용한 메서드들로 구현되어 있음. 이런 성질은 본질적으로 컬렉션 타입의 것이라 볼 수 있음. 



스트링이란?


러스트에는 크게 두 가지 스트링 타입이 존재함: str 과 String.


str: 

러스트 코어 존재하는 유일한 스트링 타입임. 레퍼런스 대여 코드에 더 많이 보이기 때문에 보통 &str 형태로 보임. 스트링 슬라이스의 타입이기도 한 이 타입은 그 위치가 어디이든 UTF-8 로 인코딩된 모든 문자열을 참조함. 따라서, 프로그램의 바이너리에 위치하는 스트링 리터럴도 스트링 슬라이스라 할 수 있음.


String:

std 라이브러리에 의해 제공되는 스트링 타입임. 이 타입은 늘어날 수 있고(growable), 뮤터블이며, 소유할 수 있고, UTF-8 임. 


러스트 프로그래머가 '문자열(스트링)' 을 이야기할 때, 이는 대부분 &str 과 String 을 동시에 의미함. (둘 중 하나만 떼어놓고 이야기하지 않는다는 의미.) 두 스트링 타입 모두 러스트에서 중요하게 다루는 타입이며, 모두 UTF-8 인코딩을 취한다는 사실을 기억해야 함.


std 는 또한 OsString, OsStr, CString, CStr 과 같은 다른 스트링 타입도 제공함. 이들의 이름은 String/&str 처럼 *String/*str 의 형태를 가짐. 이런 타입들은 다른 인코딩 타입이거나 하는 등 각각의 특징을 가지고 있음. 여기서는 자세히 다루지 않으니, 궁금한 사항은 API 를 보길 바람.



새 스트링 생성


아래는 String 타입의 빈 스트링을 생성함.


let mut s = String::new();



아래는 초기값을 가진 String 타입의 스트링을 생성함.


let date = "initial contents";
let s = data.to_string();

let s = "initial contents".to_string(); // 위와 같은 결과.



아래는 초기값을 가진 또 다른 방법. 위의 방법과 결과적으로 똑같다.


let s = String::from("initial contents");



스트링 값 변경


문자열을 더하는 방법에는 메서드 push_str 과 push 이 있음.


let mut s = String::from("foo");
s.push_str("bar");


push_str 메서드는 파라미터로 스트링 슬라이스를 받음. 때문에 파라미터 원본의 오너십을 가지지 않음. 


let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(&s2);



push 메서드는 문자 하나를 파라미터로 받음.


let mut = String::from("lo");
s.push('l');



format! 매크로와 '+' 연산자를 이용한 문자열 이어붙이기


다른 언어에서 흔히 그렇듯, 러스트도 '+' 연산자를 이용한 문자열 붙이기가 가능함.


let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2 // 여기서 s1 은 '이동' 이 발생함. 변수 s1 은 이제 사용 불가.


s3 는 'Hello, world!' 가 됨. 그런데 주석에서 보다시피, s1 은 이제 버려지고, s2 은 여전히 유효함. 그 이유는, 여기 '+' 연산자는 add 메서드를 사용함. 아래는 그 시그니처.


fn add(self, s: &str) -> String {


이건 사실 std 라이브러리의 정확한 시그니처는 아님. 정확히는 제너릭이 사용되어 있음. 일단 여기서는 중요하지 않으니 패스.


여기서 눈여겨볼 점은 따로 있음. 이 메서드는 두 번째 파라미터로 &str 을 받아서 첫 번째 파라미터인 자기 자신, 즉 String 에 더함. '+' 연산자를 사용할 때 우린 &str 을 String 에 더하는 것이며, String 과 String 을 합칠 수는 없음. 그런데, 위의 예제에서 s2 은 String 임. 그리고 &s2 는 &String 이지, &str 이 아님. 그런데 add 메서드의 파라미터로 쓰일 수 있는 이유는?


컴파일러는 인자값으로 넘어온 &String 를 &str 으로 강제함. add 메서드를 호출하면 러스트는 deref coercion 을 사용하는데, 이는 &s2 를 &s2[...] 로 바꿈. deref coercion 에 대해서는 나중에 자세히 다루니 여기선 패스. 아무튼 그렇고, add 메서드는 파라미터 s 의 오너십을 취하지 않음. 때문에 변수 s2 는 add 후에도 여전히 유효함.


그리고, add 의 시그니처에서 self 는 오너십을 취함. let s3 = s1 + &s2; 은 얼핏 보기에 두 스트링을 복사하여 하나의 새로운 스트링을 생성하는 것처럼 보이지만, 사실은 s1 의 오너십을 취한 뒤 여기에 s2 의 내용을 복사하여 더하고(append), 오너십을 다시 리턴하는 것임. 다시 말해서, 보기와 달리 이 과정에서 많은 복사 작업이 발생하지 않음. add 의 구현 방식은 단순 복사보다 효율적으로 이루어져 있음.



더 많은 수의 스트링을 더할 수도 있음.


let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;


이런 작업이 복잡해질 경우, format! 매크로를 사용할 수도 있음.


let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2 ,s3);


위 두 예제에서 s 는 같은 값을 가짐. 그런데 format! 매크로는 오너십을 취하지 않기 때문에, s1, s2, s3 모두 유효함.



스트링 인덱싱


let s1 = String::from("hello");
let h = s1[0];


위 코드는 컴파일 에러를 일으킴. 즉 러스트 스트링은 인덱싱을 지원하지 않음. 이유를 알아보자면,


내부적으로 String 은 Vec<u8> 의 래퍼(wrapper)임. 


let len = String::from("Hola").len();


위 코드에서 len 은 4 가 됨. 이는 "Hola" 를 저장하고 있는 Vec 의 길이가 4바이트라는 의미임. UTF-8 에서 각 문자는 1바이트씩 차지함. 그렇다면 다음 코드는?


let len = String::from("Здравствуйте").len();


위 문자열의 첫 문자는 키릴 문자의 대문자 Ze 임. 숫자 3 이 아님. len 의 값은 12가 아닌 24인데, 이는 위 문자열이 UTF-8 에서 24바이트를 취했다는 의미임. 각 유니코드 스칼라 값이 2바이트씩 차지함. 이렇게 러스트 스트링의 인코딩인 UTF-8 에서 스트링의 인덱스가 언제나 하나의 문자의 위치라는 것을 보장할 수 없음. 


let hello = "Здравствуйте";
let answer = &hello[0];


answer 의 값은 무엇이 되어야 함? 첫 번째 문자? UTF-8 에서 첫 번째 문자는 208 바이트임. 두 번째는 151임. 그리고 이미 알아봤듯 이 문자열은 문자 하나가 2바이트를 차지함. 때문에 첫 번째 바이트인 208이나, 두 번째 바이트인 151이 각자 단독으로는 유효한 문자가 되지 못함. 때문에 인덱스 0 으로 이 문자를 리턴할 수가 없음. 그래도 어쨌든 첫 번째 인덱스이니 일단 208 을 리턴하도록 한다면, 유저는 첫 번째 인덱싱으로 나온 바이트이니 이 값이 첫 번째 문자의 바이트라고 잘못 인식하게 될 여지가 있고, 이는 유저가 원하는 데이터가 아님. 이런 문제로 인해 러스트는 이런 방식의 스트링 인덱싱 접근을 허용하지 않음.



바이트와 스칼라 값 그리고 문자 집합 


데바나가리 스크립트로 쓰려진 힌디 단어 'नमस्ते' 은 Vec 의 u8 값으로 다음과 같이 저장됨.


[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]


이건 18바이트이며 컴퓨터가 최종적으로 데이터를 저장하는 방식임. 이걸 유니코드 스칼라 값으로 본다면 다음과 같음. 이는 러스트에서 char 타입 값임.


['न', 'म', 'स', '्', 'त', 'े']


이렇게 여섯 개의 char 값이 됨. 그런데 4, 6번째 값은 문자가 아님: 이건 diacritic 이라는, 스스로 문자화 될 수 없는 값임. 결국 이 값들을 문자 집합으로 본다면, 사람이 보기에 다음 4개의 문자가 하나의 단어로 되어버림. 4, 6번째 값은 버려지고, 남은 값들로 단어가 구성되어 원래 단어와 값이 달라짐.


["न", "म", "स्", "ते"]


러스트는 프로그램이 어떤 언어의 문자든 적절하게 해석할 수 있도록, 컴퓨터가 저장하는 원천 스트링 데이터를 해석하는 여러가지 방법을 제공함.


러스트가 String 에 인덱싱 접근을 허용하지 않는 마지막 이유는, 인덱싱 연산은 언제나 정적 시간(O1(1)) 을 취해야 한다고 여겨지기 때문임. 러스트는 문자열 안의 유효한 문자의 수를 도출하기 위해 문자열의 처음부터 끝까지 순회해야 하기 때문에 String 으로 정적 시간 연산 성능을 보장할 수 없음.



스트링 슬라이싱


문자열 인덱싱 접근은 별로 좋은 방법이 아님. 왜냐하면 리턴 타입이 뭐가 되어야 할지 분명하지 않기 때문. 바이트인지, 문자인지, 문자열인지... 때문에 러스트는 이에 대하여 범위를 지정한 슬라이스를 사용하도록 권함.


let hello = "Здравствуйте";

let s = &hello[0..4];


s 는 &str 타입이 되고, hello 의 처음 4바이트를 참조함. 위 문자가 각 2바이트를 취한다고 했으니, s 는 처음 두 문자를 참조하게 됨.


&hello[0..1] 를 참조한다면? 이건 panic 이 발생함. 이건 무효한 인덱싱 접근과 마찬가지임. 이런 결과가 발생할 수 있으므로 스트링 슬라이싱을 사용함에 주의가 필요함.



문자열 순회 메서드


아래 방법으로 문자열의 각 문자를 순회할 수 있음. char 메서드는 문자열의 각 문자를 나누어줌.


for c in "नमस्ते".chars() {
println!("{}", c);
}


아래는 바이트. bytes 메서드는 문자열의 각 문자를 바이트로 나누어줌.


for c in "नमस्ते".bytes() {
println!("{}", c);
}



아무튼 스트링은 복잡함.




'Rust' 카테고리의 다른 글

에러 핸들링(panic)  (0) 2018.02.26
컬렉션(해시맵)  (0) 2018.02.25
컬렉션(벡터)  (0) 2018.02.24
모듈(다른 모듈 접근)  (0) 2018.02.22
모듈(pub)  (0) 2018.02.22

벡터



기본적인 컬렉션 타입 Vec<T>(이하 벡터) 에 대해 알아봄. 벡터는 std 에 포함된 컬렉션 타입임. 따로 스코프로 로드하지 않아도 사용할 수 있음. 한 벡터 인스턴스에는 오직 같은 타입의 값만 저장 가능함.



벡터 인스턴스 생성 방법


기본적인, 빈 벡터 인스턴스는 아래와 같이 생성. 제너릭에 타입을 지정함.


let v: Vec<i32> = Vec::new();


아래는 벡터 인스턴스 생성과 함께 초기값을 설정. 초기값의 타입에 따라 벡터의 제너릭 타입이 추론됨.


let v = vec![1, 2, 3];



벡터 변경


기본적으로 아래와 같이 벡터에 데이터를 넣음. 벡터에 값을 넣거나 빼려면 역시 뮤터블이어야 함


let mut v: Vec<i32> = Vec::new();

v.push(1);
v.push(2);
v.push(3);



러스트의 다른 인스턴스와 마찬가지로, 벡터 또한 스코프를 벗어나면 메모리에서 해제되며, 벡터가 가지고 있던 값들도 사라짐.


{
let v = vec![1, 2, 3];
// do something..
}



벡터의 요소 읽기


아래 코드에 벡터의 요소를 읽은 두 가지 다른 방법이 있음.


let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);


첫 번째는 인덱스를 직접 접근, 두 번째는 get 메서드를 사용. 두 방법의 차이는 다음과 같음.


let v = vec![1, 2, 3, 4, 5];
let does_not_exist: &i32 = &v[100];
let does_not_exist: Option<&i32> = v.get(100);


위처럼 범위를 넘어선 인덱스를 지정하면 첫 번째 방법은 panic 이 발생함. 반면 두 번째 방법은 Option<T> 타입인 None 을 리턴하여, 해당 인덱스에 요소가 없을 경우에 실행할 코드를 지정할 수 있음. 인덱스에 요소가 없을 때 치명적인 오류로 보고 프로그램을 바로 종료한다면 첫 번째 방법을, 요소가 없는 경우가 발생할 수 있고 이에 대해 다른 대응을 해야 한다면 두 번째 방법을 취하면 됨.



무효한 레퍼런스


벡터의 요소를 읽는 데에도 레퍼런스 대여 규칙이 적용됨. 


let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0]; // 임뮤터블 대여 발생

v.push(6); // 뮤터블 대여 발생

위 코드는 컴파일 에러를 일으킴.


벡터에 충분한 여유 공간이 없다면, 벡터에 새로운 값을 넣는 push 메서드는 새로운 메모리 할당을 요구하고 기존 요소를 새로운 공간에 복사하는 작업을 수행할 수 있음. 그런데 위의 변수 first 가 벡터의 기존 첫 번째 요소(의 주소)를 참조하고 있는데 push 메서드로 인해 그 요소의 메모리가 해제되는 일이 생기면, 변수 first 의 참조는 무효한 것이 되어버림. 이런 현상을 방지하고자 컴파일러는 앞서 레퍼런스 대여 규칙을 체크함.



벡터 값 순회하기


아래는 기본적인 방법.


let v = vec![100, 32, 57];
for i in v {
println!("{}", i);
}



아래는 벡터 안의 요소값을 변경할 경우.


let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}

각각의 값에 50을 더함.


'*' 는 역참조(디레퍼런스) 연산자임. 참조(&) 로 요소를 읽어왔기에, 안의 값을 변경하려면 다시 역참조를 수행.



enum 을 이용한 다양한 타입 다루기


벡터 인스턴스는 한번에 한 가지 타입으로 제한되기 때문에, 여러 타입을 하나의 벡터에 넣으려면 enum 을 사용하는 방법이 있음.


enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}

let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text("blut".to_string()),
SpreadsheetCell::Float(10.12),
];


러스트가 이렇게 벡터의 타입을 미리 지정하도록 하는 이유는, 벡터가 요소를 저장하는 데에 필요한 메모리를 컴파일 타임에 알 수 있게 하기 위함임. 그리고 벡터에 아무 타입이나 담을 수 있게 되면, 하나의 벡터 인스턴스를 다루면서 안에 섞여 있는 요소의 다양성으로 인해 오류가 발생할 위험성이 높아지기 마련임.


하지만 enum 은 완전하지 않음. 때로는 구현하려는 프로그램이 런타임에 어떤 타입의 데이터를 받게 될지 알 수 없는 경우가 있을 이럴 때 enum 은 유효하지 못함. 이럴 땐 '특성(트레잇)' 을 사용해야 하는데, 이건 나중에 다루기로 함.




'Rust' 카테고리의 다른 글

컬렉션(해시맵)  (0) 2018.02.25
컬렉션(스트링)  (0) 2018.02.24
모듈(다른 모듈 접근)  (0) 2018.02.22
모듈(pub)  (0) 2018.02.22
모듈  (0) 2018.02.22

다른 모듈 접근하기



모듈이든 함수든, '::' 를 통해 계층 이동을 수행함.


pub mod a {
pub mod series {
pub mod of {
pub fn nested_modules() {}
}
}
}

a::series::of::nested_modules();



use 키워드 사용하기


use 키워드를 사용하면 접근을 보다 편하게 할 수 있음.


use a::series::of;

of::nested_modules();


use a::series::of; 는 of 모듈을 사용하겠다는 의미임. 이를 통해 of 모듈에 접근할 수 있음.


use 키워드는 정확히 지정된 그 항목만을 가지고 옴: 그 모듈의 자식까지 가지고 오진 않기 때문에, 아래와 같이 사용할 수는 없음.


use a::series;

of::nested_modules(); // 에러!



of 모듈은 series 모듈의 자식 모듈이지만, use 로 지정된 것은 어디까지나 series 모듈임. 때문에 이 경우 아래와 같이 사용해야 함.


use a::series;

series::of::nested_modules(); // OK



특정 함수만을 가지고 올 수도 있음.


use a::series::of::nested_modules;

nested_modules(); // OK



enum 도 모듈과 같은 하나의 형태를 가지기 때문에, enum 타입을 가지고 오는 일도 가능함. 그것도, enum 타입의 특정 항목만을 가지고 올 수도 있음.


enum TrafficLight {
Red,
Yellow,
Green,
}

use TrafficLight::{Red, Yellow};

fn main() {
let red = Red;
let yellow = Yellow;
let green = TrafficLight::Green;
}


같은 TrafficLight 타입이라도, Red, Yellow 는 use 를 통해 불러왔기에 바로 접근 가능하지만, Green 의 경우 TrafficLight::Green 의 방식으로 접근해야 함.


모든 enum 항목을 나열하는 일은 번거롭기에, 아래와 같은 처리도 가능.


use TrafficLight::*;

이제 TrafficLight 의 모든 항목을 바로 접근할 수 있음.



super 키워드 사용하기


다른 스코프의 항목 호출 시, 맨 앞에 '::' 을 사용함은 루트에서부터 접근함을 의미함.


::a::series::of::nested_modules();    // 루트 경로의 a 모듈에서부터 접근



파일 시스템에서 루트로 접근할 때와 같이, 루트를 통해 모든 모듈의 경로를 찾아갈 수 있지만, 때로는 상대경로가 편함. super 키워드를 사용하면 바로 위의 부모 모듈에 접근할 수 있음.


super::super::some_function();    // 2단계 상위 모듈의 some_function 호출




'Rust' 카테고리의 다른 글

컬렉션(스트링)  (0) 2018.02.24
컬렉션(벡터)  (0) 2018.02.24
모듈(pub)  (0) 2018.02.22
모듈  (0) 2018.02.22
Enum(if let)  (0) 2018.02.21

Public - Private



자바의 접근제한자처럼, 러스트에도 접근제한자가 존재함. 기본적으로 러스트의 모든 접근제한자는 private 임. pub 키워드를 사용하여 이를 퍼블릭으로 바꿀 수 있음.


러스트의 privacy 규칙은 다음과 같음.

  • 어떤 항목이 public 이면, 이 항목의 어떠한 부모 모듈을 통해서든 이 항목에 접근 가능.
  • 어떤 항목이 private 이면, 오직 이 항목의 직계 부모 모듈, 혹은 직계 부모 모듈의 자식 모듈(형제 or 형제의 자식) 모두 이 항목에 접근 가능.
여기서 혼동하지 말 것! 항목이 모듈이면 부모 모듈은 자신이 속한 모듈이지만, 항목이 함수이면 부모 모듈은 함수가 속한(자신이 정의된) 모듈이 됨.


'Rust' 카테고리의 다른 글

컬렉션(벡터)  (0) 2018.02.24
모듈(다른 모듈 접근)  (0) 2018.02.22
모듈  (0) 2018.02.22
Enum(if let)  (0) 2018.02.21
Enum(match)  (0) 2018.02.21

모듈의 기본



아래 cargo 명령어는 실행용 프로젝트를 생성함


cargo new communicator --bin 


아래와 같이 --bin 을 빼면 라이브러리 프로젝트를 생성함


cargo new communicator


실행용 프로젝트에 main.rs 가 존재한다면, 라이브러리 프로젝트에는 lib.rs 가 존재함 각각 각 프로젝트의 메인 파일임.


아래 코드는 network 라는 모듈을 생성함.


mod network {
fn connect() {
}
}


같은 파일(현재는 lib.rs)에 몇 개의 모듈이든 둘 수 있음.


mod network {
fn connect() {
}
}

mod client {
fn connect() {
}
}


당연하게도, 각 connect 함수는 서로 다른 범위에 존재하기에, 충돌은 없음. 각각 network::connect() / client::connect() 로 호출됨.


한 모듈은 자신의 서브모듈(자식모듈)을 얼마든지 가질 수 있음.


mod network {
fn connect() {
}

mod client {
fn connect() {
}
}
}


이제 connect 함수는 각각 network::connect() / network::client::connect() 로 호출됨.



다른 파일로 모듈 이동하기


모듈 파일 규칙은 다음과 같음.

  • 분리하려는 모듈, foo 가 있고, foo 에 서브모듈이 없을 경우, foo 가 선언된 같은 경로에 foo.rs 파일을 만들면 됨.
  • 분리하려는 모듈, foo 가 있고, foo 에 서브모듈이 있을 경우, foo 디렉토리를 만들고 mod.rs 파일을 만들면 됨. foo 모듈 본문은 mod.rs 에 작성.


아래 코드가 있음(lib.rs). 여기서 client 모듈을 분리할 것.


mod client {
fn connect() {
}
}

mod network {
fn connect() {
}

mod server {
fn connect() {
}
}
}


client 는 선언부만 남김. 


mod client;

mod network {
fn connect() {
}

mod server {
fn connect() {
}
}
}


그리고 같은 경로(client 가 lib.rs 에 선언되었으니 root 경로)에 client.rs 파일을 만들어 여기에 본문을 작성


fn connect() {
}


모듈 파일에 다시 mod 를 쓸 필요는 없음. mod 를 또 쓸 경우, 여기서 또다른 서브모듈을 선언하는 셈이 됨.


그리고 network 모듈도 분리하자면, lib.rs 를 다음과 같이 수정.


mod client;

mod network;


network 모듈은 server 라는 서브모듈을 가지고 있음. 규칙에 따라, network 라는 디렉토리를 만들어 그 안에 mod.rs 파일을 만들고 network 모듈의 본문을 그대로 복사. network/mod.rs 파일 내용은 다음과 같음.


fn connect() {
}

mod server {
fn connect() {
}
}


여기서 또 server 모듈을 분리자면, network/mod.rs 파일을 다음과 같이 수정.


fn connect() {
}

mod server;


그리고 mod.rs 의 경로(network 디렉토리) 에 server.rs 파일을 만들고 여기에 본문을 그대로 복사. network/server.rs 파일 내용은 다음과 같음.


fn connect() {
}


클리어.


이제 모듈 구조는 다음과 같음.



모듈의 파일 구조는 다음과 같음.




'Rust' 카테고리의 다른 글

모듈(다른 모듈 접근)  (0) 2018.02.22
모듈(pub)  (0) 2018.02.22
Enum(if let)  (0) 2018.02.21
Enum(match)  (0) 2018.02.21
Enum(Option<T>)  (0) 2018.02.21

Using if let with Enum



match 가 너무 장황하게 느껴질 때, if let 은 보다 간단한 옵션이 됨.


let some_u8_value = Some(0u8);

match some_u8_value {
Some(3) => println!("three"),
_ => (),
}


위 match 는 아래 if let 과 동일하게 작동함.


if let Some(3) = some_u8_value {
println!("three");
}


else 도 가능.


if let Some(3) = some_u8_value {
println!("three");
} else {
println!("not three");
}


if let 은 match 와 비슷함. 그러나, 그 코드는 match 보다 짧고, 형식적인 코드가 보다 적음. 그리고 중요한 것: if let 은 비교 패턴을 원하는 대로 정할 수 있음. match 는 반드시 enum 타입의 모든 패턴을 다루어야 하지만, if let 은 그런 거 없음. 비교하고 싶은 패턴이 하나면 하나, 둘이면 둘, 원하는 대로 가능함. 비교하고 싶은 값이 적을 때는 확실히 if let 이 편할 것임. 그러나 여기에는 match 에서의 안정성이 없다는 것을 유념해야 함.




'Rust' 카테고리의 다른 글

모듈(pub)  (0) 2018.02.22
모듈  (0) 2018.02.22
Enum(match)  (0) 2018.02.21
Enum(Option<T>)  (0) 2018.02.21
Enum  (0) 2018.02.21

Using Match with Enum



match 는 다른 언어의 switch 와 비슷함. 대상을 지정해놓고 대상의 값이 될 수 있는 목록을 나열하고 매칭되는 값에 해당하는 코드를 실행함. 그 대상은 리리터럴, 변수, 와일드카드 등 여러가지가 될 수 있는데, 여기서는 enum 과 함께 사용하는 법을 다룸.


enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}


위의 match 는 value_in_cents 의 파라미터로 넘겨받은 enum 에 매칭되는 값을 값을 반환함. if 와 비슷하지만, if 의 조건절에는 Boolean 값만이 올 수 있지만 match 에는 어떠한 타입도 사용 가능하다는 차이가 있음.


문법적으로, match 에 나열된 비교 대상을 패턴이라 칭함. 비교는 작성된 순서(위에서 아래)로 이루어지며, 패턴 매칭이 발생하면 나머지 패턴 비교는 수행하지 않음. 


위의 Penny 에서 처럼, 실행될 코드가 두 줄 이상이면 중괄호를 써서 줄을 나눔.


match 는 철저함. enum 타입으로 비교할 때, 이 타입의 모든 패턴을 다루어야 함. 해당 타입에 빼놓은 패턴이 있다면 이는 컴파일 에러를 일으킴. 아래는 그 예:


enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,

}
}

이처럼 match 로 Coin 타입을 다루면서 Quarter 를 빼놓으면 컴파일 에러!



플레이스홀더 _


match 는 편의상 플레이스홀더를 제공함. 이는 다른 언어의 switch 에 있는 default 라 보면 됨. 패턴 비교를 하다가 '여기부턴 나머지 모두 똑같이' 를 구현하고 싶을 때 사용. 패턴에 '_' 를 사용하면 됨.


let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("two"),
5 => println!("five"),
7 => println!("seven"),
_ => ();
}


1 - 3 - 5 - 7 까지 비교하여 매칭되지 않으면 모두 () 를 반환.



값을 지닌 enum 패턴


enum 은 안에 자신의 값을 가질 수 있다고 했음. 아래 코드는 이 값이 match 와 어떤식으로 사용될 수 있는지 보여줌.


enum UsState {
Alabama,
Alaska,
}

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
}
}
}


Coin::Quarter 패턴에서 보듯, enum 의 값을 실행 코드에서 사용할 수 있음. Coin::Quarter 가 파라미터로 주어지면, 각 패턴을 위에서부터 순서대로 비교하다가 Quarter 에서 매칭이 발생하고, Quarter 의 값, UsState 가 패턴 안의 state 에 바인딩 되고, 이 state 를 매칭 코드에서 사용할 수 있게 됨.



Option<T>


이전에 다룬 Option<T> 에서 Some 값을 사용할 수 있음. 


fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);


Coin::Quarter 에서와 같음. plus_one 함수에서 Some 값이 i 에 바인딩되어 + 1 한 값이 반환됨. 물론 값 그대로가 아닌, Some(i + 1) 의 형태로. None 의 경우 None 이 반환됨. None 도 Option<T> 타입이기 때문에 반환될 수 있음.







'Rust' 카테고리의 다른 글

모듈  (0) 2018.02.22
Enum(if let)  (0) 2018.02.21
Enum(Option<T>)  (0) 2018.02.21
Enum  (0) 2018.02.21
구조체(메서드)  (0) 2018.02.20

Option<T>


러스트에는 기본적으로 null 이 없음. 때문에 널포인터 참조와 같은 에러는 발생하지 않음. 하지만, '값이 없음'을 표현하는 방법이 필요할 때가 있음. 그럴 때 Option<T> 이라는 enum 타입을 사용할 수 있음.


Option<T> 은 표준 라이브러리에 아래와 같이 정의되어 있음


enum Option<T> {
Some(T),
None,
}

T 는 제너릭.


Some 은 값이 있음을 나타내며, None 은 값이 없음을 나타냄. 이 두 '상태' 를 가지고 null 체크와 같은 일을 수행할 수 있음. Option 은 기본적으로 스코프에 포함되어, 명시적으로 로드하지 않아도 사용할 수 있음.


Option<T> 은 아래와 같이 선언됨.


let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;


타입 추론에 따라 some_number 는 Option<i32> 타입, some_string 은 Option<&str> 이 됨. 그런데 absent_number 에만 타입을 지정한 이유는, None 은 값을 가지지 않기 때문에 타입을 지정하지 않으면 absent_number 의 타입을 추론할 수 없어 컴파일 에러가 발생함. 때문에 None 은 타입이 필요함.


말했듯이, Some 은 값이 있는 상태를, None 은 null 을 의미함. 하지만, 그렇다고 해서 Some 을 값 자체로 사용할 수는 없음. Some 은 어쨌든 Option 이라는 enum 타입일 뿐임. 아래와 같은 연산은 컴파일 에러를 일으킴. 인티저 타입에 Option 타입을 더할 수는 없기 때문. Some 이 값이 아니라는 걸 기억해야 함.


let x = 5;
let y = Some(5);

let sum = x + y;


5 라는 값이 있을 때, 이 값은 분명히 존재하는 값이며, 러스트는 이 값이 유효한 값인지 아닌지 체크할 필요가 없음. 그런데 Option 타입의 경우, 값이 존재할 수도(Some), 존재하지 않을 수도(None) 있음. 때문에 Option 을 사용하기 위해서는 먼저 이게 Some 인지 None 인지 체크할 필요가 있음. 그리고 우리는 Some 일 경우의 동작과, None 일 경우의 동작을 모두 코드로 작성할 수 있음. 이는 match 를 이용함.




'Rust' 카테고리의 다른 글

Enum(if let)  (0) 2018.02.21
Enum(match)  (0) 2018.02.21
Enum  (0) 2018.02.21
구조체(메서드)  (0) 2018.02.20
구조체(활용)  (0) 2018.02.20

+ Recent posts