이번 코드는 주어진 문자열에서 특정 문자 위치 범위를 추출하고, 그 범위에 해당하는 문자만 필터링하여 출력하는 프로그램이다.
전체 코드
use std::io::{self, BufRead};
use std::env;
enum Range {
Single(usize),
FromStart(usize),
ToEnd(usize),
Between(usize, usize),
}
impl Range {
fn parse(s: &str) -> Option<Range> {
let parts: Vec<&str> = s.split('-').collect();
match parts.len() {
1 => parts[0].parse().ok().map(Range::Single),
2 => {
if parts[0].is_empty() {
parts[1].parse().ok().map(Range::FromStart)
} else if parts[1].is_empty() {
parts[0].parse().ok().map(Range::ToEnd)
} else {
let start = parts[0].parse().ok()?;
let end = parts[1].parse().ok()?;
if start <= end {
Some(Range::Between(start, end))
} else {
None
}
}
}
_ => None,
}
}
fn contains(&self, n: usize) -> bool {
match self {
Range::Single(x) => *x == n,
Range::FromStart(x) => n <= *x,
Range::ToEnd(x) => n >= *x,
Range::Between(x, y) => n >= *x && n <= *y,
}
}
}
fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
eprintln!("Usage: ./cut <ranges>");
return Ok(());
}
let ranges: Vec<Range> = args[1]
.split(',')
.filter_map(|s| Range::parse(s))
.collect();
if ranges.is_empty() {
eprintln!("Invalid range input.");
return Ok(());
}
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
let mut output = String::new();
for (i, ch) in line.chars().enumerate() {
if ranges.iter().any(|r| r.contains(i + 1)) {
output.push(ch);
}
}
output.push('\n');
print!("{}", output);
}
Ok(())
}
부분 코드
Enum
enum Range {
Single(usize),
FromStart(usize),
ToEnd(usize),
Between(usize, usize),
}
주어진 코드는 Rust 언어로 작성된 enum 구조체이다. 이를 자세히 설명하면:
Range
는 enum 타입이며, 4가지의 다른 variant로 구성되어 있다.
-
Single(usize)
: 이 variant는 하나의usize
타입의 값만을 가진다. 예를 들면, 특정 위치의 하나의 원소를 가리킬 때 사용할 수 있다.예시:
let x = Range::Single(5);
-
FromStart(usize)
: 시작부터 주어진usize
위치까지의 범위를 의미한다.예시:
let x = Range::FromStart(3);
-
ToEnd(usize)
: 주어진usize
위치부터 끝까지의 범위를 의미한다.예시:
let x = Range::ToEnd(4);
-
Between(usize, usize)
: 두 개의usize
값을 가지며, 이 두 값 사이의 범위를 의미한다.예시:
let x = Range::Between(2, 6);
여기서 usize
는 Rust에서 제공하는 부호 없는 정수 타입이다. 주로 컬렉션의 크기나 인덱스와 같은 값들을 나타내기 위해 사용한다.
결론적으로, 이 Range
enum은 다양한 범위를 표현하기 위한 구조체이다.
impl Range
네, impl Range
블록 내에는 Range
enum에 대한 parse와 contains 2개의 함수를 구현하고 있다.
-
parse(s: &str) -> Option<Range>
: 문자열을 받아서 해당 문자열을 기반으로Range
enum의 variant로 변환하는 함수이다. 성공적으로 변환되면Some(Range)
를 반환하고, 변환에 실패하면None
을 반환한다. -
contains(&self, n: usize) -> bool
: 주어진 숫자가Range
범위 내에 포함되어 있는지를 판단하는 함수이다. 포함되면true
를 반환하고, 아니면false
를 반환한다.
impl
키워드는 Rust에서 "구현"을 의미하며, 주로 구조체나 enum에 대한 메서드를 정의할 때 사용된다. 즉, impl Range
는 Range
enum에 대한 메서드들을 구현하는 블록이라고 볼 수 있다.
다시 말해서, impl
블록은 특정 타입에 메서드를 추가하는 방법이다. 이 메서드들은 해당 타입의 인스턴스에 대해 호출될 수 있다. Rust에서는 객체 지향 프로그래밍 언어의 클래스 메서드와 유사한 기능을 impl
블록을 통해 제공한다.
Range
enum에 대한 함수를 구현한다는 것은, 해당 Range
타입의 각 variant에 대해 어떠한 동작이나 행위를 수행할 수 있는 함수나 메서드를 정의하는 것을 의미한다.
Enum은 여러 개의 variant로 구성된 타입이다. Rust에서 enum에 함수를 구현하는 것은, 그 enum의 각 variant에 대해 어떠한 동작을 해야 하는지나, enum 전체에 대한 공통 동작을 정의하는 것을 가능하게 한다.
예를 들어, 위에서 제공된 contains
함수는 Range
의 각 variant에 대해 주어진 숫자가 그 범위에 포함되는지 확인한다. 함수 내부의 match
문은 self
가 어떤 variant인지에 따라 다른 동작을 수행한다.
이러한 방식으로, enum은 단순히 여러 가능한 값을 나열하는 것을 넘어, 각 값에 대한 특정 동작이나 로직을 구현할 수 있게 된다. 이는 Rust에서 enum을 더욱 강력하게 만드는 중요한 특성 중 하나이다.
결론적으로, Range
enum에 대한 함수를 구현한다는 것은, 해당 enum 타입에 메서드를 추가하여 해당 타입이 수행할 수 있는 동작을 확장하거나 정의하는 것이다. 이를 통해 해당 enum 타입을 더욱 다양한 방식으로 사용할 수 있게 된다.
parse 함수
// parse라는 함수를 정의한다. 이 함수는 문자열 참조 &str을 인자로 받고, Option<Range> 타입을 반환한다. Option<Range>는 Range 타입의 값을 가질 수도 있고, 값이 없을 수도 있는 것을 의미한다.
fn parse(s: &str) -> Option<Range> {
// 입력 문자열 s를 '-' 문자를 기준으로 분리한 후, 그 결과를 parts 변수에 저장한다.
// '-'문자를 기준으로 분리할 때 '-'가 존재하면, 그 앞 혹은 뒤는 ""로 구분이 된다.
let parts: Vec<&str> = s.split('-').collect();
// parts의 길이를 확인하여, 얼마나 많은 부분으로 문자열이 분리되었는지 확인한다.
// 예를 들어, 단일 숫자만 있는 1, 8, 13 등은 ["1"], ["8"], ["13"]과 같이 하나의 부분만으로 구성된다.
// 하지만, -가 포함된 숫자인 -3, 5-, 8-13 등은 ["", "3"], ["5", ""], ["8", "13"]으로 parts가 구분된다.
match parts.len() {
// parts의 길이가 1이면, parts[0]을 숫자로 변환하려고 시도한다. 변환에 성공하면 Range::Single로 변환한다.
1 => parts[0].parse().ok().map(Range::Single),
// parts의 길이가 2인 경우의 로직을 시작한다.
2 => {
// 첫 번째 부분이 비어 있는지 확인한다. 이 경우, -3, -5 등과 같이 처음부터 해당 숫자까지의 길이를 추출하는 로직이다.
if parts[0].is_empty() {
// 첫 번째 부분이 비어 있으면, 두 번째 부분을 숫자로 변환하려고 시도하고, 변환에 성공하면 Range::FromStart로 변환한다.
parts[1].parse().ok().map(Range::FromStart)
}
// 두 번째 부분이 비어 있는지 확인한다.
else if parts[1].is_empty() {
// 두 번째 부분이 비어 있으면, 첫 번째 부분을 숫자로 변환하려고 시도하고, 변환에 성공하면 Range::ToEnd로 변환한다. 즉, 숫자부터 라인의 끝까지를 추출한다.
parts[0].parse().ok().map(Range::ToEnd)
}
// 두 부분 모두 비어 있지 않은 경우의 로직을 시작한다.
// 이 경우는 특정 숫자부터 다른 숫자까지의 범위를 추출하는 경우이다.
else {
// 첫 번째 부분을 숫자로 변환하려고 시도한다. 변환에 실패하면 바로 None을 반환한다.
let start = parts[0].parse().ok()?;
// 두 번째 부분을 숫자로 변환하려고 시도한다. 변환에 실패하면 바로 None을 반환한다.
let end = parts[1].parse().ok()?;
// start가 end보다 작거나 같은지 확인한다.
if start <= end {
// 조건이 참이면, Range::Between로 변환한다.
Some(Range::Between(start, end))
}
// start가 end보다 큰 경우의 로직을 시작한다.
else {
// 무효한 범위이므로 None을 반환한다.
None
}
}
}
// parts의 길이가 1이나 2가 아닌 다른 값인 경우 None을 반환한다.
_ => None,
}
}
추가 설명은 아래에 달아보겠다.
우선 아래의 이 코드를 분석해보자.
1 => parts[0].parse().ok().map(Range::Single),
이 코드는 parts
의 길이가 1일 때 실행되는 코드이다. 그러면 각 부분을 하나씩 살펴보자.
parts[0]
:parts
벡터의 첫 번째 요소를 참조한다. 즉, parts의 길이가 1이므로 첫 번째 요소만 참조하면 되겠다..parse()
:parse
메서드는 문자열을 특정 타입으로 변환하려고 시도한다. 여기서는 기본적으로usize
타입으로의 변환을 시도한다고 가정할 수 있다..ok()
:parse
메서드는Result
타입을 반환하는데,.ok()
메서드는Result
를Option
타입으로 변환한다. 만약parse
가 성공적이라면Some
을, 실패라면None
을 반환한다..map(Range::Single)
:map
메서드는Option
타입의 값을 다른 값으로 변환한다. 여기서는 성공적으로 변환된 숫자를Range::Single
variant로 변환한다. 만약parts[0]
의 변환이 실패하여None
이 반환되면,map
메서드는 아무런 동작도 하지 않고None
을 그대로 반환한다.
따라서 전체적인 흐름은 parts[0]
문자열을 숫자로 변환하려고 시도하고, 성공하면 그 숫자를 Range::Single
variant로 감싸서 반환하는 것이다.
map
함수에 대한 추가 설명:
map
함수는 Option
타입에 대해 정의된 메서드로, Some
값 내부의 값을 변환하거나 조작할 수 있게 해준다. 만약 Option
값이 None
이라면, map
함수는 아무런 동작도 하지 않고 None
을 그대로 반환한다. 만약 값이 Some
이라면, 해당 값을 함수에 전달하고 함수의 반환 값을 Some
으로 감싸서 반환한다.
map
함수는 주로 Option
과 Result
와 같은 Rust의 enum 타입에 대해 사용되는 메서드다. 이 함수의 주요 기능은 내부의 값을 변환하는 것이다.
Option
에 대한 map
Option
타입은 다음과 같은 두 가지 variant로 구성된다:
Some(T)
- 어떤 값T
를 가지고 있다.None
- 값이 없다.
map
함수를 사용하면, Option
내부의 값 (만약 존재한다면)에 어떤 함수를 적용할 수 있다.
fn main() {
let x: Option<i32> = Some(2);
let y = x.map(|v| v * 2);
println!("{:?}", y); // Some(4)
}
여기서 x
는 Some(2)
라는 값을 가지고 있다. map
함수를 사용하여 x
내부의 값에 함수를 적용하여 그 값을 2배로 만들었다. 결과적으로 y
는 Some(4)
값을 가진다.
Some
은 Rust 언어의 Option
타입에 속한 하나의 variant(변형)이다. Option
타입은 어떤 값이 있을 수도 있고 없을 수도 있는 상황을 나타내기 위해 사용된다.
Option
에는 두 가지 variant가 있다:
Some(T)
: 값이 있다. 여기서T
는 어떤 타입의 값이다. 예를 들면,Some(5)
는 정수 값 5가 있다는 것을 의미한다. 또는Some("안녕")
은 문자열 "안녕"이 있다는 것을 의미한다.None
: 값이 없다.
예를 들어, 우리가 상자를 가지고 있을 때, 그 상자 안에 무언가가 있을 수도 있고 비어있을 수도 있다고 생각해보자.
- 상자 안에 사과가 들어있다면, 그건
Some("사과")
로 표현할 수 있다. - 상자가 비어있다면, 그건
None
으로 표현된다.
이렇게 Some
과 None
은 값을 가지고 있는지 아닌지를 나타내는 데 사용된다.
Result
에 대한 map
Result
는 성공 값과 오류 값을 포함하는 두 가지 variant로 구성된다:
Ok(T)
- 성공적인 결과 값T
를 가지고 있다.Err(E)
- 오류 값E
를 가지고 있다.
map
함수를 사용하면, Result
의 성공적인 값에 함수를 적용할 수 있다. 오류 값에는 적용되지 않는다.
fn main() {
let x: Result<i32, &'static str> = Ok(3);
let y = x.map(|v| v + 1);
println!("{:?}", y); // Ok(4)
}
여기서 x
는 Ok(3)
이라는 값을 가지고 있다. map
함수를 사용하여 x
내부의 성공적인 값에 함수를 적용하여 그 값을 1만큼 증가시켰다. 결과적으로 y
는 Ok(4)
값을 가진다.
이와 같이, map
함수는 Option
이나 Result
와 같은 타입 내부의 값을 변환하는데 매우 유용하다.
참고로 let x 부분의 코드는 Rust에서 사용되는 Result
타입의 변수 x
를 선언하고 초기화하는 코드이다. 코드의 각 부분을 쉽게 이해할 수 있도록 분해해서 설명하겠다.
-
let x
:x
라는 이름의 변수를 선언한다. -
: Result<i32, &'static str>
:x
의 타입을Result
로 지정한다.Result
는 두 가지 가능한 값을 가질 수 있다:Ok
와Err
.Ok
는 성공적인 결과를 나타낸다. 이 경우,Ok
안에는i32
타입의 값 (정수)이 들어간다.Err
는 오류를 나타낸다. 이 경우,Err
안에는&'static str
타입의 값 (문자열 참조)이 들어간다.
-
= Ok(3);
:x
를Ok
의 값3
으로 초기화한다. 즉,x
는 성공적인 결과로 정수3
을 가진다.
결론적으로, 이 코드는 x
라는 이름의 변수를 선언하고, 그 변수에 성공적인 결과 3
을 할당한다. 만약 나중에 x
의 값이 오류로 변경되면, 그 오류는 문자열 참조 (&'static str
) 형태로 표현될 것이다.
rust에서 사용되는 기존과 다른 용법의 map
함수에 대해 아주 쉽게 설명해 보겠다.
map
함수는 마치 마법의 상자 같은 것이다. 이 상자 안에 물건이 있을 수도 있고, 없을 수도 있다.
-
Option
과map
:Some
: 상자 안에 물건이 있다.None
: 상자 안에 물건이 없다.
예를 들어, 상자 안에 사과가 있다면 (
Some("사과")
),map
함수를 사용해서 이 사과를 바나나로 바꿀 수 있다. 그럼 상자 안에는 이제 바나나가 있다. 만약 상자 안에 아무것도 없다면 (None
),map
함수를 사용해도 상자는 여전히 비어있다. -
Result
와map
:Ok
: 상자 안에 좋은 물건이 있다.Err
: 상자 안에 나쁜 물건이 있다.
예를 들어, 상자 안에 케이크가 있다면 (
Ok("케이크")
),map
함수를 사용해서 이 케이크를 아이스크림으로 바꿀 수 있다. 그럼 상자 안에는 이제 아이스크림이 있다. 만약 상자 안에 나쁜 물건이 있다면,map
함수를 사용해도 그 나쁜 물건은 그대로 있다.
결론적으로, map
은 상자 안의 좋은 물건을 다른 좋은 물건으로 바꾸는 마법과 같다. 상자 안에 아무것도 없거나 나쁜 물건이 있다면, 아무런 변화도 일어나지 않는다.
map
은 Option
타입에 사용되는 메서드 중 하나이다. Option
은 두 가지 상태 중 하나를 나타내는 타입이다:
Some(값)
: 어떤 값을 가지고 있다.None
: 값이 없다.
map
메서드를 사용하면 Option
안에 있는 값 (만약 있다면)을 바꿀 수 있다.
예를 들어, 우리가 작은 상자를 가지고 있고 그 안에 장난감 자동차가 있다고 생각해보자 (Some("장난감 자동차")
). 이제 map
을 사용하면, 그 상자 안의 장난감 자동차를 장난감 비행기로 바꿀 수 있다. 그러면 상자 안에는 이제 장난감 비행기가 있다.
하지만, 만약 상자가 비어있다면 (None
), map
을 사용해도 상자는 그대로 비어있게 된다.
요약하면, map
은 Option
에 들어있는 값을 다른 값으로 바꾸는 도구이다. 그리고 만약 Option
이 비어있다면 (None
), map
은 아무런 동작도 하지 않는다.
다시 정리하면 이렇다.
1 => parts[0].parse().ok().map(Range::Single),
코드
parts
의 길이가 1일 때 실행되는 코드다.
이 코드를 따라가면, 다음과 같은 작업을 수행한다:
parts[0]
:parts
벡터의 첫 번째 요소를 가져온다..parse()
:parts[0]
의 값을 숫자 (usize
)로 변환하려고 시도한다..ok()
:parse
의 결과가Result
타입이다. 이를Option
타입으로 변환한다. 성공적으로 변환되면Some(값)
을 반환하고, 실패하면None
을 반환한다..map(Range::Single)
: 성공적으로 숫자로 변환된 값 (Some(값)
)에Range::Single
을 적용하여Range
enum의Single
variant로 만든다.
예를 들어, parts
가 ["13"]이라면:
parts[0]
은 "13"이다.- "13"을 숫자로 변환하면 13이다.
- 변환에 성공했으므로
Some(13)
이 반환된다. Some(13)
에Range::Single
을 적용하면 결과는Some(Range::Single(13))
이 된다.
따라서 parts
가 ["13"]일 때 결과는 Some(Range::Single(13))
이다.
2 => if parts[0].is_empty() { parts[1].parse().ok().map(Range::FromStart) } 코드
parts
가 ["", "8"]일 때 ("-8"
을 '-'로 분리한 결과), 주어진 코드는 다음과 같이 작동한다:
parts[0].is_empty()
는true
이다. 왜냐하면parts[0]
(즉, ["", "8"]의 첫 번째 요소)는 빈 문자열이기 때문이다.- 그러므로 코드 내부가 실행된다:
parts[1].parse().ok().map(Range::FromStart)
parts[1]
는 "8"이다.- "8"은
.parse()
메서드를 통해 숫자 8로 변환된다. - 변환이 성공적으로 이루어지면,
ok()
메서드를 통해Some(8)
으로 반환된다. map(Range::FromStart)
를 사용하면,Some(8)
은Some(Range::FromStart(8))
로 변환된다.
결과적으로, parts
가 ["", "8"]일 때, 주어진 코드는 Some(Range::FromStart(8))
을 반환한다.
나머지 케이스 들도 위와 마찬가지로 Some(Range::ToEnd(10)
, Some(Range::Between(12,15))
등으로 변환이 되는 것이다.
fn contains(&self, n: usize) -> bool {
match self {
Range::Single(x) => *x == n,
Range::FromStart(x) => n <= *x,
Range::ToEnd(x) => n >= *x,
Range::Between(x, y) => n >= *x && n <= *y,
}
}
contains
함수는 Range
enum의 인스턴스와 주어진 숫자 n
을 받아, 해당 숫자가 Range
의 범위 내에 포함되는지 여부를 확인하는 역할을 한다.
즉, contains
함수는 주어진 Range
enum 내에 특정 숫자가 포함되어 있는지, 즉 숫자의 유효성을 검증하기 위한 함수이다. 이 함수를 사용하면 특정 범위나 조건에 숫자가 부합하는지 쉽게 확인할 수 있다.
예를 들어, Range::Between(1, 10)
이라는 범위를 설정했다면, contains
함수는 숫자가 1과 10 사이에 있는지 확인하는 역할을 한다. 이런 방식으로, Range
와 contains
함수는 숫자의 유효성을 체크하는데 사용될 수 있다.
Range
enum은 4가지 variant로 구성되어 있다:
Single(x)
: 하나의 숫자만을 포함한다.FromStart(x)
: 시작부터 특정 숫자까지의 범위를 포함한다.ToEnd(x)
: 특정 숫자부터 끝까지의 범위를 포함한다.Between(x, y)
: 두 숫자 사이의 범위를 포함한다.
contains
함수는 다음과 같이 동작한다:
Single(x)
:n
이x
와 동일한지 확인한다.FromStart(x)
:n
이x
이하인지 확인한다.ToEnd(x)
:n
이x
이상인지 확인한다.Between(x, y)
:n
이x
와y
사이에 있는지 확인한다.
예를 들어:
Range::Single(5).contains(5)
는true
를 반환한다.Range::FromStart(3).contains(2)
는true
를 반환한다.Range::ToEnd(7).contains(8)
는true
를 반환한다.Range::Between(4, 6).contains(5)
는true
를 반환한다.Range::Between(4, 6).contains(7)
는false
를 반환한다.
이 함수는 주어진 Range
의 범위 내에 특정 숫자가 포함되는지 확인할 때 사용된다. 사용자는 Range
의 인스턴스를 생성하고, contains
메서드를 호출하여 특정 숫자가 해당 범위 내에 있는지 알 수 있다.
&self
는 Rust에서 메서드의 첫 번째 인자로 자주 나타나는 특별한 파라미터다. 이것은 현재 메서드가 호출되는 인스턴스에 대한 참조를 나타낸다.
다른 언어의 this
키워드와 유사한 개념이다. 예를 들어, Java나 C++에서는 객체의 메서드 내에서 this
키워드를 사용하여 현재 객체에 접근할 수 있다. Rust에서는 self
를 사용한다.
&self
는 불변 참조로, 메서드 내에서 객체의 상태를 변경할 수 없다. 만약 객체의 상태를 변경하는 메서드가 필요하다면, &mut self
를 사용하여 가변 참조를 받아야 한다.
간단한 예시:
struct Person {
name: String,
age: u32,
}
impl Person {
fn say_hello(&self) {
println!("안녕하세요! 제 이름은 {}이고, 나이는 {}살이다.", self.name, self.age);
}
}
위의 예시에서 say_hello
메서드는 &self
를 사용하여 Person
구조체의 인스턴스에 접근한다. 이 메서드 내에서는 self.name
과 self.age
를 통해 이름과 나이에 접근할 수 있다.
contains
함수 내에서 self
를 사용하고 있다. 그러나 self.
접두사를 명시적으로 사용하지 않아도 된다. 왜냐하면 match
문에서 직접 self
를 참조하고 있기 때문이다.
match self {
Range::Single(x) => *x == n,
Range::FromStart(x) => n <= *x,
Range::ToEnd(x) => n >= *x,
Range::Between(x, y) => n >= *x && n <= *y,
}
여기서 match self
부분에서 self
의 현재 값 (즉, Range
의 인스턴스)을 검사하고 있다. 각각의 Range
variant (예: Single(x)
, FromStart(x)
등)를 통해 해당 인스턴스의 내부 값에 접근할 수 있다.
따라서 self
를 인자로 받는 이유는 contains
함수가 호출되는 Range
인스턴스의 상태와 내부 값에 접근하기 위해서이다.
let ranges: Vec<Range> = args[1]
.split(',')
.filter_map(|s| Range::parse(s))
.collect();
해당 코드를 부분별로 쪼개서 설명하겠다.
- 변수 선언:
let ranges: Vec<Range>
이 부분은 ranges
라는 이름의 변수를 선언한다. 이 변수의 타입은 Range
타입의 요소를 담는 벡터(Vec<Range>
)이다.
- 문자열 분리:
args[1].split(',')
일반적으로 Rust에서 커맨드 라인 인자를 다룰 때, args
벡터의 첫 번째 요소인 args[0]
에는 프로그램의 이름이 저장되어 있다. 그래서 실제로 프로그램에 전달되는 첫 번째 인자는 args[1]
에 위치하게 된다.
예를 들어, 다음과 같이 커맨드 라인에서 프로그램을 실행한다고 가정해보자:
$ my_program input.txt
여기서 args
벡터는 다음과 같이 구성된다:
args[0]
= "my_program"args[1]
= "input.txt"
따라서, 프로그램의 이름을 가져오려면 args[0]
를 사용하고, 실제로 전달된 첫 번째 인자를 가져오려면 args[1]
을 사용한다.
args[1]
은 args
벡터의 두 번째 요소를 가져온다. 그리고 split(',')
은 이 문자열을 쉼표(,)를 기준으로 여러 부분으로 나눈다.
- 문자열을
Range
로 변환:
.filter_map(|s| Range::parse(s))
filter_map
은 변환 함수와 필터를 한 번에 수행하는 함수다. 여기서는 각 문자열 s
를 Range::parse(s)
를 사용하여 Range
타입으로 변환한다. 만약 Range::parse(s)
가 None
을 반환하면, 해당 요소는 결과에 포함되지 않는다.
여기서 s
는 람다 함수(또는 클로저)의 매개변수로, split(',')
메서드에 의해 생성된 각 문자열 슬라이스를 참조한다.
.filter_map(|s| Range::parse(s))
위의 코드에서 filter_map
은 입력 컬렉션의 각 요소에 대해 람다 함수를 호출한다. 이 람다 함수는 split(',')
에 의해 분리된 각 문자열 슬라이스 (s
)를 받아 Range::parse(s)
를 호출하여 해당 문자열을 Range
타입으로 변환하려고 시도한다.
간단히 말하면, s
는 쉼표로 구분된 입력 문자열의 각 부분을 나타낸다.
- 결과 수집:
.collect();
collect
메서드는 이전 단계에서 처리된 결과를 최종적인 데이터 구조에 모은다. 이 경우, Range
타입의 요소를 담는 벡터로 결과를 모은다.
결론적으로, 이 코드는 args[1]
에 있는 쉼표로 구분된 문자열을 받아, 각 문자열을 Range
타입으로 변환하고, 이 변환된 결과를 ranges
벡터에 저장한다.
Range
는 다음과 같은 네 가지 variant(변형)을 갖는 enum
타입이다:
- Single(usize): 하나의 값을 포함한다. 예:
Range::Single(5)
- FromStart(usize): 시작부터 주어진 값까지의 범위를 나타낸다. 예:
Range::FromStart(3)
는 0부터 3까지의 범위를 의미한다. - ToEnd(usize): 주어진 값부터 끝까지의 범위를 나타낸다. 예:
Range::ToEnd(7)
는 7부터 무한대까지의 범위를 의미한다. - Between(usize, usize): 두 숫자 사이의 범위를 나타낸다. 예:
Range::Between(4, 6)
는 4부터 6까지의 범위를 의미한다.
이 네 가지 variant는 Range
타입이 나타낼 수 있는 다양한 숫자 범위를 표현한다.
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
let mut output = String::new();
for (i, ch) in line.chars().enumerate() {
if ranges.iter().any(|r| r.contains(i + 1)) {
output.push(ch);
}
}
output.push('\n');
print!("{}", output);
}
이 코드는 주어진 범위에 포함된 문자만을 입력된 각 라인에서 선택하여 출력하는 역할을 한다. 코드의 각 부분을 세부적으로 분석해 보겠다.
- 표준 입력 가져오기:
let stdin = io::stdin();
io::stdin()
함수를 사용하여 표준 입력 핸들을 가져온다.
- 입력된 라인 별로 처리:
for line in stdin.lock().lines() {
stdin.lock()
은 표준 입력에 대한 동기화된 접근을 제공한다.
stdin.lock()
는 표준 입력에 대한 동기화된 (synchronized) 접근을 제공한다. Rust에서는 동시성 문제를 처리하기 위해 이러한 동기화 메커니즘이 필요하며, 표준 입력을 안전하게 여러 위치에서 동시에 접근하려면 lock()
을 사용해야 한다.
자세히 설명하면:
-
동기화(Synchronization): Rust의 표준 라이브러리는 I/O 작업을 위해
Read
및Write
트레잇을 제공한다.Stdin
(표준 입력) 역시Read
트레잇을 구현하므로, 여러 위치에서 동시에 읽는 것을 방지하기 위해 동기화가 필요하게 된다. -
왜
lock()
을 사용하는가?: 만약 여러 스레드가 동시에 표준 입력에 접근하려고 시도한다면, 입력 데이터가 꼬일 수 있다. 이를 방지하기 위해lock()
메서드를 사용하여 한 번에 하나의 스레드만 표준 입력에 접근하도록 한다. -
반환 값:
stdin.lock()
은StdinLock
타입의 객체를 반환한다. 이 객체는 생명 주기 동안 표준 입력에 대한 독점적인 접근을 제공한다.StdinLock
역시Read
트레잇을 구현하므로, 표준 입력을 읽는데 필요한 모든 메서드 (예:lines()
,bytes()
)를 사용할 수 있다.
요약하면, stdin.lock()
은 표준 입력을 안전하게 동기화하여 여러 위치에서 동시 접근을 방지하며, 표준 입력을 읽는데 필요한 메서드들을 계속 사용할 수 있게 해준다.
lines()
메서드는 표준 입력에서 한 줄씩 읽는다.
- 라인 읽기:
let line = line?;
이 부분에서는 각 라인을 읽고 만약 라인 읽기에 실패하면, 오류를 반환한다.
- 출력 문자열 초기화:
let mut output = String::new();
선택된 문자들을 저장할 output
문자열을 초기화한다.
- 라인 내의 각 문자 처리:
for (i, ch) in line.chars().enumerate() {
chars()
는 문자열의 각 문자를 반환하고, enumerate()
는 각 문자와 그 문자의 인덱스를 함께 반환한다.
enumerate()
는 Rust의 Iterator 트레잇에 정의된 메서드로, 이터레이터의 각 아이템과 그 아이템의 인덱스를 함께 반환한다.
-
기본 개념:
enumerate()
는 원본 이터레이터의 각 아이템에 대해(인덱스, 아이템)
형태의 튜플을 반환한다. 인덱스는 0부터 시작한다. -
사용 예제:
let fruits = vec!["apple", "banana", "cherry"];
for (index, fruit) in fruits.iter().enumerate() {
println!("Index {}: {}", index, fruit);
}
위 코드는 다음과 같은 출력을 생성한다:
Index 0: apple
Index 1: banana
Index 2: cherry
-
동작 방식:
enumerate()
를 호출하면Enumerate
타입의 새로운 이터레이터가 반환된다. 이Enumerate
이터레이터는 원본 이터레이터와 내부 카운터(0부터 시작)를 함께 유지한다. 각 반복마다 카운터는 1씩 증가하며,(인덱스, 아이템)
형태의 튜플을 생성한다. -
활용:
enumerate()
는 원본 이터레이터의 아이템과 그 아이템의 위치(인덱스)를 동시에 알아야 할 때 유용하다. 예를 들어, 배열이나 벡터의 특정 아이템의 위치를 알아야 할 때, 아이템의 순서를 기록하거나, 특정 조건에 따라 아이템의 위치에 따라 다르게 처리해야 할 때 등에 사용된다.
간단히 말하면, enumerate()
는 이터레이터의 아이템과 그 아이템의 위치를 함께 제공하는 유용한 메서드다.
즉, line.chars().enumerate()
를 사용할 때:
-
i
: 각 문자의 인덱스가 저장된다. 첫 번째 문자의 인덱스는 0, 두 번째 문자의 인덱스는 1, 이런 식으로 계속 증가한다. -
ch
:line
의 문자열에서 차례로 추출되는 각각의 문자(character)가 저장된다.
예를 들어, line
이 "hello"라면:
- 첫 번째 반복에서
i
는 0,ch
는 'h' - 두 번째 반복에서
i
는 1,ch
는 'e' - ...
- 다섯 번째 반복에서
i
는 4,ch
는 'o'
이런 방식으로 for
루프는 line
문자열의 모든 문자를 처음부터 끝까지 차례로 처리하게 된다.
- 범위 내의 문자만 선택:
if ranges.iter().any(|r| r.contains(i + 1)) {
output.push(ch);
}
만약 현재 문자의 위치(인덱스)가 주어진 ranges
범위 내에 있다면, 해당 문자를 output
문자열에 추가한다.
해당 코드는 문자열의 특정 위치에 있는 문자가 주어진 범위(ranges
)에 포함되는지 확인하고, 포함되면 해당 문자를 output
문자열에 추가하는 역할을 한다. 각 부분별로 살펴보겠다.
- 범위 이터레이터 생성:
ranges.iter()
iter()
메서드는 ranges
벡터의 각 요소에 대한 불변 참조를 제공하는 이터레이터를 생성한다.
- 범위 내 포함 여부 검사:
.any(|r| r.contains(i + 1))
any
는 이터레이터의 요소 중 하나라도 주어진 조건을 만족하면 true
를 반환하는 메서드다. 여기서는 람다 함수 |r| r.contains(i + 1)
를 조건으로 사용한다.
람다 함수는 ranges
벡터의 각 범위(r
)를 받아 해당 범위가 현재 문자의 위치(i + 1
)를 포함하는지 확인한다. (i + 1
를 사용하는 이유는 인덱스가 0부터 시작하기 때문에 1을 더해 실제 위치를 나타낸다.)
여기서 contains
는 이전에 제공된 Range
열거형의 구현 내에서 정의된 메서드이다. 이 메서드는 주어진 숫자가 해당 범위 내에 포함되어 있는지 확인하는 역할을 한다.
코드에서 사용된 `contains` 메서드의 정의에 따르면:
- `Range::Single(x)`: 주어진 숫자가 `x`와 동일한지 확인한다.
- `Range::FromStart(x)`: 주어진 숫자가 `x` 이하인지 확인한다.
- `Range::ToEnd(x)`: 주어진 숫자가 `x` 이상인지 확인한다.
- `Range::Between(x, y)`: 주어진 숫자가 `x`와 `y` 사이에 있는지 확인한다.
따라서, 코드에서 `r.contains(i + 1)`를 호출할 때:
- `r`은 `ranges` 벡터의 각 `Range` 요소를 참조한다.
- `i + 1`은 현재 문자의 위치를 나타낸다. (인덱스 `i`는 0부터 시작하기 때문에, 실제 위치를 나타내기 위해 1을 더한다.)
`ranges.iter().any(|r| r.contains(i + 1))` 전체 표현식은 "주어진 벡터의 `Range` 중 어떤 범위에도 현재 문자의 위치(`i + 1`)가 포함되어 있는지"를 확인한다. 포함되면 `true`를 반환하고, 그렇지 않으면 `false`를 반환한다.
- 문자 추가:
output.push(ch);
만약 현재 문자의 위치가 ranges
벡터의 어느 범위에도 포함되면 (any
메서드가 true
를 반환하면), 해당 문자(ch
)를 output
문자열에 추가한다.
요약하면, 해당 코드는 ranges
벡터의 각 범위 중 하나라도 현재 문자의 위치를 포함하면 해당 문자를 output
문자열에 추가하는 역할을 합니다.
- 라인 끝에 개행 추가:
output.push('\n');
각 라인의 마지막에 개행 문자를 추가합니다.
- 결과 출력:
print!("{}", output);
변경된 라인을 출력합니다.
코드의 전반적인 동작은 표준 입력에서 라인을 읽어, 각 라인의 문자 중에서 주어진 ranges
범위에 속하는 문자만을 선택하여 출력하는 것입니다.
Ok(())
는 Rust에서 일반적으로 사용되는 패턴 중 하나로, 주로 에러를 반환할 수 있는 함수나 메서드에서 "성공적으로 완료되었으며, 특별한 값은 반환하지 않는다"는 것을 나타낼 때 사용한다.
-
Ok(())
의 의미:Ok
는Result
열거형의 한 변형이며, 연산이 성공적으로 완료되었음을 나타낸다.()
는 Rust에서 "유닛 타입" 또는 "빈 튜플"이라고 부르는 것으로, 아무런 값을 가지지 않는다.
-
왜 추가하는가?:
- 만약 함수의 반환 타입이
Result<(), ErrorType>
이라면, 이 함수는 두 가지 가능한 결과를 반환할 수 있다:Ok(())
(성공) 또는Err(some_error)
(실패). - 이런 패턴은 함수가 에러를 반환할 수 있을 때 주로 사용되며, 성공적인 실행의 결과로 특별한 값을 반환할 필요가 없을 때
Ok(())
를 사용한다.
- 만약 함수의 반환 타입이
-
생략하면 어떻게 되는가?:
- 만약 함수나 메서드의 반환 타입이
Result
이고, 마지막 라인에서Ok(())
를 반환하지 않는다면 컴파일 오류가 발생한다. - Rust의 함수와 메서드는 마지막 표현식을 자동으로 반환하는데, 이 표현식의 타입이 함수의 선언된 반환 타입과 일치하지 않으면 오류가 발생한다.
- 만약 함수나 메서드의 반환 타입이
따라서, Ok(())
는 성공적인 완료를 나타내며, 함수의 반환 타입과 일치하는 값을 제공하기 위해 종종 필요하다.
이상으로 rust 예제 코드에 대한 분석을 마무리한다. 역시나 rust는 복잡하고 어렵다. 하지만, 코드를 분석해 가면서 이렇게 정리를 하면서 rust에 대한 이해도가 조금씩 높아짐을 느낀다. 그리고 이제는 코드의 syntax가 정해진 틀에서 벗어난다고 해서 크게 거부감을 갖지 않게 되기도 했다. 바로 지난 학기에 배운 OCaml 덕분이라고 할까? ㅋㅋㅋ
댓글 없음:
댓글 쓰기