점 연산자

점 연산자는 타입을 변환하기 위해 많은 마법을 사용할 겁니다. 타입이 맞을 때까지 자동 레퍼런싱, 자동 역참조, 강제 변환을 수행하겠죠. 메서드 조회의 자세한 작동 방식은 여기에 정의되어 있지만, 기본적인 절차를 여기서 간단하게 설명하겠습니다.

어떤 수신자(self, &self 또는 &mut self 매개변수)가 있는 함수 foo가 있다고 해 봅시다. 만약 우리가 value.foo()를 호출하면, 컴파일러는 이 함수의 올바른 구현을 호출하기 전에 Self가 어떤 타입인지 밝혀내야 합니다. 이 예제에서는 valueT 타입이라고 하겠습니다.

우리는 완전 정식화 문법을 써서 우리가 어떤 타입의 함수를 호출하는지를 명확히 하겠습니다.

  • 먼저 컴파일러는 T::foo(value)를 직접 호출할 수 있는지 검사합니다. 이것은 "값에 의한" 메서드 호출이라 부릅니다.
  • 이 함수를 호출할 수 없다면 (예를 들어 함수가 잘못된 타입을 가진다거나 Self에 대해 트레잇이 구현되지 않았을 경우), 컴파일러는 자동으로 레퍼런스를 추가합니다. 이 말은 컴파일러가 <&T>::foo(value)<&mut T>::foo(value)를 시도해 본다는 뜻입니다. 이것은 자동 레퍼런스 메서드 호출이라 부릅니다.
  • 여기까지의 후보들이 실패했다면 컴파일러는 T를 역참조하여 다시 시도합니다. 이것은 Deref 트레잇을 사용합니다 - 만약 T: Deref<Target = U>라면 T 대신 U가 사용됩니다. 만약 T를 역참조할 수 없다면 T크기 지정 해제를 시도할 수도 있습니다. 이것은 만약 T가 컴파일 시간에 알려진 크기 매개변수가 있다면, 메서드를 찾기 위해 이것을 "잊어버린다는" 말입니다. 예를 들어, 이런 크기 지정 해제 작업은 배열의 크기를 "잊어버림으로써" [i32; 2][i32]로 변환할 수 있습니다.

여기 메서드 조회 알고리즘의 예제가 있습니다:

let array: Rc<Box<[T; 3]>> = ...;
let first_entry = array[0];

배열로 가는 길에 돌아가는 길이 이렇게 많은데 컴파일러는 어떻게 실제 array[0]을 계산할 수 있을까요? 먼저, array[0]은 그냥 Index 트레잇을 위한 문법적 설탕입니다 - 컴파일러는 array[0]array.index(0)으로 변환할 겁니다. 이제, 컴파일러는 함수를 호출하기 위해 arrayIndex를 구현하는지 봅니다.

그럼 컴파일러는 Rc<Box<[T; 3]>>Index를 구현하는지 보는데, 구현하지 않고, &Rc<Box<[T; 3]>>&mut Rc<Box<[T; 3]>>Index를 구현하지 않습니다. 여태까지 아무것도 맞지 않았으니, 컴파일러는 Rc<Box<[T; 3]>>Box<[T; 3]>로 역참조하여 다시 시도합니다. Box<[T; 3]>, &Box<[T; 3]>, 그리고 &mut Box<[T; 3]>Index를 구현하지 않으므로, 컴파일러는 다시 역참조합니다. [T; 3]과 그의 자동 참조들도 Index를 구현하지 않습니다. 컴파일러는 [T; 3]를 역참조할 수 없으므로, 크기 지정을 해제하여, [T]를 얻어냅니다. 마지막으로, [T]Index를 구현하므로, 컴파일러는 실제로 index 함수를 호출할 수 있게 됩니다.

점 연산자가 작동하는 좀더 복잡한 다음의 예제를 생각해 봅시다:

#![allow(unused)]
fn main() {
fn do_stuff<T: Clone>(value: &T) {
    let cloned = value.clone();
}
}

cloned는 어떤 타입일까요? 먼저, 컴파일러는 값으로 호출할 수 있는지 알아봅니다. value의 타입은 &T이고, clone 함수는 fn clone(&T) -> T의 시그니처를 가지고 있습니다. 컴파일러는 T: Clone을 알고 있으니, cloned: T인 것을 찾아냅니다.

만약 T: Clone 제한이 없어졌다면 무슨 일이 일어날까요? T를 위한 Clone 구현이 없으므로, 컴파일러는 값으로 호출하지 못할 것입니다. 따라서 컴파일러는 자동 참조로 호출을 시도합니다. 이 경우에는 Self = &T이므로 함수는 fn clone(&&T) -> &T의 시그니처를 가지게 됩니다. 컴파일러는 &T: Clone을 알아차리고, cloned: &T라고 결론짓습니다.

여기, 자동 참조 동작이 잘 보이지 않는 변화를 만들어내는 데 쓰이는, 다른 예제가 있습니다.

#![allow(unused)]
fn main() {
use std::sync::Arc;

#[derive(Clone)]
struct Container<T>(Arc<T>);

fn clone_containers<T>(foo: &Container<i32>, bar: &Container<T>) {
    let foo_cloned = foo.clone();
    let bar_cloned = bar.clone();
}
}

foo_clonedbar_cloned는 어떤 타입일까요? 우리는 Container<i32>: Clone이라는 것을 알기 때문에, 컴파일러는 clone을 값으로 호출하여 foo_cloned: Container<i32>를 얻어냅니다. 그러나, bar_cloned는 실제로는 &Container<T>를 타입으로 가지게 됩니다. 확실히 이것은 말이 되지 않습니다 - 우리는 Container#[derive(Clone)]을 추가했으므로, ContainerClone을 구현해야 합니다! 좀더 가까이 보자면, derive 매크로에 의해 생성된 코드는 (대강) 다음과 같습니다:

impl<T> Clone for Container<T> where T: Clone {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}

파생된 Clone 구현은 T: Clone일 때만 정의되어 있으며, 따라서 일반적인 T에 대해 Container<T>: Clone 구현은 없는 것입니다. 그럼 컴파일러는 &Container<T>Clone을 구현하는지 봅니다. &Container<T>Clone을 구현하므로, 컴파일러는 clone이 자동 참조로 호출된다고 결론 내리고, 따라서 bar_cloned&Container<T>의 타입을 갖게 됩니다.

우리는 T: Clone을 요구하지 않는 Clone을 수동으로 구현함으로써 이 문제를 해결할 수 있습니다:

impl<T> Clone for Container<T> {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}

이제 타입 검사기는 bar_cloned: Container<T>라고 결론짓습니다.