Rust 기초: Option과 Result에서 as_deref() 활용하기
Rust에서 Option<String>을 다루다 보면 꽤 답답한 순간이 찾아옵니다.
Option 안에 들어있는 String을 &str과 비교하고 싶은데 타입이 맞지 않아 컴파일러가 거부하는 상황이죠.
fn main() {
let name: Option<String> = Some(String::from("Rust"));
if name == Some("Rust") {
println!("찾았다!");
}
}
error[E0308]: mismatched types
--> src/main.rs:4:13
|
4 | if name == Some("Rust") {
| ^^^^^^ expected `String`, found `&str`
Option<String>과 Option<&str>은 서로 다른 타입이라 직접 비교할 수 없습니다.
이런 상황에서 as_deref()를 알고 있으면 아주 깔끔하게 해결할 수 있는데요.
이 글에서는 as_ref()와 비교하면서 as_deref()가 왜 필요하고 어떻게 동작하는지 살펴보겠습니다.
Option에서 소유와 참조
문제의 근본 원인부터 짚어보겠습니다.
Option<String>에서 String은 힙에 할당된 문자열 데이터를 소유하는 타입이고, &str은 문자열 데이터를 빌려서 보는 참조 타입입니다.
Rust의 타입 시스템은 이 둘을 엄격하게 구분합니다.
fn main() {
let owned: Option<String> = Some(String::from("hello"));
let borrowed: Option<&str> = Some("hello");
// Option<String>과 Option<&str>은 다른 타입
// owned == borrowed; // 컴파일 에러!
}
String과 &str을 직접 비교하는 건 됩니다.
String이 PartialEq<str>을 구현하고 있으니까요.
fn main() {
let s = String::from("hello");
println!("{}", s == "hello"); // true — 직접 비교는 됨
}
true
그런데 Option으로 감싸는 순간 이 비교가 안 됩니다.
Option<T>의 PartialEq 구현이 같은 T끼리만 비교하도록 되어 있기 때문입니다.
그래서 Option<String>에서 Option<&str>로 변환하는 과정이 필요해집니다.
as_ref()로는 부족한 이유
가장 먼저 떠오르는 방법은 as_ref()입니다.
Option<T>의 as_ref()는 Option<&T>를 반환하죠.
fn main() {
let name: Option<String> = Some(String::from("Rust"));
let r: Option<&String> = name.as_ref();
println!("{:?}", r);
}
Some("Rust")
소유권을 넘기지 않고 참조를 꺼낼 수 있죠.
하지만 한 가지 문제가 있습니다.
as_ref()의 결과는 Option<&String>이지 Option<&str>이 아닙니다.
fn main() {
let name: Option<String> = Some(String::from("Rust"));
// as_ref()는 Option<&String>을 반환
// Option<&str>과는 여전히 비교 불가
// name.as_ref() == Some("Rust"); // 컴파일 에러!
}
그래서 map을 통해 한 단계 더 변환해야 합니다.
fn main() {
let name: Option<String> = Some(String::from("Rust"));
// as_ref() + map으로 Option<&str> 만들기
let r: Option<&str> = name.as_ref().map(|s| s.as_str());
println!("{}", r == Some("Rust"));
}
true
작동은 하지만 as_ref().map(|s| s.as_str())이라는 조합이 좀 번거롭습니다.
이걸 매번 쓰기에는 귀찮죠.
as_deref()가 하는 일
as_deref()는 바로 이 변환을 한 번에 해결합니다.
Option<T>에서 T가 Deref 트레이트를 구현하고 있으면 Option<&T::Target>을 반환합니다.
fn main() {
let name: Option<String> = Some(String::from("Rust"));
// as_deref()로 Option<&str>을 바로 얻기
let r: Option<&str> = name.as_deref();
println!("{}", r == Some("Rust"));
}
true
String은 Deref<Target = str>을 구현하고 있으니까 as_deref()가 Option<String>을 Option<&str>로 바꿔줍니다.
as_ref().map(|s| s.as_str())을 쓸 필요 없이 as_deref() 한 방이면 됩니다.
처음에 봤던 비교 문제도 깔끔하게 해결됩니다.
fn main() {
let name: Option<String> = Some(String::from("Rust"));
if name.as_deref() == Some("Rust") {
println!("찾았다!");
}
}
찾았다!
as_deref()의 동작 원리
그렇다면 as_deref() 안에서는 어떤 일이 벌어지는 걸까요?
비밀은 Deref 트레이트에 있습니다.
Deref 트레이트는 * 연산자로 역참조했을 때 어떤 타입이 나오는지를 정의합니다.
String의 경우 Deref<Target = str>을 구현하고 있어서, String을 역참조하면 str이 됩니다.
as_deref()는 이 Deref 구현을 활용합니다.
내부적으로 보면 다음과 비슷한 동작을 합니다.
// as_deref()의 내부 동작을 풀어쓰면
fn as_deref(opt: &Option<String>) -> Option<&str> {
match opt {
Some(s) => Some(s), // &String → &str (역참조 강제)
None => None,
}
}
Some(s)에서 s는 &String인데, 반환 타입이 Option<&str>이므로 역참조 강제가 작동하여 &String이 &str로 자동 변환됩니다.
정리하면 as_ref()와 as_deref()의 차이는 이렇습니다.
fn main() {
let text: Option<String> = Some(String::from("hello"));
// as_ref(): Option<T> → Option<&T>
let a: Option<&String> = text.as_ref();
// as_deref(): Option<T> → Option<&T::Target>
let b: Option<&str> = text.as_deref();
println!("as_ref: {:?}", a);
println!("as_deref: {:?}", b);
}
as_ref: Some("hello")
as_deref: Some("hello")
출력은 같아 보이지만 타입이 다릅니다.
as_ref()는 Option<&String>을, as_deref()는 Option<&str>을 돌려줍니다.
Deref를 한 단계 더 따라가느냐 마느냐의 차이입니다.
다양한 타입에서 활용
as_deref()는 String에만 쓸 수 있는 게 아닙니다.
Deref 트레이트를 구현한 모든 타입에서 사용할 수 있습니다.
Vec은 Deref<Target = [T]>를 구현하고 있으니까 Option<Vec<T>>를 Option<&[T]>로 변환할 수 있습니다.
fn sum(numbers: Option<&[i32]>) -> i32 {
match numbers {
Some(nums) => nums.iter().sum(),
None => 0,
}
}
fn main() {
let nums: Option<Vec<i32>> = Some(vec![1, 2, 3, 4, 5]);
println!("합계: {}", sum(nums.as_deref()));
let empty: Option<Vec<i32>> = None;
println!("합계: {}", sum(empty.as_deref()));
}
합계: 15
합계: 0
PathBuf도 Deref<Target = Path>를 구현하고 있어서 같은 패턴이 적용됩니다.
use std::path::{Path, PathBuf};
fn show_extension(path: Option<&Path>) {
match path.and_then(|p| p.extension()) {
Some(ext) => println!("확장자: {}", ext.to_string_lossy()),
None => println!("확장자 없음"),
}
}
fn main() {
let path: Option<PathBuf> = Some(PathBuf::from("/tmp/data.csv"));
show_extension(path.as_deref());
}
확장자: csv
Option<PathBuf>를 Option<&Path>로 바꿔서 함수에 넘기는 거죠.
Deref 트레이트의 표준 라이브러리 구현을 알고 있으면 어떤 타입에 as_deref()를 쓸 수 있는지 바로 떠올릴 수 있습니다.
| 원래 타입 | as_deref() 결과 | Deref 관계 |
|---|---|---|
Option<String> | Option<&str> | String → str |
Option<Vec<T>> | Option<&[T]> | Vec<T> → [T] |
Option<PathBuf> | Option<&Path> | PathBuf → Path |
Option<OsString> | Option<&OsStr> | OsString → OsStr |
Option<CString> | Option<&CStr> | CString → CStr |
Option<Box<T>> | Option<&T> | Box<T> → T |
패턴 매칭에서의 활용
as_deref()는 match 표현식과 함께 쓸 때 특히 빛을 발합니다.
Option<String>을 패턴 매칭하려면 &String과 매칭해야 하는데, as_deref()를 쓰면 문자열 리터럴과 바로 매칭할 수 있습니다.
fn describe_lang(lang: Option<String>) {
match lang.as_deref() {
Some("Rust") => println!("시스템 프로그래밍 언어"),
Some("Python") => println!("범용 스크립팅 언어"),
Some(other) => println!("기타 언어: {}", other),
None => println!("언어를 선택하지 않음"),
}
}
fn main() {
describe_lang(Some(String::from("Rust")));
describe_lang(Some(String::from("Go")));
describe_lang(None);
}
시스템 프로그래밍 언어
기타 언어: Go
언어를 선택하지 않음
as_deref() 없이 같은 코드를 작성하면 이렇게 됩니다.
fn describe_lang(lang: Option<String>) {
match lang.as_ref().map(|s| s.as_str()) {
Some("Rust") => println!("시스템 프로그래밍 언어"),
// ...
}
}
as_deref() 하나로 훨씬 간결해지죠.
as_deref_mut()
as_deref()가 불변 참조를 꺼낸다면 as_deref_mut()는 가변 참조를 꺼냅니다.
Option<T>에서 T가 DerefMut을 구현하고 있으면 Option<&mut T::Target>을 반환합니다.
fn main() {
let mut text: Option<String> = Some(String::from("hello"));
// Option<&mut str>을 얻어서 내부 문자열을 직접 수정
if let Some(s) = text.as_deref_mut() {
s.make_ascii_uppercase();
}
println!("{:?}", text);
}
Some("HELLO")
as_deref_mut()으로 Option<&mut str>을 꺼내서 내부 문자열을 직접 수정했습니다.
Option을 벗기지 않고도 안에 있는 값을 변경할 수 있어서 편리합니다.
Vec에서도 마찬가지입니다.
fn main() {
let mut nums: Option<Vec<i32>> = Some(vec![3, 1, 4, 1, 5]);
if let Some(slice) = nums.as_deref_mut() {
slice.sort();
}
println!("{:?}", nums);
}
Some([1, 1, 3, 4, 5])
Result에서의 as_deref()
Option뿐 아니라 Result<T, E>에도 as_deref()가 있습니다.
Result<T, E>의 as_deref()는 Result<&T::Target, &E>를 반환합니다.
use std::io;
fn read_config() -> Result<String, io::Error> {
Ok(String::from("debug=true"))
}
fn main() {
let config = read_config();
// Result<String, io::Error> → Result<&str, &io::Error>
match config.as_deref() {
Ok("debug=true") => println!("디버그 모드"),
Ok(other) => println!("설정: {}", other),
Err(e) => println!("오류: {}", e),
}
}
디버그 모드
Result에서도 as_deref()로 Ok 안의 값을 역참조한 슬라이스로 변환해서 문자열 리터럴과 바로 매칭할 수 있습니다.
as_deref_mut()도 Result에서 동일하게 사용할 수 있습니다.
마치며
as_deref()는 메서드 하나짜리지만 쓸 곳이 정말 많습니다.
String을 &str로, Vec<T>를 &[T]로, PathBuf를 &Path로 변환하는 작업이 메서드 하나로 끝납니다.
핵심은 Deref 트레이트와의 연결입니다.
as_ref()가 T의 참조를 꺼낸다면, as_deref()는 T를 역참조한 타겟의 참조를 꺼냅니다.
이 차이를 알고 나면 as_ref().map(|s| s.as_str()) 같은 번거로운 패턴을 쓸 일이 없어집니다.
패턴 매칭에서 Option<String>이나 Result<String, E>를 문자열 리터럴과 비교해야 할 때, as_deref()를 떠올려 보세요.
더 자세한 내용은 Option::as_deref - Rust 공식 문서를 참고하세요.
This work is licensed under
CC BY 4.0