이번 포스팅에서는 rust로 text file을 읽어오고, 여기서 최빈 단어를 검출해내는 코드를 분석해 보도록 하겠다.
use std::collections::HashMap;use std::fs::File;use std::io::{self, BufRead, BufReader};use std::env;
fn main() -> io::Result<()> { // Fetch the filename from command-line arguments let args: Vec<String> = env::args().collect(); if args.len() != 2 { eprintln!("Usage: cargo run <filename>"); std::process::exit(1); }
// Open the file let filename = &args[1]; let file = File::open(filename)?;
// Use BufReader module for better performance let reader = BufReader::new(file);
// HashMap to store the frequency of each word let mut word_count: HashMap<String, u32> = HashMap::new(); // Loop through each line in the file for line in reader.lines() { // When the line from the buffer has valid value, it will store the value // else it will occur error message using ? operator let line = line?; // Split the line into words and iterate through each word for word in line.split_whitespace() { let word = word.to_lowercase(); // Increase the word count *word_count.entry(word).or_insert(0) += 1; } } // Find the maximum frequency let mut max_count: u32 = 0; for &count in word_count.values() { if count > max_count { max_count = count; } }
// Print words that have the maximum frequency for (word, count) in &word_count { if *count == max_count { println!("{}", word); } }
Ok(())}
우선 데이터를 file에서 받아오면 이것을 HashMap에 저장할 것이고, 이 HashMap에서는 String, u32 타입으로 해당 단어와 그 빈도수를 기록할 것이다.
이제 코드별로 한번 분석해보자.
물론이다. 코드를 한 줄씩 자세히 설명하겠다.
use std::collections::HashMap;
HashMap 타입을 사용하기 위해 표준 라이브러리에서 가져온다.
use std::fs::File;
파일 I/O를 위해 File 타입을 가져온다.
use std::io::{self, BufRead, BufReader};
입출력에 관련된 다양한 타입과 트레이트를 가져온다. BufRead와 BufReader는 파일을 효율적으로 읽기 위해 사용한다.
use std::env;
커맨드 라인 인자를 읽기 위해 env 모듈을 가져온다.
fn main() -> io::Result<()> {
main 함수를 정의한다. 리턴 타입은 io::Result<()>로, I/O 작업에서 발생할 수 있는 에러를 처리한다.
Result<()>라고 표현하는 것은 Rust의 제네릭 타입을 사용한 것이다. Result 열거형(enum)은 성공 또는 실패를 나타내는 타입으로, 성공할 경우 Ok(T) 형태의 값을 가지고, 실패할 경우 Err(E) 형태의 값을 가진다.
Result<T, E>에서 T는 성공한 경우 반환될 타입, E는 에러 타입을 나타낸다. 여기서 <...> 부분은 제네릭을 표현하는 문법이다.
예를 들어,
Result<i32, String>: 성공하면i32타입의 값, 실패하면String타입의 에러 메시지를 반환한다.Result<(), io::Error>: 성공하면 아무 것도 반환하지 않고(()는 빈 튜플, 즉 '아무 것도 없다'를 의미), 실패하면io::Error타입의 에러를 반환한다.
Result<()> 같은 경우는 성공시에는 아무 값도 반환하지 않고(()), 실패시에는 에러 타입에 따른 값을 반환하게 된다. 이렇게 제네릭을 사용하면 다양한 타입에 대응할 수 있는 유연한 코드를 작성할 수 있다.
let args: Vec<String> = env::args().collect();
커맨드 라인 인자를 Vector에 모은다.
let args: Vec<String> = env::args().collect(); 이 코드는 명령줄 인자(command-line arguments)를 가져와서 이를 Vec<String> 형태의 벡터로 변환하는 역할을 한다.
-
env::args(): 이 함수는 커맨드라인 인자를 나타내는 반복자(iterator)를 반환한다. 예를 들어,my_program arg1 arg2처럼 실행하면, 이 반복자는 "my_program", "arg1", "arg2"라는 세 개의 요소를 포함한다. -
collect(): 이 메서드는 반복자의 모든 요소를 컬렉션 데이터 타입으로 변환해준다. 여기서는Vec<String>으로 변환한다. 이렇게 하면 각 커맨드라인 인자가String형태로Vec에 저장된다. -
let args: Vec<String>: 이 부분은args라는 이름의 변수를 선언하고, 이 변수의 타입을Vec<String>으로 지정한다.
결과적으로, 이 한 줄의 코드는 커맨드라인 인자를 가져와서 Vec<String> 타입의 args 변수에 저장한다. 이 args 변수를 통해 프로그램 내에서 사용자가 입력한 커맨드라인 인자를 참조할 수 있다.
if args.len() != 2 { ... }
인자가 2개가 아니면 에러 메시지를 출력하고 프로그램을 종료한다.
eprintln!과 println!은 모두 출력을 위한 매크로지만, 둘 사이에는 몇 가지 차이점이 있다.
- 출력 대상:
- `println!`: 표준 출력(standard output, stdout)에 문자열을 출력한다. 일반적인 정보나 결과를 출력할 때 사용한다.
- `eprintln!`: 표준 에러 출력(standard error, stderr)에 문자열을 출력한다. 에러나 경고 메시지를 출력할 때 주로 사용한다.
- 리다이렉션:
- 표준 출력과 표준 에러 출력은 다르게 리다이렉션할 수 있다. 예를 들어, 리다이렉션을 통해 표준 출력만 파일에 저장할 수 있고, 표준 에러 출력은 콘솔에 그대로 둘 수 있다.
- 용도:
- `println!`은 일반적인 정보를 사용자에게 보여주기 위해 사용한다.
- `eprintln!`은 오류나 예외 상황에서의 메시지를 출력하기 위해 사용한다. 이런 메시지는 일반 출력과 분리되어 있어야 하므로, 표준 에러 출력에 출력된다.
간단한 예를 들면, 프로그램에서 파일을 열려고 했는데 파일이 없을 경우 eprintln!을 사용해서 "파일이 없습니다"라는 에러 메시지를 출력할 수 있다.
std::process::exit(1);는 현재 실행 중인 프로그램을 종료하고, 운영체제에게 종료 상태 코드(exit code 또는 status code)로 1을 반환한다. 종료 상태 코드는 프로그램이 어떻게 종료되었는지를 운영체제나 다른 프로그램에 알려주는 값이다.
종료 코드에 대한 몇 가지 주의 사항:
- 일반적으로
0은 성공을 의미한다. 0이 아닌 값(여기서는1)은 보통 오류나 예외 상황을 나타낸다.
이 코드가 호출되면 프로그램은 즉시 종료되므로, 그 아래에 있는 코드는 실행되지 않는다. 또한, 이렇게 종료하면 프로그램의 리소스 해제나 다른 정리 작업을 수행하지 않고 빠르게 종료한다. 따라서 이 함수를 호출하기 전에 필요한 모든 정리 작업을 미리 해야 한다.
간단하게 말해서, std::process::exit(1);은 프로그램을 오류 상태로 즉시 종료한다.
std::process::exit(1);를 호출하면 프로그램은 즉시 종료되므로, 이 함수 호출 전에 수행해야 할 정리 작업이 있다면 그것이 수행되지 않을 수 있다. 하지만, 이 함수 자체가 시스템이나 메모리에 직접적으로 나쁜 영향을 주는 것은 아니다. 이는 운영체제가 프로세스가 종료될 때 할당된 자원을 해제하는 역할을 하기 때문이다.
그렇다고 해서 무작정 이 함수를 호출해도 되는 것은 아니다. 예를 들어, 파일을 열었다면 그것을 제대로 닫지 않고 프로그램을 종료하면 데이터 손실이 발생할 수 있다. 또는 네트워크 연결을 안전하게 종료하지 않으면 문제가 생길 수 있다.
따라서 이 함수를 호출하기 전에 필요한 정리 작업을 해야 하며, 그렇게 함으로써 프로그램이 안전하게 종료될 수 있다.
간단하게 말하면, 이 함수는 프로그램을 강제로 종료시키지만, 그 자체로는 시스템이나 메모리에 나쁜 영향을 주지는 않는다. 하지만 정리해야 할 작업이 있는 경우에는 그것을 먼저 처리해주어야 한다.
참고로 Rust에는 가비지 컬렉터(Garbage Collector)가 없다. Rust는 소유권(Ownership), 참조 및 빌림(Borrowing), 생명주기(Lifetime) 등의 개념을 통해 메모리를 안전하게 관리한다. 이러한 메커니즘 덕분에 Rust는 런타임에 가비지 컬렉터를 필요로 하지 않으면서도 메모리 누수나 무효한 메모리 참조 등의 문제를 컴파일 타임에 방지할 수 있다.
소유권의 개념은 변수에 메모리를 할당하면 그 변수가 그 메모리의 '소유자'가 되고, 그 변수가 범위를 벗어나거나 명시적으로 해제되면 메모리도 자동으로 해제된다는 것이다. 이렇게 해서 Rust는 가비지 컬렉터 없이도 메모리를 효율적으로 관리한다.
간단하게 말해서, Rust는 컴파일 타임에 메모리 관리를 철저하게 검사하여 런타임에 가비지 컬렉터가 필요 없도록 설계되었다.
let filename = &args[1];
파일 이름을 args 벡터에서 가져온다.
let file = File::open(filename)?;
파일을 열고, File 객체를 생성한다. 실패할 경우 에러를 반환한다.
let reader = BufReader::new(file);
BufReader를 사용해서 파일을 효율적으로 읽는다.
BufReader를 사용하면 파일을 효율적으로 읽을 수 있는 이유는 버퍼링(buffering) 때문이다. 기본적으로, 파일 입출력은 디스크와의 상호작용을 필요로 하는데, 디스크 액세스는 메모리 액세스에 비해 상대적으로 느리다. 따라서 매번 작은 양의 데이터만 읽으면, 입출력 작업의 오버헤드가 커질 수 있다.
BufReader는 내부적으로 버퍼를 유지하면서 한 번에 큰 덩어리의 데이터를 디스크로부터 읽어온다. 그런 다음 작은 양의 데이터가 필요할 때 이 버퍼로부터 가져온다. 이렇게 하면 디스크 액세스 횟수가 줄어들고, 결과적으로 성능이 향상된다.
예를 들어, 단순히 파일을 한 바이트씩 읽는 대신에 BufReader는 8KB나 16KB 등 훨씬 큰 블록을 한 번에 읽어올 수 있다. 그 후 프로그램이 작은 단위로 데이터를 요청하면 BufReader는 이미 읽어둔 버퍼에서 그 데이터를 제공한다. 이 방식으로 빈번한 디스크 액세스를 줄이고 성능을 높일 수 있다.
let mut word_count: HashMap<String, u32> = HashMap::new();
단어의 빈도수를 저장할 HashMap을 초기화한다.
for line in reader.lines() { ... }
파일의 각 줄에 대해 반복한다.
let line = line?;
let line = line?; 이 코드는 여러 가지 작업을 한 줄에서 처리한다.
-
line?부분: 여기서의?연산자는Result타입의 값을 처리한다. 만약Result가Ok인 경우, 그 안에 들어있는 실제 값을 반환한다. 만약Result가Err인 경우, 함수 실행을 즉시 중단하고 그Err값을 반환한다. 이는match문을 사용해서Result를 수동으로 처리하는 것보다 훨씬 간결하게 에러 처리를 할 수 있게 해준다. -
let line = ...: 여기서는line?에서 반환된 실제 값을 새로운 변수line에 바인딩한다.
이 코드의 전체적인 의미는, "만약 line이 Ok 상태의 Result라면, 그 값을 가져와서 line 변수에 저장하고, 만약 Err라면, 함수를 즉시 종료하고 그 에러를 반환하라" 라는 것이다. 이렇게 하면 에러 처리를 매우 간결하고 읽기 쉽게 할 수 있다.
? 연산자는 Rust에서 흔히 에러 처리에 사용된다. 이 연산자는 Result 또는 Option 타입의 값에 적용할 수 있다. 이 연산자의 동작 방식은 다음과 같다.
-
Result<T, E>타입에서 사용할 경우:Ok(T)값이면, 내부의T값을 반환한다.Err(E)값이면, 함수를 즉시 종료하고Err(E)를 반환한다.
-
Option<T>타입에서 사용할 경우:Some(T)값이면, 내부의T값을 반환한다.None값이면,None을 반환한다.
? 연산자를 사용하면 에러 처리 로직을 간결하게 표현할 수 있다. 예를 들어, Result 타입을 리턴하는 함수 foo가 있고, 그 결과를 변수 x에 저장하려는 상황을 가정하자.
이렇게 처리할 수 있다:
let x = match foo() {
Ok(value) => value,
Err(e) => return Err(e),
};
? 연산자를 사용하면 이렇게 간단하게 표현할 수 있다:
let x = foo()?;
이렇게 하면 코드가 더 간결해지고 읽기 쉬워진다. 하지만 ? 연산자를 사용하는 함수는 반드시 Result나 Option을 반환하는 형태의 함수여야 한다. 그렇지 않으면 컴파일 에러가 발생한다.
for word in line.split_whitespace() { ... }
각 줄을 공백으로 나누고, 단어별로 빈도를 계산한다.
line.split_whitespace()은 문자열 line에서 공백을 기준으로 문자열을 나눈다. 여기서 공백이라는 것은 띄어쓰기, 탭, 줄바꿈 등을 포함한다. 이 함수는 Iterator를 반환하는데, Iterator를 사용해서 나눠진 각 부분 문자열을 하나씩 처리할 수 있다.
예를 들어, 문자열이 "Hello, world! This is a test."라고 하면 split_whitespace() 함수를 호출한 뒤 Iterator를 순회하면 "Hello,", "world!", "This", "is", "a", "test." 같은 부분 문자열을 얻을 수 있다.
코드에서 이런 식으로 사용된다:
for word in line.split_whitespace() {
// word는 나눠진 각 부분 문자열
}
여기서 word는 나눠진 각 부분 문자열을 가지고 있을 것이고, 이를 이용해서 필요한 작업을 수행할 수 있다. 이 예제에서는 각 단어의 빈도를 세기 위해 HashMap에 담고 있다.
*word_count.entry(word).or_insert(0) += 1;
HashMap에서 단어의 빈도를 가져오거나 새로 만들고, 빈도를 1 증가시킨다.
word 앞에 붙은 *는 역참조(dereference) 연산자입니다. 이 코드에서 or_insert(0) 메소드가 반환하는 것은 값의 가변 참조(&mut u32)인데, 이 참조를 실제 값으로 바꾸려면 역참조를 해야 한다.
*word_count.entry(word).or_insert(0) += 1;
여기서 word_count.entry(word).or_insert(0)는 &mut u32 타입의 참조를 반환합니다. 이 참조가 가리키는 실제 값을 변경하기 위해서는 역참조를 해야 한다. 그래서 * 연산자를 사용해서 참조를 역참조하고, 실제 값을 1만큼 증가시킨다.
간단히 말하면, *는 "참조된 값을 가져와라"라는 의미입니다. 이를 통해 실제 값을 변경할 수 있다.
* 기호는 Rust에서 "dereference"라고 하며, 포인터나 참조형 변수가 실제 값에 접근할 수 있도록 해준다. 간단하게 말하면, * 기호를 변수 앞에 붙이면 그 변수가 가리키는 실제 값을 가져온다.
예를 들어, 숫자 5를 참조해서 변수에 저장한다고 생각해보자. 그 변수는 "5를 가리키고 있다"라고 말할 수 있다. 그런데 내가 실제로 "5" 이라는 숫자가 필요할 때, 그 변수 앞에 *를 붙이면 "가리키고 있는 것에서 실제 값"을 가져올 수 있다.
let x = 5; // x라는 변수에 5를 저장
let y = &x; // y라는 변수에 x의 '참조'를 저장 (y는 5를 '가리키고' 있다)
let z = *y; // y가 가리키고 있는 '실제 값' 5를 가져와서 z에 저장
코드에서 *word_count.entry(word).or_insert(0) += 1; 부분에서 *는 HashMap에서 찾은 값을 실제로 변경하기 위해 사용된다. word_count.entry(word).or_insert(0)는 "참조"를 반환하기 때문에, 그 참조가 가리키는 실제 값을 변경하기 위해 *가 앞에 붙어 있다.
주의할 점은, 여기서 word_count 자체는 주소값을 말하는 것이 아니다. word_count는 HashMap의 인스턴스이다. 그러나 word_count.entry(word).or_insert(0)이 반환하는 것은 값의 "참조" 혹은 "가변 참조"이다. 이 참조는 실제 값에 대한 포인터 같은 것이라고 생각하면 된다.
예를 들어, 단어 "apple"이 3번 나왔다면, word_count 안에서 "apple" -> 3 이런 식으로 저장될 것이다. word_count.entry("apple").or_insert(0)을 호출하면, "3"이라는 값에 대한 "참조"를 반환한다.
이 참조를 변경하려면, *을 앞에 붙여 실제 값에 접근해야 한다. 따라서 *word_count.entry(word).or_insert(0) += 1;에서 *는 반환된 참조가 가리키는 실제 값을 변경하기 위해 사용된다.
entry(word)나 or_insert(0)이 참조를 반환하는 이유는 주로 두 가지이다: 성능과 유연성.
-
성능: 참조를 반환하면 데이터의 복사가 일어나지 않으므로, 특히 큰 데이터 구조에서는 이 방법이 효율적이다. 실제 값을 반환하면 값의 복사가 발생하는데, 이것은 메모리와 시간을 더 소비할 수 있다.
-
유연성: 참조를 반환하면, 호출한 쪽에서 그 참조를 통해 데이터를 직접 수정할 수 있다. 즉,
HashMap안의 값을 바로 업데이트할 수 있어서 코드가 간결해진다.
예를 들어, or_insert(0)이 실제 값을 반환한다면, 값을 변경하기 위해서는 다시 HashMap에 접근해야 할 것이다. 그러나 참조를 반환하면, 그 참조를 통해 값을 직접 변경할 수 있어서 코드가 더 간단하고 명확해진다.
이런 이유로, Rust의 HashMap에서는 이러한 메소드들이 참조를 반환한다.
참고로 자바의 HashMap과 Rust의 HashMap은 동작 방식이 다르다. 자바의 HashMap에서는 대부분의 메소드가 실제 값을 반환한다, 참조를 반환하는 경우는 거의 없다.
예를 들어, get() 메소드는 특정 키에 대응하는 값을 반환하고, put() 메소드는 키와 값을 매핑하면서 이전에 매핑된 값을 반환한다. 이 값들은 실제 객체의 복사본이 아니라 원래 객체 그 자체다. 그러나 Java는 객체 지향 언어로, 대부분 객체의 참조를 다루므로 실제로는 메모리 복사가 일어나지 않는다. 여기서 반환되는 것이 객체의 "참조"라고 볼 수 있으나, 이는 자바의 객체 모델과 관련이 있고 기본적으로는 "값"을 반환한다고 볼 수 있다.
따라서, 자바와 Rust는 이 부분에서 다르게 동작한다. Rust에서는 명시적으로 참조를 반환하여 더 높은 유연성과 성능을 얻지만, 자바는 그런 방식을 따르지 않는다.
이 코드는 Rust의 HashMap에 있는 entry 메소드와 or_insert 메소드를 사용해 단어의 빈도를 센다.
-
word_count.entry(word): 이 부분은HashMap에서 주어진word에 해당하는 엔트리를 찾는다.entry메소드는Entry열거체(enum)를 반환하는데, 이 열거체는Occupied와Vacant라는 두 가지 상태를 가진다. 여기서Occupied는 이미word키가HashMap에 존재한다는 것을 의미하고,Vacant는 그렇지 않다는 것을 의미하는 것이다. -
.or_insert(0): 이 메소드는Entry열거체에 동작한다. 만약Occupied상태라면 이미 있는 값을 반환하고,Vacant상태라면 새로운 엔트리를 생성하고 초기값0을 넣는다. -
*... += 1: 이 부분은or_insert에서 반환된 값에 대한 가변 참조(&mut u32)를 역참조(dereference)하여 1을 더한다. 이렇게 하면 단어의 빈도가 1 증가하게 된다.
결과적으로, 이 한 줄의 코드는 주어진 단어(word)가 HashMap에 이미 존재하는지 확인하고, 존재한다면 그 빈도를 1 증가시키고, 존재하지 않는다면 새로운 엔트리를 만들어 빈도를 1로 설하게 된다. 이렇게 해서 단어의 빈도를 효율적으로 계산할 수 있다.
for &count in word_count.values() { ... }
HashMap에 저장된 모든 빈도에 대해 반복하면서 최대 빈도를 찾는다.
&count in word_count.values() 코드는 Rust에서 word_count HashMap의 모든 값을 순회하는 구문이다. 여기서 &count는 각 값에 대한 불변 참조(immutable reference)를 나타낸다. 즉, &count는 HashMap의 각 값에 대한 참조를 받아서 이 값을 사용할 수 있게 해준다.
word_count.values()는 HashMap에서 모든 값을 반환하는 iterator를 생성한다. 이 iterator는 각각의 값에 대해 루프를 돌며 이 값을 &count에 넣는다.
간단한 예시로, 만약 word_count가 {"apple": 1, "banana": 2, "cherry": 3} 이라면, 이 루프는 각각 1, 2, 3이라는 값들을 참조로써 순회할 것이다. &count에는 순서대로 &1, &2, &3이 들어가게 된다.
이렇게 참조를 사용하면 메모리 복사를 줄여 성능을 개선할 수 있다.
여기서 &는 borrow를 의미한다. Rust에서 borrow라는 개념은 불변 참조(immutable reference) 또는 가변 참조(mutable reference)를 생성해서 값을 안전하게 접근할 수 있게 해준다. 그리고 참조는 주소값을 가리키는 개념이기도 하다.
word_count.values()는 HashMap의 값들에 대한 iterator를 반환한다. 이 iterator가 반환하는 각 값은 참조 형태로 제공된다. 따라서 &count에 들어가는 것은 이 값들에 대한 참조, 즉 borrow다. 주소값을 직접 다루는 것이 아니라, 값을 안전하게 접근할 수 있도록 하는 참조를 만든다고 이해하는 게 맞다.
즉, word_count.values()는 주소값을 반환하지 않고, 값에 대한 참조를 반환한다. 그래서 &count에는 이 참조가 저장되는 것이다.
여기서 count가 &count가 아닌 이유는 for &count in word_count.values()라는 구문 때문이다. 이 구문에서 &count는 패턴 매칭을 사용해서 값을 참조에서 벗겨내는 역할을 한다. 즉, &count라고 쓰면 이 &는 참조를 '디리퍼런싱(dereferencing)'하고, 그 결과로 나온 값을 count에 저장한다.
그래서 이 loop 내부에서 count를 사용하면 그것은 이미 참조가 아닌 실제 값이 된다. 따라서 if count > max_count 같은 연산에서 별도로 borrow할 필요가 없다.
간단한 예시로, 만약 let a = &5;이라면 a는 5에 대한 참조다. &a를 통해 이 참조를 벗겨내면 다시 5가 나온다. 그래서 count는 그냥 숫자 값이 되고 별도로 borrow할 필요가 없다.
죄송하다. 다시 쉽게 설명해 볼게.
&count는 사실상 "박스 안에 있는 숫자를 꺼내줘"라는 뜻이다. 이 박스는 word_count.values()에서 나온 값들이 참조(주소)로 되어 있기 때문에, &count를 쓰면 그 참조를 벗겨내고 실제 숫자만 count에 저장한다.
간단한 예를 들자면, 상자 안에 사과 3개가 있다고 치자. &count는 이 상자를 열고 사과만 꺼내는 것과 같다. 상자를 열고 나면, 이제 count에는 상자가 아니라 그 안의 사과만 있다. 그래서 상자(참조)와 관련된 일을 하지 않고, 사과(실제 값)와만 비교하거나 계산할 수 있다.
이런 이유로 if count > max_count에서 count는 그냥 숫자로 취급되고, 별도로 참조와 관련된 일을 할 필요가 없다.
for (word, count) in &word_count { ... }
HashMap을 순회하면서 최대 빈도수를 가진 단어를 출력한다.
Rust의 for 루프에서 &word_count를 사용하면, 이 HashMap의 각 엔트리(즉, 키와 값의 쌍)을 순회하게 된다. 각 엔트리는 튜플 형태로 되어 있고, 튜플의 첫 번째 요소는 키(word), 두 번째 요소는 값(count)이다.
그런데, &word_count를 사용하면 각 튜플은 실제 값이 아니라 참조(주소값)으로 되어 있다. 따라서 for (word, count)는 이 참조를 자동으로 해제(dereference)해서 실제 값을 word와 count에 저장한다.
간단한 예로, 상자 안에 "apple"이라는 단어와 3이라는 숫자가 들어있다고 생각하자. &word_count를 순회하면서 이 상자를 하나씩 꺼내와서 상자를 열고 "apple"과 3을 word와 count에 저장한다. 이런 식으로 for 루프가 &word_count의 모든 상자를 열어서 word와 count에 값을 저장하게 된다.
Ok(())
함수의 마지막에 Ok를 반환해 에러가 없음을 표시한다.
main 함수의 리턴 타입이 io::Result<()>이다. 이것은 main 함수가 I/O 연산을 수행할 수 있고, 에러가 발생할 수 있다는 것을 의미한다. Rust에서는 에러를 명시적으로 처리해야 하기 때문에, 함수가 성공적으로 끝나면 Ok(())를 리턴해야 한다.
여기서 Ok는 Result 타입의 열거형(enum) 중 하나로, 연산이 성공적이라는 것을 표시한다. 괄호 안의 ()는 "unit"이라고 부르며, 아무 값도 없음을 나타낸다.
간단한 예를 들자면, 친구에게 숙제를 끝냈는지 물어봤을 때 "다 끝냈다"라고 대답하는 것이 Ok(())와 비슷하다. "다 끝냈다"라고 말하면 숙제를 끝낸 것이고, 에러가 없다는 것을 의미한다.
이게 전체 코드의 세부 분석이다.
댓글 없음:
댓글 쓰기