[Rust] 함수의 lifetime parameter는 언제 써야 하고 언제 생략할 수 있나요?

누군가가 나에게 러스트가 다른 언어와 다른 가장 큰 차이가 뭐냐고 묻는다면, 라이프타임이라고 대답할 것이다. 그래서 러스트를 처음 배우는 사람들이 러스트의 적응하는데 가장 어려운 부분도 이것이라고 생각한다. 특히 함수의 경우 라이프타임을 표기하는 파라미터는 생략이 가능해서 이 생략 규칙에 익숙하지 않은 사람은 함수가 의미하는 것을 이해하지 못하기도 한다.

일단 함수를 정의할 때는 크게 신경 쓰지 않아도 된다. 러스트는 분석 도구가 매우 잘 만들어져 있다. 지금까지 나온 어떤 언어보다 잘 돼 있다고 말하고 싶을 정도다. 어떤 라이프타임을 생략할 수 있는지 확실치 않은 경우는 모든 레퍼런스에 파라미터를 적어주고 clippy를 사용하면 어떤 파라미터가 필요 없는지 친절하게 알려준다. 이에 따라 코드를 다듬으면 된다.

문제는 다른 사람이 작성한 함수를 읽을 때이다. 라이프타임에 따라 함수의 의미가 달라진다. 때문에 라이프타임을 정확히 파악하지 못하면, 코드를 잘못 이해하는 경우가 생긴다.

생략된 라이프타임 파라미터가 어떤 값이 되는가는 생략된 라이프타임이 함수의 인자에 사용됐는가 결괏값에 사용됐는가에 따라 다르다. 우선 입력에 사용된 라이프타임이 생략된 경우는 간단하다. 전부 다른 파라미터로 생각하면 된다. 다시 말해서, 입력에 사용된 라이프타임은 다른 라이프 타임과 관계없다면 모두 생략할 수 있다. std::cmp::Ord::cmp(&self, other: &Self) -> Ordering가 2개의 레퍼런스 타입을 받지만, 라이프타임이 명시되지 않은 이유가 이 때문이다.

결괏값에 사용된 라이프타임을 생략하는 것은 이보다 약간 더 복잡하다. 우선 출력에 사용된 라이프타임은 특정한 두 경우만 생략될 수 있다.

첫 번째 경우는 함수의 인자에 단 하나의 라이프타임만 사용된 경우이다. 함수의 인자에 라이프타임 파라미터가 하나만 사용된 경우, 결괏값에 생략된 라이프타임 파라미터는 전부 인자의 라이프타임으로 간주한다. 예를 들어 std::cell::Cell::from_mut(t: &mut T) -> &Cell<T>T레퍼런스를 인자로 받아 이 레퍼런스를 감싸는 Cell의 레퍼런스를 돌려주는 함수이다. from_mut의 인자에는 T의 레퍼런스를 한정하는 라이프타임만을 필요로 하므로, 결괏값에 생략된 라이프타임은 인자와 같은 라이프타임이다. 즉, 위의 코드는 사실 std::cell::Cell::from_mut<'a>(t: &'a mut T) -> &'a Cell<T>와 같은 코드다.

여기서 라이프타임이 레퍼런스에만 사용되는 것이 아니라 타입의 파라미터로도 사용될 수 있다는 것을 조심해야 한다. 예를 들면 std::cell::RefMut::map<U>(orig: RefMut<'b, T>, f: F) -> RefMut<'b, U>의 경우 인자에 레퍼런스가 사용되지 않았지만 RefMut 자체가 라이프타임 파라미터를 포함하고 있는 구조체이기 때문에 RefMut::map의 인자에는 하나의 라이프타임 파라미터가 사용된 것이다. 따라서 위의 함수는 결괏값의 'b를 생략하여 RefMut::map<U>(orig: RefMut<'b, T>, f: F) -> RefMut<U>이라고 적을 수도 있다.

같은 이유로 std::cell::Ref::clone(orig: &Ref<'b, T>) -> Ref<'b, T>의 결괏값에 있는 라이프타임 'b는 생략할 수 없다. 명시적으로 보이는 라이프타임은 'b뿐이지만, 사실 orig의 타입은 &'a Ref<'b, T>로 레퍼런스의 라이프타임, 'a를 생략해 표현한 것이다. 즉, Ref::clone 함수는 인자에 2개의 라이프타임이 사용됐기 때문에 결괏값의 라이프타임을 생략하여 표현할 수 없다.

결괏값의 라이프타임을 생략할 수 있는 두 번째 경우는 함수의 인자로 self 혹은 mut self 레퍼런스가 사용된 경우다. 이 경우 결괏값에서 생략된 라이프타임은 &self와 같은 라이프타임으로 취급된다. std::convert::AsRef::as_ref(&self) -> &T의 결괏값 &Tself와 같은 라이프타임을 가진다. 즉, 위의 as_ref는 사실 std::convert::AsRef::as_ref<'a>(&'a self) -> &'a T와 같은 시그니처다.

지금까지 함수의 라이프타임 파라미터를 생략하여 표기할 수 있는 경우를 알아봤다. 러스트를 처음 쓰는 사람에게는, 특히 garbage collection이 있는 언어를 사용하던 사람에게 라이프타임은 과도한 제약으로 느껴질 수 있다. 하지만 라이프타임은 러스트의 안전성의 한 축을 맡는 중요한 기능이다. 라이프타임을 제대로 모르고 러스트를 사용하는 것은 타입을 모르고 러스트를 쓰는 것과 같다. 그리고 생소한 개념이라 그렇지 생각보다 어렵지 않다. 생략된 라이프타임을 읽을 줄 아는 것만으로도 코드를 보면서 많은 것을 배울 수 있다고 생각한다.

댓글

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

RAII는 무엇인가

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API