مقدمه

در این فصل با مفاهیمی آشنا می‌شویم که به ما اجازه می‌دهند کدهایی بنویسیم که بسیار انعطاف‌پذیر و قابل استفاده مجدد هستند و در عین حال، ایمنی نوع (type safety) را در زمان کامپایل حفظ می‌کنند.

فرض کنید می‌خواهیم تابعی بنویسیم که بزرگترین آیتم را در یک لیست پیدا کند. می‌توانیم یک تابع برای لیستی از i32 و یک تابع دیگر برای لیستی از f64 بنویسیم، اما این کار منجر به تکرار کد می‌شود. «نوع‌های جنریک» (Generic Types) به ما اجازه می‌دهند تا یک تابع یا یک ساختار داده را به صورت انتزاعی و مستقل از نوع‌های داده‌ی مشخصی که روی آنها کار می‌کند، تعریف کنیم.

جنریک‌ها در تعریف توابع

برای تعریف یک تابع جنریک، ما از یک نام پارامتر نوع (type parameter) در داخل براکت‌های زاویه‌ای (<>) بعد از نام تابع استفاده می‌کنیم. این پارامتر نوع، به عنوان یک جایگاه (placeholder) برای یک نوع داده‌ی واقعی عمل می‌کند. طبق قرارداد، نام پارامترهای نوع معمولاً یک حرف بزرگ و کوتاه است، مانند T.

بیایید تابع largest را به صورت جنریک بنویسیم تا بتواند روی لیستی از هر نوعی کار کند.

Copy Icon src/main.rs
fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        // This code will not compile yet!
        // if item > largest {
        //     largest = item;
        // }
    }

    largest
}

در امضای این تابع، ما یک پارامتر نوع به نام T تعریف کرده‌ایم. پارامتر list یک اسلایس از نوع T است و تابع نیز یک رفرنس به یک مقدار از نوع T را برمی‌گرداند. اما اگر سعی کنید این کد را کامپایل کنید، با خطا مواجه می‌شوید:

error[E0369]: binary operation `>` cannot be applied to type `&T`
                    

این خطا به ما می‌گوید که کامپایلر نمی‌داند چگونه دو مقدار از نوع T را با عملگر < مقایسه کند. T می‌تواند هر نوعی باشد و همه‌ی نوع‌ها قابل مقایسه نیستند. برای حل این مشکل، باید به کامپایلر بگوییم که T فقط می‌تواند نوع‌هایی باشد که قابلیت مقایسه شدن را دارند. این کار با استفاده از Traitها انجام می‌شود که در درس بعدی به تفصیل آنها را بررسی خواهیم کرد.

جنریک‌ها در تعریف struct و enum

ما می‌توانیم از نوع‌های جنریک در تعریف structها و enumها نیز استفاده کنیم تا ساختارهای داده‌ای بسازیم که بتوانند مقادیری از هر نوعی را در خود نگه دارند.

جنریک‌ها در struct

Copy Icon src/main.rs
struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let integer_point = Point { x: 5, y: 10 };
    let float_point = Point { x: 1.0, y: 4.0 };
    let mixed_point = Point { x: 5, y: 4.0 };
}

در اینجا، ما یک struct به نام Point با دو پارامتر نوع T و U تعریف کرده‌ایم. این به ما اجازه می‌دهد تا نقاطی با مختصات از نوع‌های مختلف (مثلاً هر دو i32، هر دو f64، یا ترکیبی از آنها) بسازیم.

جنریک‌ها در enum

ما قبلاً با دو enum جنریک بسیار پرکاربرد در کتابخانه استاندارد آشنا شده‌ایم: Option<T> و Result<T, E>. این enumها به صورت جنریک تعریف شده‌اند تا بتوانند هر نوع داده‌ای را در واریانت‌های خود نگه دارند.

جنریک‌ها در پیاده‌سازی متدها

ما می‌توانیم متدها را روی structها و enumهای جنریک پیاده‌سازی کنیم. برای این کار، باید پارامترهای نوع جنریک را بلافاصله بعد از کلمه کلیدی impl نیز اعلام کنیم.

Copy Icon src/main.rs
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

این کد یک متد به نام x را برای Point<T> پیاده‌سازی می‌کند که یک رفرنس به فیلد x برمی‌گرداند. همچنین، می‌توانیم یک پیاده‌سازی را فقط برای یک نوع جنریک خاص محدود کنیم.

Copy Icon src/main.rs
// This block will only be compiled for Point instances where T is f32.
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

این بلوک impl یک متد جدید به نام distance_from_origin را تعریف می‌کند که تنها روی نمونه‌هایی از Point که دارای نوع f32 هستند، قابل فراخوانی است.

در این درس با مفهوم قدرتمند نوع‌های جنریک برای نوشتن کدهای انتزاعی و قابل استفاده مجدد آشنا شدیم. دیدیم که چگونه می‌توان از آنها در توابع، structها، و enumها استفاده کرد. همچنین با این چالش مواجه شدیم که کامپایلر برای کار با نوع‌های جنریک، نیاز به اطلاعات بیشتری درباره قابلیت‌های آن نوع‌ها دارد. در درس بعدی، با Traitها به عنوان راه حل این مشکل و روش Rust برای تعریف رفتارهای مشترک آشنا خواهیم شد.