소유권은 호출자에게 남습니다. 다만 수신자를 가변으로 빌릴 수 있어야 하고, 호출과 충돌하는 다른 대여가 없어야 합니다.
3
값으로 받기 · self
self:Self의 축약형이며 일반적인 값 전달 규칙을 따릅니다. 변환 후 새 값을 반환하는 메서드에 자주 쓰입니다.
Copy가 아닌 이 절의 Rectangle은 이동되어 원래 바인딩을 다시 쓸 수 없습니다. 수신자 타입이 Copy라면 복사본이 전달되어 원본도 유효합니다.
&self · 공유 대여
읽기 중심 계약입니다. 호출자가 계속 소유하며, 이 예제의 u32 필드는 Copy라 값이 복사됩니다.
&mut self · 가변 대여
읽고 쓸 수 있지만 독점 대여입니다. 수신자를 가변으로 빌릴 수 있고 충돌하는 대여가 없어야 합니다.
self · 값으로 받기
Copy가 아닌 값은 이동합니다. Copy 타입이라면 복사본이 전달되어 원본도 계속 쓸 수 있습니다.
추가 매개변수는 별도 계약
can_hold의 other:&Rectangle처럼 self 뒤의 매개변수는 일반 함수 매개변수와 같습니다.
점 호출은 수신자만 메서드 시그니처에 맞춰 자동 보정합니다. rect1.can_hold(&rect2)의 추가 인수 &rect2는 호출자가 명시합니다.
기존의 Rectangle 매개변수를 갖던 area 함수를 수정하여
예제 4-13처럼 Rectangle 구조체에 정의된
area 메서드로 바꿔봅시다.
예제 4-13: Rectangle 구조체에
area 메서드 정의하기
src/main.rs
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}impl Rectangle { fn area(&self) -> u32 { self.width * self.height }}fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() );}
Rectangle의 컨텍스트에 함수를 정의하기 위해서, Rectangle에 대한 impl(implementation, 구현) 블록을 만드는 것으로 시작합니다.
이 impl 블록 내의 모든 것은 Rectangle 타입과 연관됩니다.
그런 다음 area 함수를 impl의 중괄호 안으로 옮기고 함수 시그니처의 첫 번째 매개변수를 (이번 경우는 유일한 매개변수군요) self로 변경하고 본문 내의 나머지 모든 부분도 변경합니다.
그리고 main 함수 내에서는 rect1을 인수로 전달하여 area 함수를 호출했었는데, 그 대신 메서드 문법(method syntax) 을 사용해 Rectangle 인스턴스의 area 메서드를 호출할 수 있습니다.
메서드 문법은 차례대로 인스턴스, 점, 메서드명, 괄호 및 인수로 구성됩니다.
area 시그니처에서는 rectangle: &Rectangle 대신 &self를 사용했습니다.
&self는 실제로는 self: &Self를 줄인 것입니다.
impl 블록 내에서 Self는 impl 블록의 대상이 되는 타입의 별칭입니다.
메서드는 Self 타입의 self라는 이름의 매개변수를 첫 번째 매개변수로 가져야 하는데, 그렇게 해야 첫 번째 매개변수 자리에 적어 넣은 self 형태의 축약형을 사용할 수 있습니다.
rectangle: &Rectangle에서 그랬던 것처럼 Self이 메서드가 Self의 인스턴스를 빌려온다는 것을 나타내기 위해서는 self 축약형 앞에 &를 계속 붙여둘 필요가 있음을 주목하세요.
메서드는 다른 매개변수가 그런 것처럼 self의 소유권을 가져올 수도, 지금처럼 self를 불변으로 빌려올 수도, 가변으로 빌려올 수도 있습니다.
여기서 &self를 선택한 이유는 기존의 함수 버전에서 &Rectangle을 사용했던 이유와 같습니다.
지금 원하는 것이 소유권을 가져오는 것도, 데이터를 쓰는 것도 아닌, 데이터를 읽는 것뿐이니까요.
만약 메서드에서 작업 중 호출한 인스턴스를 변경하고 싶다면, 첫 번째 매개변수로 &mut self를 사용하면 됩니다.
self라고만 작성하여 인스턴스의 소유권을 가져오도록 만드는 일은 거의 없습니다; 이러한 기법은 보통 해당 메서드가 self를 다른 무언가로 변환하고 그 이후에는 원본 인스턴스의 사용을 막고자 할 때 사용됩니다.
값으로 받는 self에도 일반적인 이동 규칙이 적용됩니다. 이 절의 Rectangle은 Copy를 구현하지 않았으므로 이런 메서드를 호출하면 값이 이동하여 원래 바인딩을 다시 사용할 수 없습니다. 반대로 수신자 타입이 Copy라면 메서드는 복사본을 받고 원래 값은 계속 사용할 수 있습니다.
&self인 area에서 self.width와 self.height를 값으로 읽을 수 있는 것은 두 필드가 Copy인 u32이기 때문입니다. 공유 대여한 값의 Copy가 아닌 필드는 밖으로 이동시킬 수 없으며 참조로 사용해야 합니다.
함수 대신 메서드를 사용하는 주된 이유는 메서드 구문을 제공하고 모든 메서드 시그니처 내에서 self 타입을 반복할 필요가 없다는 것 외에도 코드를 더 조직적으로 만들기 위해서입니다.
향후 우리가 제공한 라이브러리를 사용할 사람들이 Rectangle의 기능과 관련된 코드를 라이브러리 곳곳에서 찾아내야 하는 것보다는, 하나의 impl 블록 내에 이 타입의 인스턴스로 할 수 있는 모든 것들을 모아두는 것이죠.
구조체의 필드 이름과 동일한 이름의 메서드를 만들 수도 있습니다.
예를 들면, width라는 중복된 이름의 메서드를 Rectangle 상에 정의할 수 있지요.
src/main.rs
impl Rectangle { fn width(&self) -> bool { self.width > 0 }}fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); }}
여기서는 인스턴스의 width 필드가 0보다 크면 true를 반환하고 0이면 false를 반환하는 메서드의 이름으로 width를 선택했습니다.
같은 이름의 메서드 내에서 필드를 어떤 목적으로든 사용할 수 있습니다.
main에서 rect1.width 뒤에 괄호를 붙이면 러스트는 width 메서드를 의도한다는 것을 인지합니다.
괄호를 사용하지 않으면 러스트는 width 필드를 의미한다는 것으로 봅니다.
필드와 동일한 이름의 메서드를 만드는 경우는 해당 필드의 값을 얻어오는 것 말고는 안하는 경우가 대부분이긴 합니다.
이러한 메서드를 게터(getter) 라고 부르는데, 러스트는 다른 언어들처럼 구조체 필드에 대한 게터를 자동으로 만들지 않습니다.
필드를 비공개(private)로 하고 메서드는 공개(public)로 만들 수 있기 때문에 게터는 어떤 타입의 공개 API로써 어떤 필드에 대해 읽기 전용 접근만 허용하고자 하는 경우 유용합니다.
공개와 비공개가 무엇이고, 필드 혹은 메서드를 공개 혹은 비공개로 만드는 방법에 대해서는 6장에서 다루겠습니다.
C 나 C++ 언어에서는 메서드 호출에 두 종류의 연산자가 쓰입니다.
어떤 객체의 메서드를 직접 호출할 땐 .를 사용하고,
어떤 객체의 포인터를 이용해 메서드를 호출하는 중이라서 역참조가 필요할 땐 ->를 사용하죠.
예를 들어서 object라는 포인터가 있다면,
object->something()는 (*object).something()로 나타낼 수 있습니다.
이 -> 연산자와 동일한 기능을 하는 연산자는 러스트에 없습니다.
러스트에는 자동 참조 및 역참조(automatic referencing and dereferencing) 라는 기능이 있고,
메서드 호출에 이 기능이 포함되어 있기 때문입니다.
여러분이 object.something() 코드로 메서드를 호출하면,
러스트에서 자동으로 해당 메서드의 시그니처에 맞도록 &, &mut, *를 추가합니다.
즉, 다음 두 표현은 서로 같은 표현입니다.
# #[derive(Debug,Copy,Clone)]# struct Point {# x: f64,# y: f64,# }## impl Point {# fn distance(&self, other: &Point) -> f64 {# let x_squared = f64::powi(other.x - self.x, 2);# let y_squared = f64::powi(other.y - self.y, 2);## f64::sqrt(x_squared + y_squared)# }# }# let p1 = Point { x: 0.0, y: 0.0 };# let p2 = Point { x: 5.0, y: 6.5 };p1.distance(&p2);(&p1).distance(&p2);
첫 번째 표현이 더 깔끔하죠?
이런 자동 참조 동작은 메서드의 수신자(self의 타입을 말합니다)가 명확하기 때문에 가능합니다.
수신자와 메서드명을 알면 해당 메서드가 인스턴스를 읽기만 하는지(&self),
변경하는지(&mut self), 소비하는지(self) 러스트가 알아낼 수 있거든요.
또한 메서드의 수신자를 러스트에서 암묵적으로 빌린다는 점은
실제로 소유권을 인체공학적으로 만드는 중요한 부분입니다.
이 자동 보정은 점 문법의 수신자를 찾는 과정에 적용됩니다. 러스트는 수신자 타입을 반복해서 역참조하며 후보를 만들고, 각 후보 T 뒤에 &T와 &mut T를 추가한 다음 보이는 메서드를 순서대로 탐색합니다. 선택된 메서드를 실제로 호출할 수 있는지는 가변성, 수명, 대여 규칙까지 확인하며, 후보가 없거나 모호하면 컴파일 오류가 납니다.
다른 매개변수까지 자동으로 대여해 주는 것은 아닙니다. 예를 들어 rect1.can_hold(&rect2)에서 수신자 rect1은 &self에 맞춰 자동으로 빌리지만, 추가 매개변수인 rect2의 &는 호출자가 직접 작성합니다.
아래 다이어그램은 메서드 호출 시 러스트가 수신자 타입에 맞춰 자동으로
참조와 역참조를 보정하는 과정을 정리한 것입니다.
Rust 점 메서드 호출이 수신자 타입의 반복 역참조 후보와 각 후보의 공유·가변 참조 형태를 만든 뒤 보이는 메서드를 찾고, 유일하게 호출 가능하면 실행하며 그렇지 않으면 컴파일 오류가 되는 흐름도
METHOD LOOKUP · AUTOREF / AUTODEREF
점 문법은 수신자에 무조건 & 하나를 붙이는 단축 규칙이 아닙니다. 수신자 타입의 역참조 사슬과 대여 후보를 순서대로 만들고 보이는 메서드를 탐색합니다.
receiver.method(args)에서 먼저 수신자 표현식의 타입을 얻습니다.
수신자 타입을 반복해서 역참조하며 후보 타입을 만듭니다.
각 후보 T 바로 뒤에 &T와 &mut T를 추가하고 보이는 메서드를 찾습니다.
선택한 메서드가 가변성·수명·대여 규칙상 실제로 호출 가능한지 검사합니다.
유일하고 호출 가능
수신자를 맞는 형태로 자동 보정해 메서드를 호출합니다.
오류
후보가 없거나 모호하거나 선택된 대여가 허용되지 않으면 컴파일 오류입니다.
자동 참조와 역참조는 점 호출의 수신자에 대한 탐색입니다. 추가 인수는 호출자가 명시하며, 이름이 모호한 경우에는 완전 한정 함수 호출 문법으로 구분해야 할 수 있습니다.