앞에서 봤던 마지막 코드에서 String이 calculate_length로 이동해버린 것 때문에 calculate_length를 호출한 함수로 String을 반환하여 함수 호출 이후에도 String을 사용할 수 있게 했다.
이 방법 대신 참조자(reference)를 사용하여 주솟값으로 데이터에 접근하도록 해준다.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
이 방식으로 하면 튜플을 사용할 필요가 없어진다.
calculate_length 함수에 s1 대신 &s1을 전달하고 함수 정의에 String 대신 &String을 사용했다.
이 방식에서는 calculate_length의 함수가 소유권을 가지지 않는다. 그렇기 때문에 함수가 끝나고 스코프 밖으로 s가 벗어나게 되었을 때도 원본 데이터를 삭제하지 않는다.
또한, 참조자를 만드는 행위를 대여(borrow) 라고 한다.
만약 이 코드에서 참조하는 값을 바꾸려고 하면 어떻게 될까?
당연하게도, 불변으로 선언되어 있기 때문에 에러를 발생시킨다.
하지만 위의 코드를 가변 참조자(mutable reference)를 사용하면 에러를 없앨 수 있다.
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
하지만, 가변 참조자는 한 가지 큰 제약사항이 있다.
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
러스트에서는 가변으로 두 번 이상 빌려올 수 없기 때문에 코드가 유효하지 않다고 말해줍니다.
첫 번째 가변 대여는 r1에 있고, println!에서 사용될 때까지 남아있어야ㅑ 하지만, 이 가변 참조자의 생성과 사용 사이에서 r1과 같은 데이터를 빌리는 r2 가변 참조자를 만들고 있습니다.
이 점이 코드 작성이 복잡하지만 컴파일할 때 데이터 경합(data race)를 방지할 수 있다.
또한 데이터 경합은 아래 세 가지 상황이 겹칠 때 발생한다.
또는 중괄호로 새로운 스코프를 만드는 방식으로 가변 참조자가 여러개 있는 상황을 방지할 수 있다.
두 참조자를 혼용하면 문제가 된다.
불변 참조자는 중간에 변경할 수 없기 때문에 문제가 되지 않지만, 이후에 가변 참조자를 사용하면 값이 변할 수도 있기 때문에 컴파일 에러가 발생한다.
어떤 메모리를 가리키는 포인터가 남아있는 상황에서 일부 메모리를 해제해 버림으로써, 다른 개체가 할당받았을지도 모르는 메모리를 참조하게 된 포인터
포인터가 있는 언어에서는 잘못하면 댕글링 포인터를 만들기 쉽다.
하지만 러스트에서는 어떤 데이터의 참조자를 만들면, 해당 참조자가 스코프를 벗어나기 전에 데이터가 먼저 스코프를 벗어나는지 컴파일러에서 확인하여 댕글링 참조가 생성하지 않도록 보장한다.
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
이 코드를 실행하면 라이프타임과 관련된 에러가 나오지만, 나중에 보자.
여기서 왜 댕글링 참조가 일어났는지 보면, 반환하는 값으로 s의 주소를 보내는데, 문제는 s가 함수를 벗어나면 메모리에서 해제된다는 점이다.
즉, 없어진 데이터의 주소를 반환하기 때문에 위험한 것이다.
이 경우에는 String을 직접 반환하면 해결할 수 있다.
fn no_dangle() -> String {
let s = String::from("hello");
s
}