::은 “어디에 속한 항목인지” 또는 “어떤 타입인지”를 명시할 때 쓰이며, enum일 수도, 모듈일 수도, 제네릭 타입 지정(Turbofish)일 수도 있으며, 연관 함수, 상수 또는 연관 상수에 접근할 때도 사용되고, 트레이트 경로를 통한 메소드 호출 시에도 사용됩니다.
1. Rust에서 ::의 용도 정리표
구분
예시
설명
관련 키워드
모듈/경로 구분자
std::fs::File
모듈(std) → 하위 모듈(fs) → 항목(File)로 내려가는 경로 지정
모듈(module), 네임스페이스(namespace)
enum variant 접근
Option::Some(5)
Optionenum 안의 Somevariant에 접근
enum
연관 함수(associated function) 접근
String::from(“hi”)
타입(String)의 연관 함수(from) 호출
struct, enum, trait
상수 또는 연관 상수 접근
std::f64::consts::PI
f64타입의 연관 상수 PI에 접근
상수(const)
Turbofish (제네릭 타입 명시)
“42”.parse::<i32>()
제네릭 함수의 타입 매개변수를 명시적으로 지정
제네릭(Generic)
제네릭 타입 생성 시 타입 지정
Vec::<i32>::new()
제네릭 타입(Vec<T>)의 매개변수를 명시적으로 지정
제네릭 타입
트레이트 경로를 통한 메서드 호출
<T as SomeTrait>::method()
특정 트레이트에 정의된 메서드를 명시적으로 호출
트레이트(impl 충돌 해결용)
2. 예시 모음
mod math {
pub const PI: f64 = 3.14;
}
#[derive(Debug)]
#[allow(dead_code)]
enum Color {
Red,
Blue,
}
fn main() {
// 1️⃣ 모듈 경로
println!("{}", math::PI); // -> 3.14
// 2️⃣ enum variant 접근
let c = Color::Red;
println!("{c:?}");
// 3️⃣ 연관 함수
let s = String::from("hello");
println!("{s:?}");
// 4️⃣ Turbofish
let n = "42".parse::<i32>().unwrap();
println!("{n:?}");
// 5️⃣ 제네릭 타입 지정
let v = Vec::<i32>::new();
println!("{v:?}");
// 6️⃣ 트레이트 메서드 명시 호출
use std::string::ToString;
let x = 10;
let s = <i32 as ToString>::to_string(&x);
println!("{}", s); // "10"
}
Rust는 코드의 재사용성과 타입 안전성을 동시에 보장하기 위해 제네릭(Generic)과 트레잇(Trait)을 사용합니다. 이 개념은 Rust의 함수, 구조체, 열거형 등 다양한 곳에 적용되는데, 제네릭은 일반화된 타입을 의미하고, Trait은 공통된 행위 또는 특성을 의미합니다.
1. 제네릭(Generic)
가. 형식별 별도 함수
제네릭은 “특정 타입에 얽매이지 않고 다양한 타입에 대해 작동할 수 있도록 하는 일반화된 타입”을 의미합니다. 따라서, 타입에 의존하지 않는 유연한 코드를 작성할 수 있도록 도와줍니다.
아래 코드는 정수 타입 i32에 대해서는 largest_i32 함수를 사용하고, char에 대해서는 largest_char 함수를 따로 적용하고 있습니다.
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest_i32(&numbers);
println!("The largest number is {}", result);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&chars);
println!("The largest char is {}", result);
}
위 코드를 실행하면 아래와 같이 “가장 큰 수는 100, 가장 큰 문자는 y(번역)”라고 화면에 표시됩니다.
나. 제네릭 타입의 하나의 함수
위와 같이 i32와 char라는 데이터 타입에 따라 다른 함수를 하나의 함수로 합치기 위해 T라는 제네릭을 이용했습니다.
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("The largest number is {}", result);
let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("The largest char is {}", result);
}
위 코드를 실행하면 The largest number is 100 The largest char is y 가 화면에 출력되는데,
Generic T를 정의하는 구문에서 : PartialOrd + Copy를 제거하고 실행하면
error[E0369]: binary operation > cannot be applied to type T (에러 E0369: 이진 동작인 > 는 T 타입에 적용될 수 없습니다) 란 에러 메시지가 표시되고,
그 아래 T 다음에 “: std:cmp::PartialOrd를 추가하라고 합니다.
PartialOrd는 두 값을 비교하여 크거나 작거나 같은지 부분적으로만 결정할 수 있는 경우를 위해 설계되었습니다. 예를 들어 부동 소수점 숫자(floating-point numbers)의 경우 NaN (Not a Number) 값이 존재하기 때문에 완전한 비교가 불가능합니다. Ord는 두 값을 항상 비교할 수 있는 경우를 위해 설계되었습니다.
그래서 T 다음에 PartialOrd만 추가하고 Copy는 빼고 실행하면
let mut largest = list[0];의 list[0]에서 error[E0508]: cannot move out of type [T], a non-copy slice (복사할 수 없는 슬라이스인 `[T]` 유형에서 이동할 수 없습니다) 란 에러가 발생하고,
let mut largest = &list[0];에서는 type ‘T’가 Copy trait를 구현하지 않았기 때문에 move가 일어났는데, 여기서 data가 move될 수 없다고 합니다.
정수나 문자 타입에 공통적으로 적용하기 위해 제네릭 타입을 적용하는 것은 좋은데, 적용하기 위해 추가해야 할 요소들이 많습니다. 그리고, 두 개의 Trait을 연결하기 위해서는 +를 사용헸습니다.
다. 구조체에 한 가지 제네릭 적용
struct Point<T> {
x: T,
y: T,
}
fn main() {
let int_point = Point { x: 1, y: 2 };
let float_point = Point { x: 1.0, y: 2.0 };
println!("({:.1}, {:.1})", int_point.x, int_point.y);
println!("({:.1}, {:.1})", float_point.x, float_point.y);
}
위 코드의 경우 타입이 T 하나뿐이 없기 때문에, 타입이 한 가지인 경우만 적용가능합니다. 따라서 Point 구조체를 정의할 때 i32 또는 f64 타입 한 가지로만 지정했습니다.
그리고, {}라고 하면 실수인 경우에도 1로 출력이 돼서 {:.1}로 출력 형식을 변경했습니다.
라. 구조체에 두 가지 제네릭 적용
아래와 같이 하나의 구조체에 여러 타입, T와 U를 지정할 수 있습니다. 이 경우 T나 U는 특별한 의미가 있는 것이 아니고, 타입이 다르다는 것을 의미합니다.
struct Mixed<T, U> {
x: T,
y: U,
}
위에서 두 가지 타입을 지정했으므로 아래와 같이 i32와 f64를 같이하거나 달리해서 지정할 수 있습니다.
위 코드를 실행하면 (1, 2) (1.0, 2) 식으로 화면에 표시됩니다.
2. 트레잇(Trait)
트레잇은 공통된 동작을 정의하는 인터페이스입니다. 특정 타입이 트레잇을 구현하면, 해당 트레잇의 메서드를 사용할 수 있습니다.
Trait을 구현하는 방식은 impl Trait for Type으로서 impl Summary for Article이라고 적은 다음 중괄호 안에서 fn summarize로 메소드를 구현하는데, 인수는 &self로 자기 자신, 여기서는 Article을 받고, 반환 형식은 위에서 지정했듯이 String 타입이며, 구체적으로 실행하는 것은 format!(“{} by {}”, self.title, self.author)으로 self.title by self.author란 문자열을 생성합니다.
(4) main 함수에서 구조체 생성 및 트레잇 메소드 실행
main 함수에서는 구조체를 생성한 다음 aricle.summarize로 title과 author를 이용한 문자열을 만든(format) 후 println!로 화면에 출력합니다.
따라서, 위 코드를 실행하면 아래와 같이 “요약 : Rust 배우기 by 홍길동”이 출력됩니다.
나. 트레잇 바운드(Trait Bound)
함수 인자에서 트레잇을 사용하는 방법은 다음과 같이 세 가지가 있습니다.
(1) 케이스 1
item을 impl Summary로 지정하고, main 함수에서 notify 함수의 인수로 aritcle을 입력하는 것입니다.