Intro
Generic이란? Type에 의존하지 않는 유연한 코드를 작성할 수 있도록 도와주는 기능입니다. Generic을 처음 접하게 되면 Code의 새로운 문법으로 인한 거부감이 있을 수 있습니다. 하지만 Rust에서 Generic은 꽤 이점이 많은 기능입니다.
- 코드 재사용성 향상 : 여러가지 Type에 대해서 동작이 필요할 때 각 Type별로 작성할 필요 없이 Generic을 통해 범용 코드를 작성할 수 있습니다.
- 타입 안전성 보장 : Generic을 사용하면 Compiler가 코드에서 type의 일관성을 검증합니다. 특히 Trait Bound (Generic Constraints) 로 특성이 포함되지 않은 Type은 Generic에 허용하지 않도록 Compiler가 검증합니다.
- zero-cost abstraction : "사용하지 않는 기능에 대해서는 비용을 지불하지 않는다"는 철학입니다. Rust의 제네릭(Generic)은 컴파일 시점에 사용된 구체적인 타입으로 변환(모놀리틱 처리)되어, 각 타입에 대해 최적화된 코드를 생성합니다. 이 과정은 컴파일 타임에 이루어지며, 제네릭 사용으로 인한 추가적인 런타임 비용이 발생하지 않습니다.
generic은 그 효과성, 그리고 문법 둘 다 중요한 요소이기 때문에 다양한 예시를 담은 code를 준비해 보았습니다. 확인해 보겠습니다.
Code Example
Rust 1.79.0 (released 2024-06-13)
#[allow(warnings)]
fn main(){
// ============================================================================================
// Generic : Type에 의존하지 않고 유연한 코드를 작성할 수 있는 기능
// ============================================================================================
/* Generic struct */
// struct에서 사용되는 필드의 type을 generic 선언했다.
struct Point<T>{
x: T,
y: T,
}
let p1 = Point{x: 12, y: 34}; // 정수값을 넣으면 기본 i32 Type로 추론된다.
let p2: Point<u8> = Point{x: 12, y: 34}; // 특정 Type을 지정할 수도 있다.
// 여기까지 Point를 Generic으로 사용했다면 Compile time에서 아래와 같은 code가 생성된다.
// 컴파일 타임에 다음과 같이 구체적인 타입으로 변환된다 (Monomorphization).
struct PointI32{
x: i32,
y: i32,
}
struct PointU8{
x: u8,
y: u8,
}
let p1 = PointI32{x:12, y: 34};
let p2 = PointU8{x:12, y: 34};
/* impl + Generic struct */
impl<T> Point<T> { // impl<T>로 작성해야 함. specific type과 문법적 구분 필요.
fn new(x: T, y: T) -> Self {
Self{x, y}
}
fn x(&self) -> &T {
&self.x
}
}
/* impl + 특정 type struct. */
// f64 type인 경우만 distance를 구하도록 했다.
impl Point<f64> {
fn distance(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
let p1: Point<i32> = Point::new(10, 20);
// [Error] no method named `distance` found for struct `m_10_generic::example::Point<i32>` in the current scope
// p1.distance();
let p2: Point<f64> = Point::new(10.0, 20.0);
println!("p2.distance : {}", p2.distance());
/* 변수 개수만큼 Generic 사용 + method */
#[derive(Debug)]
struct GenericPoint<T, U>{
x: T,
y: U,
}
impl<T1, U1> GenericPoint<T1, U1> {
// impl은 T1, U1을 사용, mix_up은 other의 type을 받은 후 self와 type을 합침
fn mix_up<T2, U2>(self, other: GenericPoint<T2, U2>) -> GenericPoint<T1, U2>{
GenericPoint{
x: self.x,
y: other.y,
}
}
}
let gp1: GenericPoint<u8, f64> = GenericPoint{x:1, y:2.2};
let gp2: GenericPoint<f64, i32> = GenericPoint{x:3.3, y:4};
let gp3 = gp1.mix_up(gp2); // GenericPoint<u8, i32>
/* Generic Constraints (Trait Bounds) : 특정 동작이 구현된 Generic type만 허용하도록 하는 방법 */
// Display trait 예 : "print 동작이 구현된 Generic type만 허용"하려면 Display trait을 사용
// 문법으로는 <T: Display>로 작성해서 T에 Display trait이 구현된 Type만 허용하도록 명시한다.
use std::fmt::Display;
fn display_item<T: Display>(item: T) {
println!("{}", item); // Display trait
// println!("{:?}", item); // [Error] "{:?}"은 Debug trait 이다.
}
// 2개 이상의 Trait이 필요할 때는 +로 표현한다.
use std::fmt::Debug;
fn debug_item<T: Display + Debug>(item: T){
println!("{}", item); // Display trait
println!("{:?}", item); // Debug trait
}
// PartialOrd trait 예 : PartialOrd는 크기를 비교할 수 있는 type에 대한 특성이다.
struct Item<T> {
value: T,
}
use std::cmp::Ordering;
impl<T: PartialOrd> Item<T> {
fn is_bigger(&self, other: &Item<T>) -> bool {
self.value > other.value // PartialOrd trait제한으로 크기 비교가 가능한 Type만 허용
}
}
struct Dot<T>{
x: T,
y: T,
}
// no partialOrd type (custom type : Dot)
let item_string1: Item<Dot<i32>> = Item{value: Dot{x:1, y: 2}};
let item_string2: Item<Dot<i32>> = Item{value: Dot{x:2, y: 3}};
// [Error] Dot에는 PartialOrd trait 구현이 안되어 있어 Compare할 수 없다.
// item_string1.compare(&item_string2);
// partialOrd type (i32 has partialOrd trait)
let item_int1: Item<i32> = Item{value: 10};
let item_int2: Item<i32> = Item{value: 20};
println!("item_int1 is_bigger than item_int2 : {}", item_int1.is_bigger(&item_int2));
/* Where keyword를 사용하여 Generic Constraints를 구현 */
#[derive(PartialEq, PartialOrd)]
struct Position<T> {
x: T,
y: T,
}
use std::ops::Add;
// 아래와 같이 작성해도 된다.
// impl<T: Add<Output = T> + PartialOrd + Copy> Position<T>{
impl<T> Position<T>
where
T: Add<Output = T> + PartialOrd + Copy, // 크기 비교, Add가 가능한 T, Copy
// U: ...
// V: ...
// 이와 같이 많은 type에 대한 다양한 trait을 작성해야 할 경우 where을 사용한다.
{
fn ge(&self, other: &Self) -> bool {
(self.x + self.y) >= (other.x + other.y)
}
fn gt(&self, other: &Self) -> bool {
(self.x + self.y) > (other.x + other.y)
}
fn le(&self, other: &Self) -> bool {
(self.x + self.y) <= (other.x + other.y)
}
fn lt(&self, other: &Self) -> bool {
(self.x + self.y) < (other.x + other.y)
}
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let self_sum = self.x + self.y;
let other_sum = other.x + other.y;
if self_sum < other_sum {
Some(Ordering::Less)
} else if self_sum > other_sum {
Some(Ordering::Greater)
} else {
Some(Ordering::Equal)
}
}
}
// Position<i32>
let pos1 = Position{x: 10, y: 20};
let pos2 = Position{x: 15, y: 5};
if pos1 > pos2 {
println!("pos1 is greater then pos2");
} else {
println!("pos1 is not greater then pos2");
}
// Position<f64>
let pos3 = Position{x: 2.1, y: 3.4};
let pos4 = Position{x: 1.2, y: 2.5};
if pos3 > pos4 {
println!("pos3 is greater then pos4");
} else {
println!("pos3 is not greater then pos4");
}
}
'Rust > Basic' 카테고리의 다른 글
[Rust Tutorial] 11 - Option (0) | 2024.11.25 |
---|---|
[Rust Tutorial] 9 - mod, use (2) | 2024.11.20 |
[Rust Tutorial] 8 - Struct, Impl, Trait (0) | 2024.11.18 |
[Rust Tutorial] 7 - Ownership (0) | 2024.11.17 |
[Rust Tutorial] 6 - Control Flow (0) | 2024.11.17 |