[Rust] reference와 패턴매칭

undefcat·2022년 9월 14일
1

rust

목록 보기
1/6
post-thumbnail

변수할당도 패턴매칭

러스트에서는 let을 이용해 값을 변수에 바인딩합니다.

let a = 10;

그런데, 사실 이 변수에 값을 할당하는 행위조차 패턴매칭입니다.

다음의 예제를 봅시다.

let message = String::from("Hello, World!");
let message_ref = &message;

message의 값을 message_ref가 borrow 했습니다. 그런데, let은 패턴매칭입니다.

let message = String::from("Hello, World!");
let &message_owned = &message;

따라서 Lhs에 &message_owned와 같이 쓸 수 있습니다. 이는 let이 패턴매칭을 통해 변수를 바인딩할 수 있음을 보여줍니다. 하지만 컴파일되지 않습니다.

컴파일러는 뭐라고 불평할까요?

error[E0507]: cannot move out of a shared reference
 --> src/main.rs:5:24
  |
5 |     let &message_owned = &message;
  |         --------------   ^^^^^^^^
  |         ||
  |         |data moved here
  |         |move occurs because `message_owned` has type `String`, which does not implement the `Copy` trait
  |         help: consider removing the `&`: `message_owned`

즉, message 변수의 값을 borrow한 &message를 패턴매칭을 통해 &message_owned에 할당하려고 했습니다. 따라서, 이는 messagemessage_owned로 move 하는 행위입니다. 그런데 당연하지만 못합니다. &message로는 소유권을 옮길 수 없기 때문이죠.

즉, Copy trait를 구현하고 있지 않다면 let 바인딩으로 let &var_name = &value와 같이 사용할 수 없습니다. 위의 예제는 String 타입을 사용했는데, 만약 Copy를 구현하고 있는 타입을 사용하면 아무런 문제가 없을 것입니다.

let number = 10;
let &number_owned = &number;

이는 &number가 패턴매칭에 의해 number_owend 변수로 move되면서 Copy trait가 작동하여 값이 복사되기 때문에 상관 없습니다.

& 그리고 ref

&ref는 참조를 나타낸다는 점에서 비슷하지만, ref는 패턴매칭에서 주로 사용됩니다. 이는 &는 패턴매칭으로 인해 변수로 소유권이 이동될 수도 있기 때문인데요. 이를 막기 위해 ref를 사용합니다.

tuple struct 예제를 통해 알아봅시다.

struct Foo(String);

let foo = Foo(String::from("Hello, World"));

match foo {
	Foo(a) => println!("{}", a),
    _ => panic!("Unreachable pattern"),
}

println!("{}", foo.0);

이 코드는 당연하지만 컴파일되지 않습니다. match의 첫번째 branch에서 foo.0의 소유권을 가져갔으므로, 더이상 foo를 사용할 수 없죠. (참고로 match (EXPR)에 의해 소유권이 move되진 않습니다.)

10 |         Foo(a) => println!("{}", a),
   |             - value moved here
...
14 |     println!("{}", foo.0);
   |                    ^^^^^ value borrowed here after move
   |
   = note: move occurs because `foo.0` has type `String`, which does not implement the `Copy` trait

따라서, match 에서 해당 값을 사용하고 싶지만 소유권은 가져가고 싶지 않을 때 사용하는 것이 바로 ref입니다.

struct Foo(String);

let foo = Foo(String::from("Hello, World"));

match foo {
	Foo(ref a) => println!("{}", a),
    _ => panic!("Unreachable pattern"),
}

println!("{}", foo.0);

첫번째 branch를 Foo(ref a)와 같이 수정해줬습니다. 이제 foo.0의 소유권을 가져가지 않았기 때문에, 정상적으로 동작합니다.

두 레퍼런스의 모호함

문제는 &ref 모두 레퍼런스를 나타내는데, 왜 Foo(ref a)와 같이 써야하고 &Foo(a) 혹은 Foo(&a)처럼은 못쓰냐는 겁니다.

Foo(&a)로 패턴매칭을 하게 된다면

9  |     match foo {
   |           --- this expression has type `Foo`
10 |         Foo(&a) => println!("{}", a),
   |             ^^ expected struct `String`, found reference
   |
   = note: expected struct `String`
           found reference `&_`
help: consider removing `&` from the pattern
   |
10 -         Foo(&a) => println!("{}", a),
10 +         Foo(a) => println!("{}", a),
   |

컴파일러가 씅을 내고, 마찬가지로 &Foo(a)로 패턴매칭을 하게 된다면

9  |     match foo {
   |           --- this expression has type `Foo`
10 |         &Foo(a) => println!("{}", a),
   |         ^^^^^^^ expected struct `Foo`, found reference
   |
   = note: expected struct `Foo`
           found reference `&_`

비슷하게 씅을 냅니다.

이는 처음에 살펴봤던 let에서의 이유와 같은데, 패턴매칭에서 & 역시 패턴으로 인지하기 때문입니다. 즉, ref라는 키워드를 모른다면 match에서 값을 잠시 borrow하는 방법을 모를 수밖에 없는데, 문제는 &ref는 둘 다 참조를 나타내기 때문에 너무 모호하다는 점에 있습니다.

사실, reflet에서도 사용할 수 있습니다.

let ref hello = String::from("Hello");

이는 사실 아래와 같습니다.

let hello = &String::from("Hello");

ref는 패턴매칭에 사용되는 키워드이며, let도 패턴매칭이므로 당연히 쓸 수 있습니다.

즉, &는 Lhs에서 사용될 땐 타입을, Rhs에서 사용될 땐 borrow를 하는 연산자가 되며 ref는 Lhs에서만 사용할 수 있는 패턴매칭 키워드이면서 동시에 해당 매칭되는 변수가 값을 레퍼런스를 가리키도록 해주는(즉, borrow하는) 역할을 하는 것입니다.

이는 러스트를 처음 접하는 개발자에게 혼동을 줍니다. 특히 패턴매칭에서요. 레퍼런스를 얻는 방법이 두가지라니요?

&, ref와 함께 춤을

코드는 명료하면 이해하기가 쉽습니다. 예를 들어, 아래와 같은 코드를 봅시다.

let str = &String::from("Hello, World");

match str {
	&String => println!("&String"),
    
    String => println!("String"),
}

과연 어느 branch와 매칭 되는게 자연스러운가요? 누구나 &String 이라고 얘기할 것입니다. 하지만, 사실 저렇게 레퍼런스에 따라 경우를 나누는건 흔하지 않을 겁니다. 아무튼, 저런 코드는 상당히 직관적이지만 실용적이진 않습니다. 레퍼런스냐 아니냐에 따라 패턴매칭을 하는 경우는 드물지만, match에서 값의 소유권을 가져오진 않고 borrow만 하고 싶은 경우는 많으니까요.

struct Wrapper(String);

let wrapper = Wrapper(String::from("Hello, World");

match wrapper {
    // 이 branch에 의해 str의 소유권이 이동됐다.
	Wrapper(str) => println!("{}", str),
    _ => println!("?"),
}

// 이제 `wrapper`는 사용할 수 없다!
println!("{}", wrapper.0);
   |
7  |         Wrapper(str) => println!("{}", str),
   |                 --- value moved here
...
11 |     println!("{}", wrapper.0);
   |                    ^^^^^^^^^ value borrowed here after move

따라서 ref가 나오게 되었습니다.

match wrapper {
	Wrapper(ref str) => println!("{}", str),
    _ => println!("?"),
}

// 사용 가능
println!("{}", wrapper.0);

과거의 러스트에서 패턴매칭은 레퍼런스까지 맞춰야만 패턴매칭이 됐었던 것 같습니다.

let x: &Option<_> = &Some(0);

// &와 매칭하거나
match x {
    &Some(ref y) => { ... },
    &None => { ... },
}

// 아니면 dereference한 값을 value와 매칭하거나
match *x {
    Some(ref x) => { ... },
    None => { ... },
}

이는 결국 코드를 작성하는 사람이 어떤 변수를 봤을 때, 이게 레퍼런스인지 아닌지 명확히 알아야 된다는 점, 또는 불필요한 dereferencing을 하거나 branch마다 &를 명시하거나 하는 귀찮은 상황을 만들었습니다. 개발자가 * & ref를 가지고 이랬다가 저랬다가 춤을 춰야 된다는 뜻이죠.

하지만 RFC 2005를 통해 이 문제가 해결됐죠. 계속 언급하지만, 일반적으로 레퍼런스냐 아니냐로 패턴매칭을 하는 경우는 드물지만, 매칭된 branch에서 레퍼런스를 얻어와야 하는 경우는 많습니다.

따라서, 레퍼런스인 값을 레퍼런스가 아닌 패턴과 매칭하게 될 때, 러스트는 알아서 ref를 추가해줍니다.

즉, 아래와 같이 사용할 수 있는 겁니다.

let wrapper = &Wrapper(String::from("Hello, World");

// &Wrapper 타입을 Wrapper와 매칭한다.
match wrapper {
    // &Wrapper 타입을 Wrapper와 매칭했으니, str: &String이 된다.
    // 마치 &Wrapper(ref str)와 매칭하는 것처럼 된다!
	Wrapper(str) => println!("{}", str),
    _ => println!("?"),
}

// 소유권이 이동되지 않았다.
println!("{}", wrapper.0);

이는 tuple을 사용할 때, 마치 드모르간의 법칙을 연상하게 합니다. &를 분배하는 것 같거든요.

let tuple = &(String::from("0"), String::from("1"));

match tuple {
    // &(ref a, ref b)로 받는 것과 같다.
	(a, b) => println!("{}, {}", a, b),
    _ => println!("?"),
}

이러한 규칙은 함수형 프로그래밍을 사용할 때 반드시 이해해야 하는 중요한 요소입니다. 레퍼런스가 어떻게 패턴매칭 되는지 모른다면, 코드를 작성할 때 상당히 헷갈릴 것입니다.

    let users = vec![
        (String::from("Sooran"), 29),
        (String::from("Nio"), 32),
        (String::from("Elon"), 40),
    ];

    let names = users
        .iter()
        .map(|(name, _age)| name)
        .collect::<Vec<_>>();

    assert_eq!(names[0], users[0].0);

위의 예제는 컴파일되지 않습니다. 왜냐하면, iter()는 Item을 reference로 리턴하는데, 우린 map을 사용할 때 패턴매칭을 reference로 안했기 때문에 러스트에서는 name_ageref를 추가하고, 따라서 names의 타입은 Vec<&String>이 되며, 결국 이는 String&String의 비교가 되므로 컴파일이 되지 않는 것이죠.

즉, 여기서 알아야 할 점들을 나열해보자면

  • iter()는 Item을 레퍼런스로 전달해준다.
  • map 클로저 매개변수의 패턴은 값형태의 tuple이므로, 컴파일러에 의해 레퍼런스가 추가된다.

이를 컴파일하려면 어떻게 해야할까요? |&(name, _age)|와 같이 패턴매칭을 하면 될 것 같습니다. 하지만 그렇게 되면 결국 레퍼런스된 값의 소유권을 move하는 행위가 되므로 역시 컴파일 되지 않습니다.

따라서, 소유권을 포함한 into_iter()를 호출하거나 비교할 때 assert_eq!(names[0], &users[0].0) 과 같이 비교해야 할 것입니다.

이처럼 러스트에서 참조값의 패턴매칭이 어떤 식으로 발생하는지 아는 것은 참으로 중요합니다.

Reference

profile
undefined cat

0개의 댓글