I/O 프로젝트 개선

Beginner

This tutorial is from open-source community. Access the source code

소개

I/O 프로젝트 개선에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 반복자 (iterator) 를 사용하여 12 장에서 다룬 I/O 프로젝트의 Config::build 함수와 search 함수의 구현을 어떻게 개선할 수 있는지 살펴봅니다.

I/O 프로젝트 개선

반복자에 대한 새로운 지식을 바탕으로, 코드를 더 명확하고 간결하게 만들기 위해 반복자를 사용하여 12 장의 I/O 프로젝트를 개선할 수 있습니다. 반복자가 Config::build 함수와 search 함수의 구현을 어떻게 개선할 수 있는지 살펴보겠습니다.

반복자를 사용하여 clone 제거하기

Listing 12-6 에서, String 값의 슬라이스를 받아 슬라이스에서 인덱싱하고 값을 복제하여 Config 구조체의 인스턴스를 생성하는 코드를 추가했습니다. 이를 통해 Config 구조체가 해당 값을 소유할 수 있었습니다. Listing 13-17 에서는 Config::build 함수의 구현을 Listing 12-23 과 동일하게 재현했습니다.

파일 이름: src/lib.rs

impl Config {
    pub fn build(
        args: &[String]
    ) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

Listing 13-17: Listing 12-23 의 Config::build 함수 재현

당시에는 비효율적인 clone 호출에 대해 나중에 제거할 것이라고 말씀드렸습니다. 이제 그 시간이 왔습니다!

여기서 clone이 필요했던 이유는 매개변수 argsString 요소의 슬라이스가 있지만, build 함수는 args를 소유하지 않기 때문입니다. Config 인스턴스의 소유권을 반환하기 위해, Config 인스턴스가 자체 값을 소유할 수 있도록 Configqueryfilename 필드에서 값을 복제해야 했습니다.

반복자에 대한 새로운 지식을 바탕으로, build 함수가 슬라이스를 빌리는 대신 반복자의 소유권을 인수로 받도록 변경할 수 있습니다. 슬라이스의 길이를 확인하고 특정 위치를 인덱싱하는 코드 대신 반복자 기능을 사용할 것입니다. 반복자가 값에 접근하므로 Config::build 함수가 무엇을 하는지 명확해질 것입니다.

Config::build가 반복자의 소유권을 가져가고 빌리는 인덱싱 작업을 중단하면, clone을 호출하고 새로운 할당을 하는 대신 반복자에서 String 값을 Config로 이동할 수 있습니다.

반환된 반복자 직접 사용하기

I/O 프로젝트의 src/main.rs 파일을 엽니다. 다음과 같이 보일 것입니다.

파일 이름: src/main.rs

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    --snip--
}

먼저 Listing 12-24 에 있던 main 함수의 시작 부분을 Listing 13-18 의 코드로 변경합니다. 이번에는 반복자를 사용합니다. Config::build도 업데이트해야 컴파일됩니다.

파일 이름: src/main.rs

fn main() {
    let config =
        Config::build(env::args()).unwrap_or_else(|err| {
            eprintln!("Problem parsing arguments: {err}");
            process::exit(1);
        });

    --snip--
}

Listing 13-18: env::args의 반환 값을 Config::build에 전달

env::args 함수는 반복자를 반환합니다! 반복자 값을 벡터로 수집한 다음 슬라이스를 Config::build에 전달하는 대신, 이제 env::args에서 반환된 반복자의 소유권을 직접 Config::build에 전달합니다.

다음으로, Config::build의 정의를 업데이트해야 합니다. I/O 프로젝트의 src/lib.rs 파일에서 Config::build의 시그니처를 Listing 13-19 와 같이 변경해 보겠습니다. 함수 본문을 업데이트해야 하므로, 이것 역시 아직 컴파일되지 않습니다.

파일 이름: src/lib.rs

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        --snip--

Listing 13-19: 반복자를 예상하도록 Config::build의 시그니처 업데이트

env::args 함수에 대한 표준 라이브러리 문서는 반환하는 반복자의 유형이 std::env::Args이며, 해당 유형이 Iterator 트레이트를 구현하고 String 값을 반환한다는 것을 보여줍니다.

Config::build 함수의 시그니처를 업데이트하여 매개변수 args&[String] 대신 impl Iterator<Item = String> 트레이트 바운드를 가진 제네릭 타입을 갖도록 했습니다. "매개변수로서의 트레이트"에서 논의한 impl Trait 구문을 사용하면 argsIterator 트레이트를 구현하고 String 항목을 반환하는 모든 유형이 될 수 있습니다.

args의 소유권을 가져가고 반복하여 args를 변경할 것이므로, 가변으로 만들기 위해 args 매개변수 사양에 mut 키워드를 추가할 수 있습니다.

인덱싱 대신 반복자 트레이트 메서드 사용하기

다음으로, Config::build의 본문을 수정하겠습니다. argsIterator 트레이트를 구현하므로, next 메서드를 호출할 수 있다는 것을 알고 있습니다! Listing 13-20 은 Listing 12-23 의 코드를 업데이트하여 next 메서드를 사용합니다.

파일 이름: src/lib.rs

impl Config {
    pub fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };

        let file_path = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file path"),
        };

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

Listing 13-20: 반복자 메서드를 사용하도록 Config::build의 본문 변경

env::args의 반환 값에서 첫 번째 값은 프로그램의 이름이라는 것을 기억하세요. 우리는 그것을 무시하고 다음 값을 얻고 싶으므로, 먼저 next를 호출하고 반환 값으로 아무것도 하지 않습니다. 그런 다음 next를 호출하여 Configquery 필드에 넣을 값을 얻습니다. nextSome을 반환하면, match를 사용하여 값을 추출합니다. None을 반환하면, 인수가 충분하지 않다는 의미이므로 Err 값으로 조기에 반환합니다. filename 값에 대해서도 동일한 작업을 수행합니다.

반복자 어댑터로 코드 명확하게 만들기

I/O 프로젝트의 search 함수에서도 반복자를 활용할 수 있습니다. Listing 13-21 에 Listing 12-19 와 동일하게 재현되어 있습니다.

파일 이름: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

Listing 13-21: Listing 12-19 의 search 함수 구현

반복자 어댑터 메서드를 사용하여 이 코드를 더 간결하게 작성할 수 있습니다. 그렇게 하면 가변 중간 results 벡터를 사용하지 않아도 됩니다. 함수형 프로그래밍 스타일은 코드를 더 명확하게 만들기 위해 가변 상태의 양을 최소화하는 것을 선호합니다. 가변 상태를 제거하면 results 벡터에 대한 동시 접근을 관리할 필요가 없으므로, 향후 검색을 병렬로 수행할 수 있는 기능 향상을 가능하게 할 수 있습니다. Listing 13-22 는 이러한 변경 사항을 보여줍니다.

파일 이름: src/lib.rs

pub fn search<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    contents
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

Listing 13-22: search 함수 구현에서 반복자 어댑터 메서드 사용

search 함수의 목적은 query를 포함하는 contents의 모든 줄을 반환하는 것임을 기억하세요. Listing 13-16 의 filter 예제와 유사하게, 이 코드는 line.contains(query)true를 반환하는 줄만 유지하기 위해 filter 어댑터를 사용합니다. 그런 다음 collect를 사용하여 일치하는 줄을 다른 벡터로 수집합니다. 훨씬 간단합니다! search_case_insensitive 함수에서도 동일한 변경을 수행하여 반복자 메서드를 사용해 보세요.

루프와 반복자 중 선택하기

다음으로 논리적인 질문은 자신의 코드에서 어떤 스타일을 선택해야 하는지, 그리고 그 이유입니다. Listing 13-21 의 원래 구현과 Listing 13-22 의 반복자를 사용하는 버전 중 어떤 것을 선택해야 할까요? 대부분의 Rust 프로그래머는 반복자 스타일을 선호합니다. 처음에는 익숙해지기 조금 더 어렵지만, 다양한 반복자 어댑터와 그 기능에 대한 감을 익히면 반복자가 더 이해하기 쉬울 수 있습니다. 루프의 다양한 부분들을 조작하고 새로운 벡터를 구성하는 대신, 코드는 루프의 상위 수준 목표에 집중합니다. 이는 일반적인 코드를 추상화하여, 반복자의 각 요소가 통과해야 하는 필터링 조건과 같이 이 코드에 고유한 개념을 더 쉽게 볼 수 있도록 합니다.

하지만 두 구현이 실제로 동일할까요? 직관적인 가정은 하위 수준 루프가 더 빠를 것이라는 것입니다. 성능에 대해 이야기해 봅시다.

요약

축하합니다! I/O 프로젝트 개선 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.