انواع جنریک، ویژگیها (Traits)، و طول عمرها (Lifetimes)
هر زبان برنامهنویسی ابزارهایی برای مدیریت موثر تکرار مفاهیم دارد. در Rust، یکی از این ابزارها جنریکها هستند: جایگزینهای انتزاعی برای انواع مشخص یا ویژگیهای دیگر. ما میتوانیم رفتار جنریکها یا نحوه ارتباط آنها با جنریکهای دیگر را بیان کنیم بدون اینکه بدانیم هنگام کامپایل و اجرای کد چه چیزی جایگزین آنها خواهد شد.
توابع میتوانند پارامترهایی از نوع جنریک بگیرند، به جای یک نوع مشخص مانند i32
یا String
، به همان روشی که پارامترهایی با مقادیر ناشناخته میگیرند تا بتوانند کد مشابهی را روی مقادیر مشخص مختلف اجرا کنند. در واقع، ما قبلاً در فصل ۶ با Option<T>
، در فصل ۸ با Vec<T>
و HashMap<K, V>
، و در فصل ۹ با Result<T, E>
از جنریکها استفاده کردهایم. در این فصل، یاد خواهید گرفت که چگونه انواع، توابع، و متدهای خود را با جنریکها تعریف کنید!
ابتدا نحوه استخراج یک تابع برای کاهش تکرار کد را مرور میکنیم. سپس از همان تکنیک برای ایجاد یک تابع جنریک از دو تابع که تنها در نوع پارامترهایشان متفاوت هستند استفاده خواهیم کرد. همچنین توضیح خواهیم داد که چگونه میتوان از انواع جنریک در تعریف ساختار دادهها (struct) و شمارشها (enum) استفاده کرد.
سپس یاد میگیرید که چگونه از ویژگیها (Traits) برای تعریف رفتار به صورت جنریک استفاده کنید. میتوانید ویژگیها را با انواع جنریک ترکیب کنید تا نوع جنریک را محدود کنید که فقط آن نوعهایی را بپذیرد که رفتار خاصی دارند، به جای هر نوعی.
در نهایت، درباره طول عمرها (Lifetimes) صحبت خواهیم کرد: نوعی از جنریکها که به کامپایلر اطلاعاتی درباره نحوه ارتباط مراجع با یکدیگر میدهند. طول عمرها به ما اجازه میدهند اطلاعات کافی درباره مقادیر قرض گرفته شده به کامپایلر بدهیم تا اطمینان حاصل کند که مراجع در شرایط بیشتری معتبر خواهند بود.
حذف تکرار با استخراج یک تابع
جنریکها به ما اجازه میدهند که نوعهای مشخص را با یک جایگزین که نمایانگر چندین نوع است جایگزین کنیم تا تکرار کد را حذف کنیم. قبل از ورود به نحو جنریکها، ابتدا به نحوه حذف تکرار به روشی که شامل انواع جنریک نمیشود، با استخراج یک تابع که مقادیر مشخص را با یک جایگزین که نمایانگر مقادیر چندگانه است جایگزین میکند، نگاه خواهیم کرد. سپس از همان تکنیک برای استخراج یک تابع جنریک استفاده خواهیم کرد! با بررسی نحوه تشخیص کد تکراری که میتوانید به یک تابع استخراج کنید، شروع به تشخیص کد تکراری خواهید کرد که میتواند از جنریکها استفاده کند.
با برنامه کوتاه در لیست ۱۰-۱ که بزرگترین عدد را در یک لیست پیدا میکند، شروع میکنیم.
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {largest}"); assert_eq!(*largest, 100); }
ما یک لیست از اعداد صحیح را در متغیر number_list
ذخیره میکنیم و یک مرجع به اولین عدد در لیست را در متغیری به نام largest
قرار میدهیم. سپس تمام اعداد لیست را پیمایش میکنیم و اگر عدد فعلی بزرگتر از عدد ذخیره شده در largest
باشد، مرجع در آن متغیر را جایگزین میکنیم. با این حال، اگر عدد فعلی کوچکتر یا مساوی با بزرگترین عدد دیده شده تاکنون باشد، متغیر تغییری نمیکند و کد به عدد بعدی در لیست میرود. پس از بررسی تمام اعداد در لیست، largest
باید به بزرگترین عدد اشاره کند که در این مورد ۱۰۰ است.
اکنون از ما خواسته شده است که بزرگترین عدد را در دو لیست مختلف اعداد پیدا کنیم. برای انجام این کار، میتوانیم انتخاب کنیم که کد در لیست ۱۰-۱ را تکرار کنیم و از همان منطق در دو مکان مختلف در برنامه استفاده کنیم، همانطور که در لیست ۱۰-۲ نشان داده شده است.
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {largest}"); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!("The largest number is {largest}"); }
اگرچه این کد کار میکند، تکرار کد خستهکننده و مستعد خطاست. همچنین وقتی بخواهیم کد را تغییر دهیم، باید به یاد داشته باشیم که آن را در مکانهای مختلف بهروزرسانی کنیم.
برای حذف این تکرار، یک انتزاع ایجاد خواهیم کرد با تعریف یک تابع که روی هر لیستی از اعداد صحیح که به عنوان پارامتر پاس داده میشود عمل میکند. این راهحل کد ما را واضحتر میکند و به ما اجازه میدهد مفهوم یافتن بزرگترین عدد در یک لیست را به صورت انتزاعی بیان کنیم.
در لیست ۱۰-۳، کدی که بزرگترین عدد را پیدا میکند در تابعی به نام largest
استخراج میکنیم. سپس این تابع را فراخوانی میکنیم تا بزرگترین عدد را در دو لیست از لیست ۱۰-۲ پیدا کنیم. همچنین میتوانیم از این تابع روی هر لیست دیگری از مقادیر i32
که ممکن است در آینده داشته باشیم استفاده کنیم.
fn largest(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!("The largest number is {result}"); assert_eq!(*result, 6000); }
تابع largest
یک پارامتر به نام list
دارد که نمایانگر هر بخش مشخصی از مقادیر i32
است که ممکن است به تابع پاس دهیم. در نتیجه، وقتی تابع را فراخوانی میکنیم، کد روی مقادیر مشخصی که پاس میدهیم اجرا میشود.
به طور خلاصه، مراحل زیر را برای تغییر کد از لیست ۱۰-۲ به لیست ۱۰-۳ طی کردیم:
- کد تکراری را شناسایی کنید.
- کد تکراری را به بدنه یک تابع استخراج کرده و ورودیها و مقادیر بازگشتی آن کد را در امضای تابع مشخص کنید.
- دو نمونه از کد تکراری را به جای آن با فراخوانی تابع بهروزرسانی کنید.
در مرحله بعد، از همین مراحل با جنریکها برای کاهش تکرار کد استفاده خواهیم کرد. همانطور که بدنه تابع میتواند روی یک list
انتزاعی به جای مقادیر مشخص عمل کند، جنریکها به کد اجازه میدهند که روی انواع انتزاعی عمل کند.
برای مثال، فرض کنید دو تابع داشتیم: یکی که بزرگترین مورد را در یک بخش از مقادیر i32
پیدا میکند و دیگری که بزرگترین مورد را در یک بخش از مقادیر char
پیدا میکند. چگونه میتوانیم این تکرار را حذف کنیم؟ بیایید پیدا کنیم!