이번 주에 배운 내용을 가지고 작성된 Rust의 샘플 코드를 분석해 보겠다.
impl fmt::Display for Record {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {} {}", self.score, self.last_name, self.first_name)
}
}
이 코드는 Rust의 fmt::Display 트레이트를 Record 구조체에 대해 구현하고 있다. fmt::Display 트레이트는 오브젝트를 사용자 친화적인 형태로 문자열로 표현하기 위해 사용되는 트레이트다. 구현된 fmt 메서드를 이용해서 오브젝트를 어떻게 출력할지 지정한다.
-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result: 이것은fmt::Display트레이트에 정의된 메서드 시그니처다. 이 함수를 구현해서Record오브젝트를 어떻게 문자열로 변환할 것인지 정의한다.&self: 현재Record오브젝트에 대한 레퍼런스다.f: &mut fmt::Formatter: 출력 형식을 제어하는데 사용되는Formatter오브젝트에 대한 mutable 레퍼런스다.fmt::Result: 출력이 성공했는지 실패했는지를 나타내는 타입이다. 이것은Result<(), fmt::Error>의 type alias다.
-
write!(f, "{} {} {}", self.score, self.last_name, self.first_name): 이 부분은write!매크로를 사용해Formatter에 데이터를 쓴다.{}는 플레이스홀더로,self.score,self.last_name,self.first_name값들이 차례로 들어가게 된다.
간단한 예를 들어보자. Record 오브젝트가 { score: 100, last_name: "Kim", first_name: "John" } 이라고 하면, fmt 메서드는 이 오브젝트를 "100 Kim John"이라는 문자열로 변환할 것이다. 이렇게 하면 println!("{}", record); 같은 코드를 사용할 때, 원하는 형식으로 Record 오브젝트가 출력될 것이다.
매크로 사용법
!를 사용하는 매크로는 Rust에서 코드를 더 효율적으로 작성하거나 반복을 줄이는 데 도움을 준다. ! 기호는 Rust에서 해당 이름의 매크로라는 것을 나타내며, 일반 함수와는 다르게 작동한다. 매크로는 주로 코드의 일부를 인자로 받아 컴파일 타임에 그 코드를 확장시킨다.
사용법
-
println! 매크로: 문자열을 콘솔에 출력할 때 자주 사용한다.
println!("Hello, world!"); -
vec! 매크로: Vec 타입의 벡터를 쉽게 생성할 수 있다.
let my_vec = vec![1, 2, 3]; -
assert_eq! 매크로: 테스트에서 두 값이 같은지 확인한다.
assert_eq!(2, 1+1); -
macro_rules!: 사용자가 직접 매크로를 정의할 수 있다.
macro_rules! add { ($a:expr, $b:expr) => ($a + $b); } let sum = add!(3, 4); // sum은 7이 된다.
호출 방법
- 대부분의 매크로는 함수처럼 호출한다. 즉,
매크로이름!(인자1, 인자2, ...) - 일부 매크로는 중괄호
{}나 대괄호[]등을 사용해 호출할 수 있다. 예:vec![1, 2, 3]
간단히 말해 !가 붙은 이름은 주로 매크로이며, 그 뒤에 오는 괄호나 대괄호, 중괄호 안에 들어가는 인자는 매크로가 처리하여 일종의 '코드 템플릿'을 생성한다.
write! 매크로는 Rust의 표준 라이브러리에서 제공하는 매크로로, 주로 출력을 위해 사용된다. 이 매크로는 std::fmt::Write 트레이트를 구현한 어떤 오브젝트에도 작동한다. 예를 들어, 문자열을 파일이나 콘솔, 메모리 등에 출력할 때 이 매크로를 사용할 수 있다.
용도
-
파일에 쓰기:
std::fs::File오브젝트와 같이std::io::Write트레이트를 구현한 타입에 데이터를 쓸 때 사용한다.use std::fs::File; use std::io::Write; let mut file = File::create("output.txt").unwrap(); write!(&mut file, "Hello, world!").unwrap(); -
문자열에 쓰기:
String을 생성하거나 수정할 때String오브젝트에 문자열을 추가한다.use std::fmt::Write; let mut s = String::new(); write!(&mut s, "The answer is {}.", 42).unwrap(); -
커스텀 출력 형식:
std::fmt::Formatter와 같이std::fmt::Write트레이트를 구현한 타입에 특별한 형식으로 출력할 때 사용한다.
write!와 println!의 차이
write!는 명시적으로 어디에 쓸지 지정해야 하고,Result타입을 반환하여 성공 여부를 확인할 수 있다.println!은 표준 출력(콘솔)에만 데이터를 쓰고, 명시적인 오류 처리를 제공하지 않는다.
간단히 말해, write! 매크로는 출력 대상과 에러 처리가 필요할 때 사용되며, 다양한 출력 대상에 유연하게 대응할 수 있다.
결론적으로 위의 코드는 Record 타입에 대해 std::fmt::Display 트레이트를 구현하고 있다. std::fmt::Display는 오브젝트를 사용자 친화적인 형태로 출력할 수 있게 하는 트레이트다.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result 함수는 이 트레이트의 구현에서 필수로 정의해야 하는 함수이다. 이 함수는 Record 타입의 인스턴스를 어떻게 출력할지 정의한다.
&self는 현재Record오브젝트의 레퍼런스다.f: &mut fmt::Formatter는 출력을 위한 포매터다.fmt::Result는 출력이 성공했는지 실패했는지를 나타내는Result타입을 반환한다.
write!(f, "{} {} {}", self.score, self.last_name, self.first_name) 부분은 fmt::Formatter 오브젝트인 f에 self.score, self.last_name, self.first_name을 순서대로 쓴다. 이때 {}는 해당 값들이 들어갈 위치를 나타낸다.
즉, 이 코드는 Record 오브젝트를 출력할 때 score, last_name, first_name 필드를 이 순서대로 공백으로 구분하여 출력하도록 정의하고 있다.
let args: Vec<String> = env::args().collect();
env는 Rust의 표준 라이브러리의 std::env 모듈을 의미한다. 이 모듈은 환경 변수나 명령행 인수, 현재 프로세스의 작업 디렉토리와 같은 운영 체제의 환경과 관련된 함수와 타입을 제공한다.
let args: Vec<String> = env::args().collect(); 이 코드는 다음과 같은 일을 한다:
-
env::args()함수는 현재 프로그램에 전달된 명령행 인수를 반환하는 반복자를 생성한다. 이 반복자는String타입의 인수를 순회한다. -
collect()함수는 이 반복자를Vec<String>형태로 수집한다.
결과적으로 args 변수는 프로그램에 전달된 모든 명령행 인수를 Vec<String> 형태로 가지고 있게 된다. 이를 통해 프로그램은 사용자가 명령행에서 어떤 인수를 주었는지 알 수 있고, 이에 따라 동작을 변경할 수 있다.
let words: Vec<&str> = line.split_whitespace().collect();
let words: Vec<&str> = line.split_whitespace().collect(); 이 코드는 다음과 같은 일을 한다:
-
line.split_whitespace()은 문자열line을 공백 문자(스페이스, 탭, 줄 바꿈 등)를 기준으로 나누는 반복자를 생성한다. 여기서line은 문자열 타입이어야 한다. -
collect()함수는 이 반복자의 모든 원소를 수집하여 지정된 컬렉션 타입(Vec<&str>여기서는)에 넣는다.
결과적으로 words 변수는 line 문자열을 공백으로 나눈 단어들을 가지고 있는 Vec<&str> 타입의 벡터가 된다. 예를 들어, line이 "hello world"이면, words는 ["hello", "world"]라는 벡터를 가지게 된다. 이렇게 하면 문자열을 공백으로 쉽게 나눌 수 있다.
Vec<&str>는 벡터(Vec) 타입이고, 그 안에 들어가는 원소는 &str 타입이다.
-
Vec<T>:Vec은 벡터라고 불리며, Rust의 표준 라이브러리에서 제공하는 동적 배열이다.T는 그 안에 들어갈 원소의 타입을 나타낸다. 즉,Vec<i32>는i32타입의 원소를 가질 수 있는 벡터를 의미한다. -
&str: 이것은 문자열 슬라이스(string slice)를 나타내는 타입이다. 문자열 슬라이스는 문자열 데이터의 참조와 길이 정보를 담고 있다. 예를 들어,"hello"라는String객체가 있을 때, 이를 참조하는&str은 그 데이터의 주소와 길이(5)를 갖게 된다.
따라서, Vec<&str>은 문자열 슬라이스(&str)를 원소로 가지는 벡터를 의미한다. 이렇게 되면 여러 개의 문자열 슬라이스를 동적으로 저장하고 관리할 수 있다. 예를 들어, ["hello", "world"]와 같이 문자열 슬라이스 여러 개를 하나의 Vec<&str>에 넣을 수 있다.
let score: i32 = match words[2].parse() {
Ok(n) if n >= 0 && n <= 100 => n,
_ => continue,
};
이 코드는 words[2] 즉, 두번째 인자인 score에 해당하는 인자에 들어있는 문자열을 정수(i32)로 변환하려고 시도한다. 변환에 성공하면 그 값을 score라는 변수에 저장한다. 여기에 몇 가지 조건이 추가되어 있다.
words[2].parse()는 문자열을i32타입으로 변환하려고 시도한다.Ok(n) if n >= 0 && n <= 100 => n,: 만약 변환에 성공(Ok(n))하고 그 값(n)이 0 이상 100 이하이면, 그 값을n으로 반환한다._ => continue,: 위의 조건에 맞지 않으면 (_는 모든 나머지 경우를 의미),continue문을 실행하여 현재 반복을 건너뛴다.
결론적으로, 이 코드는 words[2]에 들어있는 문자열을 0 이상 100 이하의 i32 정수로 안전하게 변환하려고 시도한다. 만약 변환에 실패하거나 값이 범위 밖이면, 현재 반복을 건너뛴다(continue).
이 코드는 Rust의 match 표현식과 패턴 매칭을 사용하고 있다. match는 다른 언어의 switch-case와 유사한 역할을 한다. 코드에서 words[2].parse() 부분은 문자열을 정수(i32)로 변환하려고 시도한다. 이 parse 메서드는 Result 타입을 반환하며, 변환에 성공하면 Ok(T)를, 실패하면 Err(E)를 반환한다.
-
Ok(n) if n >= 0 && n <= 100 => n,: 이 부분은 조건이 맞으면 실행되는 패턴이다.Ok(n)은parse메서드가 성공적으로 값을 파싱했을 때 반환되는Result타입의 열거자이다. 이 열거자에 담긴 값은n에 바인딩된다.if n >= 0 && n <= 100는 그 값n이 0과 100 사이인지 확인하는 조건이다. 만약 이 조건이 참이면n을 반환한다. -
_ => continue,: 이 부분은 위의 조건이 아닌 모든 경우를 처리한다._는 와일드카드 패턴으로, 어떤 값이 오더라도 일치시킨다. 이 경우에는continue문을 실행해서 다음 반복으로 건너뛴다.
따라서 이 코드는 words[2]에 들어 있는 문자열을 i32 타입으로 변환하려고 시도하고, 변환된 값이 0과 100 사이일 경우 그 값을 score에 저장한다. 만약 변환에 실패하거나 값이 범위를 벗어나면 continue를 통해 다음 라인을 읽으러 간다.
이런 방식으로 match와 패턴 매칭은 Rust에서 강력한 에러 처리와 조건 분기를 가능하게 해준다.
Result type enum
Result 타입은 Rust에서 자주 사용되는 열거자(enum) 중 하나다. 이 Result 열거자는 두 가지 가능한 값, Ok와 Err,을 가질 수 있다. 간단히 말해, 어떤 작업이 성공적으로 끝났을 때는 Ok를 사용하고, 어떤 문제가 발생했을 때는 Err를 사용한다.
예를 들어보자:
- 숫자 변환을 시도하면서 성공했다면,
Ok에 숫자를 넣어서 반환한다. 즉, "성공했어! 여기 성공한 결과(숫자)가 있다"라는 뜻. - 만약 숫자 변환을 실패했다면,
Err에 실패 이유를 넣어서 반환한다. 즉, "실패했어, 문제가 있어"라는 뜻.
Ok와 Err 각각 안에는 상세한 정보(값)를 넣을 수 있다. 예를 들어, Ok(42)라고 하면, 작업이 성공적으로 끝나고 그 결과가 42라는 것을 나타낸다. Err("문제 발생")이라고 하면, 작업에 문제가 발생했고 그 문제의 설명은 "문제 발생"이라는 문자열이다.
이런 Result 타입을 사용하면 함수나 작업의 결과가 성공인지 실패인지, 그리고 그 상세한 값을 쉽게 알 수 있게 돼서 에러 처리를 간단하고 명확하게 할 수 있다.
물론이다. 아래는 간단한 Rust 코드 예제로, 문자열을 정수로 바꾸는 작업을 Result 타입으로 처리한다.
fn main() {
let my_string = "10"; // 숫자로 바꿀 문자열
let result = my_string.parse::<i32>(); // 문자열을 i32 타입의 정수로 바꾼다
match result {
Ok(number) => println!("성공! 숫자는 {}이다.", number),
Err(_) => println!("실패! 숫자로 바꿀 수 없다."),
}
}
이 예제에서 my_string.parse::<i32>()는 문자열을 정수로 바꾸려고 시도하고, Result 타입을 반환한다.
Ok(number)는 변환이 성공적이면 실행되는 부분이다.number변수에 성공적으로 변환된 숫자가 저장된다.Err(_)는 변환이 실패했을 때 실행되는 부분이다.
즉, 이 match문은 문자열을 숫자로 성공적으로 바꿨는지, 아니면 실패했는지 확인하고 그에 따라 다른 작업을 수행한다.
let record = Record {
first_name: words[0].to_string(),
last_name: words[1].to_string(),
score,
};
이 코드는 Record라는 구조체(struct)의 새로운 인스턴스(오브젝트)를 생성한다. Record 오브젝트에는 세 개의 필드가 있는데, first_name, last_name, 그리고 score다.
first_name: words[0].to_string(),: 첫 번째 단어(words[0])를 가져와서String타입으로 변환한 후,first_name필드에 저장한다.last_name: words[1].to_string(),: 두 번째 단어(words[1])를 가져와서String타입으로 변환한 후,last_name필드에 저장한다.score,: 이미score변수에 저장된 정수값을 그대로score필드에 저장한다.
이렇게 해서 새로운 Record 오브젝트가 만들어지고, 이 오브젝트는 first_name, last_name, score라는 세 개의 정보를 가지게 된다. 이 오브젝트는 바로 다음 줄의 records.push(record);에서 records라는 벡터에 추가될 것이다.
간단히 말해, 이 코드는 파일에서 읽은 한 줄의 정보를 바탕으로 하나의 Record 오브젝트를 만들고 그 오브젝트를 records 벡터에 추가하는 역할을 한다.
records.sort_by(|a, b| {
b.score.cmp(&a.score)
.then(a.last_name.cmp(&b.last_name))
.then(a.first_name.cmp(&b.first_name))
});
이 코드는 records 벡터에 있는 Record 오브젝트들을 정렬하는 역할을 한다. 정렬 기준은 세 가지다.
score가 높은 순서:b.score.cmp(&a.score)라고 되어 있어서, 높은 점수부터 낮은 점수로 정렬한다.last_name이 알파벳 순서대로: 만약score가 같다면,last_name으로 정렬한다.first_name이 알파벳 순서대로:score와last_name이 둘 다 같다면, 마지막으로first_name으로 정렬한다.
여기서 cmp는 비교(comparison) 함수다. 두 값을 비교해서 어느 것이 더 큰지, 작은지, 같은지를 판단한다.
.then()은 첫 번째 비교(b.score.cmp(&a.score))가 "같다(equal)"라는 결과를 냈을 때만 두 번째 비교(a.last_name.cmp(&b.last_name))를 수행한다. 두 번째 비교도 "같다"면 세 번째 비교를 수행한다.
결과적으로, 이 코드는 Record 오브젝트들을 score가 높은 순으로 정렬하고, 점수가 같으면 last_name과 first_name으로 추가 정렬한다. 이렇게 하면 최종적으로 잘 정렬된 records 벡터가 만들어진다.
Closure의 개념
|a, b| 형식은 Rust에서 클로저(closure)를 정의할 때 사용하는 문법이다. 클로저는 이름이 없는 함수처럼 동작하는 코드 블록이다. 클로저는 자신이 정의된 환경의 변수를 캡쳐해서 사용할 수 있어서 유용하다.
여기서 |a, b|는 클로저의 입력 파라미터를 정의한다. 즉, a와 b는 records.sort_by() 함수가 정렬을 수행할 때 비교되는 두 개의 Record 오브젝트다.
|: 클로저의 시작과 끝을 나타낸다.a, b: 클로저에 전달될 두 개의 입력 파라미터다. 이 경우에는 비교할 두 개의Record오브젝트다.
이 클로저는 두 개의 Record 오브젝트(a와 b)를 받아서 그 오브젝트들의 score, last_name, first_name을 비교하고, 그 결과에 따라 어떤 오브젝트가 더 "크거나 작거나 같은지"를 판단한다. 이 판단 결과를 records.sort_by() 함수가 사용해서 전체 records 벡터를 정렬한다.
더 쉽게 설명하면, |a, b|는 두 가지 박스(a와 b)를 가져와서 어떤 것이 더 큰지 비교하는 규칙을 만드는 것 같다. 이 규칙을 따라서 박스들을 나열할 것이다.
예를 들어, 마블을 가지고 놀 때 '무거운 마블은 왼쪽에, 가벼운 마블은 오른쪽에' 놓는다고 해보자. 여기서 |a, b|는 '무거운지 가벼운지 어떻게 판단할지'를 정하는 규칙이라고 생각하면 되겠다.
이 코드에서는 점수(score)가 높은 사람이 먼저 오고, 점수가 같으면 이름 순으로 나열한다는 것이다. 그래서 이 규칙을 따라서 records라는 이름의 목록을 예쁘게 나열해 줄 것이다.
클로저는 간단하게 말하면 "작은 작업 지시서"라고 생각하면 돼. 예를 들어, '가방 안의 책을 정리해라'라는 작업 지시서가 있다고 해보자. 이 작업 지시서는 언제든 가방 안의 책을 정리할 수 있게 해준다.
이제 |를 사용한 아주 쉬운 예제를 보자.
let add = |x, y| x + y;
let result = add(5, 3);
println!("Result: {}", result);
여기서 |x, y| x + y는 두 숫자를 더하는 작은 작업 지시서다. 이 작업 지시서를 add라는 변수에 넣었어. 그리고 add(5, 3)을 통해 이 작업 지시서에 5와 3을 넣으면 8이 나온다.
이렇게 |x, y| x + y는 두 숫자를 더하는 작업을 하는 작은 작업 지시서라고 생각하면 되겠다.
|를 사용하는 이유는 클로저라는 특별한 작업 지시서를 만들기 위해서이다. 함수와 클로저는 비슷하게 동작하지만, 클로저는 주변에 있던 정보도 같이 가져갈 수 있다는 특별한 능력이 있다.
예를 들어, 친구와 놀러 갈 때 가방에 물병만 가져가는 것과 가방에 물병과 간식, 책까지 모두 가져가는 것의 차이라고 생각하면 된다. 함수는 물병만 가져가고, 클로저는 물병, 간식, 책까지 모두 가져간다.
|를 쓰면 그 작업 지시서(클로저)가 주변의 것들도 함께 쓸 수 있게 해준다. 그래서 때로는 함수보다 클로저를 쓰는 게 더 유용할 때가 있다.
이해하기 어려운 개념이기에 더 쉬운 예제로 다시 설명해 보겠다.
게임을 좋아한다고 생각해보자. "점수 더하기" 버튼이 있는 게임이 있다. 버튼을 누르면 점수가 올라가는 것이다.
- 함수는 단순하게 "점수 더하기" 버튼만 있는 상자라고 생각하면 된다. 버튼을 누르면 점수가 1 올라간다.
- 클로저는 "점수 더하기" 버튼이 있는 상자인데, 버튼을 누르면 점수가 1 올라가고, 상자 안에 들어있는 초코칩 쿠키도 하나 가져올 수 있다.
함수(일반 상자) 예제
fn add_score(score: i32) -> i32 {
score + 1
}
fn main() {
let my_score = 0;
let new_score = add_score(my_score);
println!("New Score: {}", new_score); // 출력은 "New Score: 1"
}
클로저(특별한 상자) 예제
fn main() {
let cookie = "choco chip";
let add_score_and_get_cookie = |score| {
let new_score = score + 1;
println!("You got a {} cookie!", cookie);
new_score
};
let my_score = 0;
let new_score = add_score_and_get_cookie(my_score);
println!("New Score: {}", new_score); // 출력은 "New Score: 1", "You got a choco chip cookie!"
}
클로저 예제에서 add_score_and_get_cookie는 점수를 더하고, "choco chip" 쿠키도 가져올 수 있다. 그게 클로저의 특별한 능력이고, |score| 이 부분이 그 특별한 능력을 활성화하는 코드라고 생각하면 된다.
결론적으로, 클로저를 사용하면 내가 직접 함수 안에서 다루는 변수들 외의 것들도 같이 끌고와서 사용이 가능하게 된다.
클로저 뒤에 {}가 오는 것은 그 클로저가 여러 줄의 코드를 포함할 수 있다는 것을 의미한다. 간단한 클로저는 |a, b| a + b처럼 한 줄로 작성할 수 있지만, 좀 더 복잡한 로직이 필요한 경우에는 {}를 사용해서 여러 줄의 코드를 작성한다.
예를 들어, 아래 두 클로저는 같은 역할을 하는데, 하나는 한 줄로, 다른 하나는 여러 줄로 작성되었다.
한 줄로 작성된 클로저
let add = |a, b| a + b;
여러 줄로 작성된 클로저
let add = |a, b| {
let result = a + b;
result
};
두 번째 예제에서처럼 여러 줄의 코드가 필요하면 {}를 사용해서 클로저를 정의한다. 이렇게 하면 변수 선언이나 조건문, 반복문 등도 클로저 안에 넣을 수 있다.
다시 비교하는 부분으로 돌아와보자.
records.sort_by(|a, b| {
b.score.cmp(&a.score)
.then(a.last_name.cmp(&b.last_name))
.then(a.first_name.cmp(&b.first_name))
});
// Display the sorted records
for record in records {
println!("{}", record);
}
sort_by() 함수는 Rust의 Vec 타입에 속해 있고, 벡터 내의 요소들을 정렬하는 역할을 한다. 이 함수는 클로저를 인자로 받아서, 그 클로저를 사용해 벡터의 두 요소를 비교하고 정렬한다.
클로저 안에서는 주로 cmp 함수 같은 것을 사용해 두 요소를 비교하고, 어떤 것이 먼저 와야 하는지를 결정한다. sort_by() 함수는 이 클로저의 결과에 따라 벡터 내의 요소 위치를 바꾼다.
간단한 예를 들면, 아래와 같이 정수들로 이루어진 벡터를 정렬할 수 있다.
let mut vec = vec![5, 2, 10, 1, 15];
vec.sort_by(|a, b| a.cmp(b));
이 코드는 vec 벡터를 오름차순으로 정렬한다. 여기서 클로저 |a, b| a.cmp(b)가 sort_by()에게 어떻게 정렬을 할지 알려주는 역할을 한다.
이 코드는 두 개의 Record 오브젝트 a와 b를 비교하여 정렬 순서를 결정한다. 여기서는 세 가지 기준을 사용한다.
b.score.cmp(&a.score): 점수를 내림차순으로 정렬한다. 즉, 점수가 높은 것이 앞으로 오게 된다.a.last_name.cmp(&b.last_name): 점수가 같다면 성(last name)을 기준으로 오름차순 정렬한다.a.first_name.cmp(&b.first_name): 성이 같다면 이름(first name)을 기준으로 오름차순 정렬한다.
여기서 cmp 함수는 두 값 a와 b를 비교하여 Ordering 열거자를 반환한다. Ordering에는 Less, Equal, Greater 세 가지 값이 있어서, a가 b보다 작으면 Less, 같으면 Equal, 크면 Greater를 반환한다.
.then() 메서드는 첫 번째 cmp의 결과가 Equal일 때만 두 번째 cmp를 실행한다. 즉, 첫 번째 비교에서 이미 순서가 결정나면 두 번째 비교는 실행되지 않는다.
이러한 방식으로 여러 기준을 사용하여 오브젝트를 정렬할 수 있다.
오름차순과 내림차순
cmp 함수를 사용할 때 오름차순과 내림차순은 어떤 값들을 비교하는지에 따라 결정된다.
a.cmp(&b)를 사용할 때 Rust의 정렬 알고리즘이 어떻게 작동하는지 이해하는 게 중요하다. cmp 함수는 세 가지 가능한 값 중 하나를 반환한다: Ordering::Less, Ordering::Equal, 또는 Ordering::Greater.
Ordering::Less:a가b보다 작다Ordering::Equal:a와b가 같다Ordering::Greater:a가b보다 크다
a.cmp(&b)를 쓸 때, 이 함수가 Ordering::Less를 반환하면, 정렬 알고리즘은 a가 b보다 앞에 와야 한다고 판단한다. 즉, a가 b보다 작으면 a가 먼저 오게 되는 것이다.
이렇게 설계된 이유는, 일반적으로 작은 수부터 큰 수로 정렬하는 것이 자주 사용되기 때문이다. 이게 기본 동작 방식이라고 생각하면 편하다. 만약 다르게 정렬하고 싶다면 b.cmp(&a)처럼 작성하면 되고, 이렇게 하면 큰 수부터 작은 수로 정렬된다(내림차순).
오름차순 예:
a.score.cmp(&b.score)
이렇게 하면 a와 b를 비교해서 a의 점수가 더 낮으면 a가 먼저 오게 될 것이다.
내림차순 예:
b.score.cmp(&a.score)
여기에서는 b와 a의 위치를 바꾸었다. 이렇게 하면 b의 점수가 a보다 더 낮으면 a가 먼저 오게 될 것이다. 즉, 점수가 높은 것부터 정렬되게 된다.
간단하게 말하자면, cmp를 호출하는 값과 비교 대상이 되는 값의 위치를 바꾸면 오름차순과 내림차순을 바꿀 수 있다. a.cmp(&b)는 오름차순, b.cmp(&a)는 내림차순이 되는 것이다.
a와 b는 records라는 벡터에 저장된 Record 오브젝트 두 개를 가리킨다. sort_by 함수는 이 두 오브젝트를 비교해서 어떤 순서로 배열할지 결정한다.
간단히 말하면, a는 첫 번째 Record 오브젝트, b는 두 번째 Record 오브젝트다. 이 두 개를 비교해서 어떤 걸 먼저 놓을지 정하는 거다.
b.score.cmp(&a.score) 부분에서는 두 오브젝트의 score를 내림차순으로 비교한다.
a.last_name.cmp(&b.last_name) 부분에서는 두 오브젝트의 last_name을 오름차순으로 비교한다.
a.first_name.cmp(&b.first_name) 부분에서는 두 오브젝트의 first_name을 오름차순으로 비교한다.
이렇게 여러 조건을 겹쳐서 최종적으로 어떤 순서로 둘 것인지 결정하는 것이다.
a.cmp(&b)가 반환하는 Ordering 값은 오름차순이나 내림차순을 직접 결정하지 않는다. 이 함수는 단순히 a와 b 사이의 관계를 나타낼 뿐이다. 오름차순이나 내림차순을 결정하는 것은 이 Ordering 값을 어떻게 사용하는지에 따라다.
즉, cmp 함수의 반환 값은 정렬 순서를 결정하는 데 사용되는 "원시 데이터"라고 볼 수 있다. 오름차순이냐 내림차순이냐는 이 값을 어떻게 해석하고 사용하는지에 따라 결정된다.
여기서 앞의 요소와 뒤의 요소라는 개념이 등장하는데, 이는 이해를 쉽게하기 위해서 도입한 비유이다. 기본적으로 앞의 요소는 a를 가리키고, 뒤의 요소는 b를 가리킨다. 그리고 a.cmp(&b) 와 b.cmp(&a)는 앞/뒤의 요소와는 관계없이 무슨 값이 크냐/작냐만 판별한다.
이제 해석을 해보자.
기본적으로 정렬 알고리즘과 sort_by 함수의 설계에 따른 것이다. 이 함수는 작은 것부터 큰 것으로 정렬하려는 기본적인 방향성을 가지고 있다.
따라서 sort_by는 a.cmp(&b)의 결과로 Ordering::Less가 반환되면 실제로 a와 b가 어느것이 크냐에 상관없이 앞의 요소(a)가 뒤의 요소(b)보다 더 작다고 판단하여 a, b 의 순서 그대로 둔다. 즉, 작은 수나 알파벳 순서 등이 먼저 오도록 정렬을 한다. 그래서 기본적으로는 오름차순 정렬이 되는 것이다.
그런데, 만약 b.cmp(&a)의 결과로 Ordering::Less가 반환되면 뒤의 요소(b)가 앞의 요소(a)보다 더 작다고 판단하게 된다. 따라서 b, a 의 순서로 바꾸게 되며 이는 내림차순 정렬이 되는 것이다.
이것을 이해하는데 정말 한참 걸렸다. 하지만, 이렇게 시간과 노력을 들여서 한번 이해를 하고나니 기억이 더 오래갈 것 같다.
전체 코드는 아래와 같다.
// Criteria
// 1. Descending order of scores.
// 2. If same above, ascending order of last names
// 3. If same above, ascending order of first names
// Import necessary modules
use std::env;
use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader};
// Define a struct to hold a record
struct Record {
first_name: String,
last_name: String,
score: i32,
}
// Implement the Display trait for the Record struct
impl fmt::Display for Record {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {} {}", self.score, self.last_name, self.first_name)
}
}
fn main() -> std::io::Result<()> {
// Collect command line arguments
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("Usage: ./sort <filename>");
return Ok(());
}
// Get the filename from command line arguments
let filename = &args[1];
// Open the file
let file = File::open(filename)?;
// Create a buffered reader for the file
let reader = BufReader::new(file);
let mut records: Vec<Record> = Vec::new();
// Read each line from the file
for line in reader.lines() {
let line = line?;
let words: Vec<&str> = line.split_whitespace().collect();
if words.len() < 3 {
continue;
}
// Parse the score and ensure it's an integer between 0 and 100
let score: i32 = match words[2].parse() {
Ok(n) if n >= 0 && n <= 100 => n,
_ => continue,
};
// Create a record from parsed data
let record = Record {
first_name: words[0].to_string(),
last_name: words[1].to_string(),
score,
};
// Push the record into the vector
records.push(record);
}
// Sort the records in descending order based on the score,
// and in case of a tie, sort by last name, and then by first name.
records.sort_by(|a, b| {
// Compare the scores in descending order (higher scores first)
// using the `cmp` method.
b.score
.cmp(&a.score)
// If scores are equal, compare by last names in ascending order.
// This sorts names alphabetically from A to Z.
.then(a.last_name.cmp(&b.last_name))
// If last names are also equal, compare by first names in ascending order.
.then(a.first_name.cmp(&b.first_name))
});
// Display the sorted records
for record in records {
println!("{}", record);
}
Ok(())
}
댓글 없음:
댓글 쓰기