Rust 패턴

mohadang·2023년 3월 4일
0

Rust

목록 보기
27/30
post-thumbnail

패턴에 사용할 수 있는 타입

  • 리터럴 값(Literals)
  • 분해한 배열(Array), 열거형(Enum), 구조체(Struct), 튜플(Tuple)
  • 변수(Variable)
  • 와일드카드(Wildcard)
  • 임시 값(Placeholders)

패턴이 사용될 수 있는 곳

match

match{
    패턴 => 표현,
    패턴 => 표현,
    패턴 => 표현,
}
  • match 표현에 대응 시킬 값이 가질 수 있는 모든 경우의 수를 빠짐 없이 표현해야 한다.
  • _라는 특별한 패턴은 아무 값에나 대응되지만 그 어떠한 변수에도 묶이지 않는다. 그래서 match의 마지막 갈래에 종종 쓰인다.(명시 하지 않은 값들을 무시할 때 유용)

if let {Option/Result} 조건 표현

주로 갈래가 하나 밖에 없는 match 표현을 더 짧게 표현하는 방법으로 사용
그냥 if 문과는 동작 기능이 약간 다르다.

ex) 그냥 if
흔히 다른 언어에서도 볼 수 있는 if 문이다.

fn main() {

    let favorite_color = "red";
    if favorite_color == "red" { // 값 비교 후 적절한 흐름으로 이동한다.
        println!("Using your favorite color, {}, as the background", favorite_color);
    } else {
        println!("Using blue as the background color");
    }
}

ex) if let
그냥 if라기 보다는 단순해진 match와 비슷하다.

fn main() {
    let favorite_color: Option<&str> = Some("red");
    if let Some(color) = favorite_color { // 값 비교가 아닌 타입이 맞는지 비교
    	//color라는 값으로 받아온다.
        println!("Using your favorite color, {}, as the background", color);
    } else {
        println!("Using blue as the background color");
    }
}

if let을 사용하면 여러 타입들을 같은 흐름 처리에 둘 수 있다.

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(color) = favorite_color { // &str 타입
        println!("Using your favorite color, {}, as the background", color);
    } else if is_tuesday { // bool 타입
        println!("Tuesday is green day!, {}", is_tuesday);
    } else if let Ok(age) = age { // u8 타입
        if age > 30 {
            println!("Using purple as the background color");
        } else {
            println!("Using orange as the background color");
        }
    } else {
        println!("Using blue as the background color");
    }
}

match는 이렇게 못한다.

fn main() {
    let favorite_color: Option<&str> = None;
    let is_tuesday = true;
    let age: Result<u8, _> = "34".parse();

    match favorite_color {
        Some(color) => {
            println!("Using your favorite color, {}, as the background", color)
        },
        is_tuesday => {
            // println!("Tuesday is green day! {}", is_tuesday);
        },
        age => {
            // if age > 30 {
            //     println!("Using purple as the background color");
            // } else {
            //     println!("Using orange as the background color");
            // }            
        },
        None => {
            println!("Using blue as the background color");
        },
    }
}

특이한 점은 컴파일은 된다. 하지만 is_tuesday 값이나 age 값을 사용하려고 하면 컴파일 에러가 바로 발생한다. 이유는 favorite_color가 Option<&str>이기 때문에 Option<&str> 타입으로 레이블 변수들을 해석하기 떄문이다.

while let 조건 루프

while루프는 pop이 Some을 반환 할 동안 실행한다.
None이 반환되는 경우에 더 이상 패턴에 맞지 않으므로 멈춘다.

let mut stack = Vec::new();

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

while let Some(top) = stack.pop() {
    println!("{}", top);
}

for 루프

for 루프의 패턴은 for 키워드 바로 다음에 오는 값이다.(for x in y에서 x)

let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
    println!("{} is at index {}", value, index);
}

let 구문

변수 선언인줄 알았던 let 역시 패턴이다.
let PATTERN = EXPRESSION;
변수명 자체가 하나의 무지하게 단순한 패턴의 한 형태이다.
러스트는 EXPRESSION을 PATTERN에 비교하고 거기서 발견한 이름들에 그 EXPRESSION을 대입한다.
let의 좀 더 "패턴 매칭"스러운 면모는 튜플을 사용할때 나타난다.

let (x, y, z) = (1, 2, 3);
// let (x, y) = (1, 2, 3); // error, 패턴 불일치

튜플을 분해하기 위해 (x, y, z) 패턴에 대응 시키고 있다.

함수의 매개변수

let도 패턴이니 함수의 매개변수도 당연히 패턴이다.

fn print_coordinates(&(x, y): &(i32, i32)) {
    println!("Current location: ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    print_coordinates(&point);
}

반증 가능성(Refutability)

패턴은 2가지 형태가 존재한다.

  • 반증 가능 패턴
  • 반증 불가 패턴.
    반증 가능성이란 패턴이 매칭에 실패할지의 여부이다.

반증 불가 패턴

  • let x = 5의 x가 있다. x 패턴은 어떠한 값이 오건 대응하기 때문에 실패할 수 없고, 곧 반증 불가
  • let구문, for루프들은 반증 불가한 패턴만 허용
    • 패턴에 대응하는데 실패할 경우 프로그램이 할 수 있는 행동이 없기 때문입니다

반증 가능 패턴

  • if let Some(x) = a_value의 Some(x) 패턴은 a_value의 값이 None인 경우가 있다면 Some(x)에 대응하지 못하고 실패하게 된다. 즉 반증 가능하다.
  • if let과 while let표현은 반증 가능 패턴만 허용
    • 성공 여부에 따라 다른 행동을 하도록 설계 됐기 때문에 실패의 여지가 있는 패턴이 올 것을 가정(while let으로 stack않의 값을 하나씩 추출한 사례를 생각해보라)

반증 불가한 패턴이 필요한 곳에서 반증 가능 패턴을 쓰는 경우와 같이 잘못된 사용을 할 경우 에러가 발생한다.
이런 에러 메시지를 보았을때 반증 가능성 문제라는 것을 인식하고 문제를 해결해야 한다.

ex) let구문에서 반증 가능 패턴 Some(x)를 사용.

let Some(x) = some_option_value;

error[E0005]: refutable pattern in local binding: `None` not covered
   --> src\main.rs:3:9
    |
3   |     let Some(x) = some_option_value;
    |         ^^^^^^^ pattern `None` not covered

Some(x)의 가능한 모든 경우를 다루지 않았다고 에러가 출력 되지만 let 구문은 모든 경우를 다룰 수 없다.

ex) if let과 반증 불가 패턴

fn main() {
    if let x = 5 { // 반증 불가
        println!("{}", x);
    } else {
    	// 위 if 조건에서 반증될 일이 없기에 절대 실행 안됨
        println!("never run");
    }
}

2 |     if let x = 5 {
  |        ^^^^^^^^^
  |
  = note: `#[warn(irrefutable_let_patterns)]` on by default
  
  irrefutable_let_patterns이라고 알려준다(irrefutable : 반증 불가)

컴파일과 정상 실행은 되지만 경고가 발생한다.

기타

match 사용시 변수 쉐이딩에 주의해야 한다.

변수 쉐이딩이 발생하여 의도했던 흐름 처리가 안될 수 있다.

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(y) => {
            // y 변수 쉐이딩에 의하여 여기서 매치 발생
            println!("Matched, y = {:?}", y) // 5 출력
        },
        _ => println!("Default case, x = {:?}", x),
    }
    // 여기서 y는 처음에 선언한 y 변수를 의미
    println!("at the end: x = {:?}, y = {:?}", x, y); // ... y = 10 출력
}

변수 이름을 중복해서 남발하는것은 Rust 역시 지양해야한다.

다중 매치(|)

|를 사용해서 여러 패턴을 조합할 수 있다.

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

범위 매치(...)

... 구문을 이용해 값의 범위 내에 매치

let x = 5;

match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}

..= 에서 = 을 생략할 수 없다

char 값도 사용 가능

let x = 'c';
match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

값을 해체하여 분리

패턴을 이용해 구조체, 열거형, 튜플, 참조자 등의 값들을 해체(destructuring)할 수도 있다.

구조체 해체

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };
    let Point { x: a, y: b } = p; // a, b는 어디서 ??
    assert_eq!(0, a);
    assert_eq!(7, b);
}

이 코드는 p 변수의 필드인 x 와 y 에 각각 대응되는 a 와 b 변수를 생성한다.
변수의 이름이 꼭 구조체의 필드명과 일치할 필요는 없다. 하지만 해당 변수가 어떤 필드를 나타내는지 기억하기 쉽도록 필드명과 일치하도록 작성하는게 일반적인 상식일 것이다.

let Point { x: x, y: y } = p;

하지만 이러면 멤버 변수 값과 일치하여 혼동된다. 그리고 경고도 발생 시킨다.
생략 가능하니 생략해서 사용하는게 제일 좋다.

struct Point {
    x: i32,
    y: i32,
}
fn main() {
    let p = Point { x: 0, y: 7 };
    let Point { x, y } = p;
    assert_eq!(0, x);// x 변수 그대로 사용
    assert_eq!(7, y);// y 변수 그대로 사용
}

이런 식의 사용도 생각 해볼 수 있을 것이다.

fn main() {
    let p = Point { x: 0, y: 7 };
    let Point { x: p_x, y: p_y } = p;
    assert_eq!(0, p_x);
    assert_eq!(7, p_y);
}

구조체 패턴의 일부에 리터럴 값을 이용해 해체할 수도 있다.

fn main() {
    let p = Point { x: 0, y: 7 };
    match p {
        Point { x, y: 0 } => println!("On the x axis at {}", x),
        Point { x: 0, y } => println!("On the y axis at {}", y), // run
        Point { x, y } => println!("On neither axis: ({}, {})", x, y),
    }
}

이렇함으로써 어떤 필드가 특정 값에 해당하는지를 검사하면서 나머지 필드를 해체한 변수를 만들 수 있다.

열거형 해체

열거형을 해체하기 위한 패턴은 해당 열거형에 내장된 데이터의 정의 방식과 일치해야 한다.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
fn main() {
    // let msg = Message::Quit;
    let msg = Message::Write(String::from("hello"));
    // let msg = Message::Move {x: 1, y: 2};
    // let msg = Message::ChangeColor(0, 160, 255);
    match msg {
        Message::Quit => {
            println!("The Quit variant has no data to destructure.")
        },
        Message::Move { x, y } => {
            println!(
                "Move in the x direction {} and in the y direction {}",
                x,
                y
            );
        }
        Message::Write(text) => println!("Text message: {}", text),
        Message::ChangeColor(r, g, b) => {
            println!(
                "Change the color to red {}, green {}, and blue {}",
                r,
                g,
                b
            )
        }
    }
}

// 정말 신기한 언어다.

참조자 해체

패턴과 매칭하려는 값이 참조자를 포함하고 있을 땐 패턴 내에서 & 를 사용해 값으로부터 참조자를 해체해야 한다.
참조자들을 반복하는 반복자에서 클로저를 사용할 때, 해당 클로저 내에서 참조자가 아닌 참조자가 가리키는 값을 사용하길 원할 경우에 유용하다.

ex) 참조자 해체가 필요한 경우

struct Point {
    x: i32,
    y: i32,
}
fn main() {
    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 1, y: 5 },
        Point { x: 10, y: -3 },
    ];
    
    let sum_of_squares: i32 = points
        .iter()
        .map(|&Point { x, y }| x * x + y * y) // & 빼면 error
        .sum();
    println!("{}", sum_of_squares);
}

만약 &Point { x, y } 에서 & 를 뺀다면 타입 불일치(type mismatch) 에러가 발생
iter 는 벡터 내 요소들의 실제 값이 아닌 참조자이기 때문이다.

구조체와 튜플 해체

let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });

복잡한 타입의 컴포넌트를 분리하고 각각의 값을 의미 있는 변수로서 사용할 수 있게 한다.

패턴 내에서 값 무시

_ 를 이용해 전체 값 무시

_ 이용하여 match, 함수 인자 등에서 무시할 수 있다.

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
}
fn main() {
    foo(3, 4);
}

무시 가능 하지만 함수 시그니처에서 제거 하는게 맞을 것이다. 다만 몇몇 경우에는 함수의 매개변수를 무시하는 것이 유용할 떄가 있다.
트레잇을 구현할때 특정 타입의 시그니처가 필요한데 함수 본문에선 매개변수중 하나가 필요하지 않은 경우이다.
컴파일러는 이때 사용되지 않은 매개 변수에 관해서 경고하지 않습니다. 단, 언더스코어가 아닌 이름을 사용할 경우엔 경고합니다.

중첩되어 있는 상황에서도 무시 가능하다.

struct Point {
    x: i32,
    y: i32,
}
fn main() {
    let mut setting_value = Some(5);
    let new_setting_value = Some(10);
    
    match (setting_value, new_setting_value) {
        (Some(_), Some(_)) => {
        	// 실행
            println!("Can't overwrite an existing customized value"); 
        }
        _ => {
            setting_value = new_setting_value;
        }
    }
    
    println!("setting is {:?}", setting_value);
}

여러번 사용도 가능하다.

let numbers = (2, 4, 8, 16, 32);
match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {}, {}, {}", first, third, fifth)
    },
}

미사용 변수 경고를 피하기 위해 변수명을 언더스코어로 시작하도록 하면 된다.

fn main() {
    let _x = 5;// 경고 X
    let y = 10;// 경고 
}

_ 와 달리 _x 는 값이 바인드 되어 사용 가능 하며 소유권도 가져갈 수 있다.

let s = Some(String::from("Hello!"));
//if let Some(_) = s { // 소유권 가져가지 않음, 에러 발생하지 않음
if let Some(_s) = s { // _s가 소유권 가져감
    println!("found a string");
}
println!("{:?}", s); // error, 소유권 없어져서

.. 를 이용해 값의 나머지 부분 무시

값의 일부만 사용하고 나머지는 무시하기 위해 .. 구문을 사용

struct Point {
    x: i32,
    y: i32,
    z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
	// y: _ 와 z: _ 를 나열하는 것 보다 간결하다.
    Point { x, .. } => println!("x is {}", x),
}
fn main() {
    let numbers = (2, 4, 8, 16, 32);
    match numbers {
    	// (.., second, ..) => { // 바인딩이 모호해서 error
        (first, .., last) => {
            println!("Some numbers: {}, {}", first, last);
        },
    }
}

ref 와 ref mut 를 이용해 패턴 내에서 참조자 생성

ref 를 사용해 참조자를 만들어서 패턴 내 변수로 값의 소유권이 이동하지 않도록 할 수 있다.
러스트의 소유권 규칙에 따르면 값은 match 내부 혹은 여러분이 패턴을 사용하는 모든 곳으로 이동한다.

let robot_name = Some(String::from("Bors"));
match robot_name {
    Some(ref name) => println!("Found a name: {}", name),
    None => (),
}
println!("robot_name is: {:?}", robot_name);

ref가 없으면 robot_name 소유권이 match로 이동하기에 밑에서 출력시 에러가 발생한다.
가변을 하고 싶다면 &mut 대신 ref mut을 사용해야 한다.

let mut robot_name = Some(String::from("Bors"));
match robot_name {
    Some(ref mut name) => *name = String::from("Another name"),
    None => (),
}
println!("robot_name is: {:?}", robot_name);

매치 가드를 이용한 추가 조건

매치 가드(match guard) 는 match 갈래 뒤에 추가로 붙는 if 조건으로, 이것이 있을 경우 패턴 매칭과 해당 조건이 모두 만족되어야 해당 레이블이 실행된다.

let num = Some(4);
match num {
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}
fn main() {
    let x = Some(5);
    let y = 10;
    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {:?}", n),
        _ => println!("Default case, x = {:?}", x),
    }
    println!("at the end: x = {:?}, y = {:?}", x, y);
}

| 과 조합 가능 하다.

fn main() {
    let x = 4;
    let y = false;
    match x {
    	// 6 에만 매치 가드 적용 되는것 처럼 보이지만.
        //4, 5, 6 다 적용
        // (4 | 5 | 6) if y =>        
        4 | 5 | 6 if y => println!("yes"),
        _ => println!("no"), // run
    }
    
    // 이것은 컴파일 에러 발생
    // 4 | 5 | (6 if y) => println!("yes"),
}

@ 바인딩

at 연산자인 @ 는 해당 값이 패턴과 매치되는지 확인하는 동시에 해당 값을 갖는 변수를 생성할 수 있도록 해준다.

enum Message {
    Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
    //Message::Hello { id: 3..=7 } => {
    //    println!("Found an id in range: {}", id) // error, 획득 불가
    //},
    Message::Hello { id: id_variable @ 3..=7 } => {
    	// VALUE @ PATTERN
        // 패턴과 일치하면 id_variable로 값을 가져온다.
        println!("Found an id in range: {}", id_variable)
    },
    Message::Hello { id: 10...12 } => {
        println!("Found an id in another range")
    },
    Message::Hello { id } => {
        println!("Found some other id: {}", id)
    },
}
profile
mohadang

0개의 댓글