Rust 프로그래밍 10장 - Copy와 Clone의 차이
이 튜토리얼 시리즈는 다른 언어 하나 정도는 이미 정통하고 있는 사람들을 기반으로 합니다.
그게 Modern C++이면 더 할 나위 없이 좋으나, 자바나 다른 언어라도 별 무리는 없을 거 같습니다.(라이프타임이나 스마트포인터 같은거만 조금 더 공부해 보면 될 듯)
--
이게 먼가 하면 Clone은 명시적으로 객체들을 깊은 복사하는 느낌이고 (포인터를 그대로 복붙하는게 아니라 포인터가 가르키는 객체를 따라가서 까지 복사하는거..)
Copy는 C의 memcpy처럼 그냥 단순히 메모리 데이터를 비트 단위로 복사하는 느낌? 이 과정에서 추가적인 연산이나 힙 메모리의 복사가 발생하지 않고 그래서 단순 구조에서 사용하면 빠르겠지요.
1. Copy와 Clone의 기본 개념
Copy: 값을 복사하여 다른 변수에 동일한 값을 생성. 메모리에서 간단한 복사를 수행하며 비용이 매우 적음.값의 크기가 고정된 경우(스택에 저장) 자동으로 구현 가능.
Clone: 깊은 복사로, 값뿐만 아니라 참조된 데이터까지 복사하여 완전히 독립적인 객체를 생성.힙에 저장된 데이터를 복사해야 하므로 명시적으로 호출.
fn main() {
let x = 5; // i32는 이미 Copy 트레이트가 구현되어 있음
let y = x; // x의 값이 y로 복사됨
println!("x: {}, y: {}", x, y); // 둘 다 사용 가능
let s1 = String::from("hello");
let s2 = s1.clone(); // String은 Clone만 가능
println!("s1: {}, s2: {}", s1, s2); // 둘 다 사용 가능
}2. Copy가 자동으로 구현되는 조건
트레이트를 명시하지 않아도 Copy가 가능한 타입:
기본 타입(e.g.,
i32,f64)고정 크기의 배열(e.g.,
[i32; 3])Copy 가능한 타입만 포함하는 구조체
#[derive(Debug, Copy, Clone)] // Copy와 Clone 트레이트를 파생 가능
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 0, y: 0 };
let p2 = p1; // Copy 발생. 즉 clone 을 명시적으로 안해도 소유권 복사 된다는 거 (이동이 아니라)
println!("p1: {:?}, p2: {:?}", p1, p2); // 둘 다 사용 가능
}당연하게도 위의 예제에서 Copy,Clone을 빼면 소유권이 이동되어 p1은 사용못한다.
Copy만 적어주면 Clone도 같이 적어주라고 안내한다. (법칙!)
Clone만 적으면 명시적으로 p1.clone()으로 적어 줘야 한다. 이것도 법칙!! 좀 더 자세히는 6번 아이템에서 설명 합니다.
3. Clone을 구현해야 하는 경우
힙 데이터와 같은 동적 데이터가 있는 경우 반드시
Clone을 구현해야 함.
#[derive(Clone)]
struct Data {
name: String,
value: i32,
}
fn main() {
let d1 = Data {
name: String::from("Rust"),
value: 42,
};
let d2 = d1.clone(); // 깊은 복사 발생
println!("d1: {:?}, d2: {:?}", d1, d2);
}구조체 내부에 Clone이 구현되어 있는 속성만 있을 경우 #[derive(Clone)] 만 적어주면 clone을 통해 복제 할 수 있다.
4. 사용자 정의 타입에 Copy와 Clone 트레이트 구현
Copy를 구현하려면 모든 필드가Copy가능해야 함.Clone은 직접 구현하거나derive속성을 사용할 수 있음.
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
#[derive(Clone)]
struct Data {
point: Point,
label: String,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let d1 = Data { point: p1, label: String::from("Example"), };
let d2 = d1.clone();
println!("d1: {:?}, d2: {:?}", d1, d2);
}
Point는 Copy, Clone 모두 사용 할 수 있고 (굳이 Clone을 사용 할 필요도 없음. 메모리 카피로 충분하니까)
Data는 Clone만 사용 가능 (String이 힙에 저장되어 있으니)
Copy는 즉 아래의 특징을 가진다.
비트 단위 복사
Copy는 메모리 레이아웃 그대로 복사합니다.추가적인 함수 호출이나 동작 없이 원본 데이터를 그대로 복제합니다.
스택 메모리만 복사
Copy는 스택에 저장된 값만 복사합니다.힙 데이터를 참조하는 포인터는 복사되지만, 포인터가 가리키는 힙 메모리 자체는 복사되지 않습니다.
성능이 중요할 때 유용
비트 단위 복사이므로 연산 오버헤드가 거의 없습니다.
값의 크기가 작거나 단순한 경우 매우 효율적으로 작동합니다.
5. Copyr과 Clone 트레이트 구현의 상관 관계
Clone은 Copy를 구현할 필요가 없다.
Clone은 깊은 복사를 수행할 때 사용되며, 반드시 명시적으로 호출해야 한다.Copy없이Clone만 구현된 타입은 소유권 이동 없이도 복사 가능하지만, 복사를 위해서는.clone()을 호출해야 한다.
하지만 Copy는 Clone을 구현 해야한다.
Copy는 메모리 복사를 위한 간단한 동작으로, 복사를 위해 추가적인 연산이 필요하지 않다.Rust에서는 일반적으로
Copy가 구현된 타입은Clone도 함께 구현되는 것이 자연스럽다.이는
Clone이 깊은 복사를 명시적으로 호출해야 하는데,Copy와 함께 구현되면 일관성을 유지할 수 있기 때문이다.Rust 컴파일러는
Copy를 구현할 때Clone이 자동으로 구현되지 않으면 컴파일 에러를 발생시킨다.
6. Copyr과 Clone 트레이트를 직접 구현하기
이렇게 구현하는 경우는 트레이트의 동작을 사용자 정의하거나,
기본 구현에서 제공하지 않는 추가적인 작업을 수행하려는 경우 직접 구현한다.
6-1. Copy와 Clone을 직접 구현하는 방법
#[derive(Debug)]
struct Point { x: i32, y: i32, }
// Copy 트레이트 구현
impl Copy for Point {}
// Clone 트레이트 구현
impl Clone for Point {
fn clone(&self) -> Self {
// 사용자 정의 동작
*self // `Copy`가 구현되어 있으므로 단순히 값을 복사해 반환
}
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // `Copy`로 인해 값이 복사됨
let p3 = p1.clone(); // `Clone` 메서드를 명시적으로 호출
println!("p1: {:?}, p2: {:?}, p3: {:?}", p1, p2, p3);
}결과:
Cloning Point: (10, 20) p1: Point { x: 10, y: 20 }, p2: Point { x: 10, y: 20 }, p3: Point { x: 10, y: 20 }6-2. 좀 더 그럴싸한 예제
use std::sync::atomic::{AtomicUsize, Ordering};
// 전역적으로 고유 ID를 생성하기 위한 카운터
static NEXT_ID: AtomicUsize = AtomicUsize::new(1);
#[derive(Debug)]
struct User {
id: usize,
name: String,
}
// 사용자 정의 Clone 트레이트 구현
impl Clone for User {
fn clone(&self) -> Self {
let new_id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
println!("Cloning User: old_id = {}, new_id = {}", self.id, new_id);
User {
id: new_id, // 새 ID 생성
name: self.name.clone(),
}
}
}
fn main() {
let user1 = User {
id: NEXT_ID.fetch_add(1, Ordering::SeqCst),
name: "Alice".to_string(),
};
let user2 = user1.clone(); // 고유 ID가 부여된 복사본 생성
println!("{:?}", user1);
println!("{:?}", user2);
}
결과)
Cloning User: old_id = 1, new_id = 2
User { id: 1, name: "Alice" }
User { id: 2, name: "Alice" }6-3. 결론
**
#[derive(Copy, Clone)]**는 간단하고 일반적인 경우에 사용됩니다.직접
Copy또는Clone트레이트를 구현하면:추가적인 디버깅 로그를 추가하거나,
복사 과정에서 맞춤 동작을 정의할 수 있습니다.
일반적으로
Copy는 트리비얼한 복사만 가능하므로 구현할 일이 거의 없으며, 대부분Clone을 직접 구현합니다.
다음 차례)
- 컬렉션과 이터레이터
- Option 과 Result 및 예외처리
- 제네릭이란?
- 멀티쓰레드와 동기화객체
- select, channel 알아보기
- Tokio를 통한 비동기 예제 보기
- Custome Future 및 Runtime 만들어 보기