وبلاگ شخصی
محمّدرضا
علی حسینی

جایی که تجربیات, علایق
و چیزهایی که یادگرفته‌ام را
با هم مرور می‌کنیم.

آموزش زبان برنامه‌نویسی Rust - قسمت ۱۸ : شروع کار با Generic ها

آموزش زبان برنامه‌نویسی Rust - قسمت  ۱۸ : شروع کار با Generic ها

در دو قسمت قبلی ابتدا با همدیگر مفهوم چندریختی (Polymorphism) را مرورکردیم. بعدش شیوه‌ی اوّلی که Rust برای پیاده‌سازی آن پیش‌گرفته است، یعنی ویژگی (trait)، را دیدیم. حالا در این قسمت می‌خواهیم دومین راه حل Rust را برای پیاده‌سازی مفهوم چندریختی یادبگیریم.

Generic چیست؟

اوّل ببینیم که generic چه چیزی است. شما فرض کنید که ما در یک جای کدمان می‌خواهیم میانگین آرایه‌ای که ۳ عنصر ۱، ۲ و ۳ را دارد به دست بیاوریم:

let my_first_array = [1, 2, 3];
let mut sum = 0;
for array_item in my_first_array.iter() {
    sum += *array_item;
}
println!("Average value of my_first_array is: {}", sum / my_first_array.len());

حالا در یک بخش دیگر از کد یک آرایه‌ی چهارتایی داریم که می‌خواهیم میانگین آن را هم حساب کنیم:

let my_second_array = [1, 2, 3, 4];
let mut sum2 = 0;
for array_item in my_second_array.iter() {
    sum2 += *array_item;
}
println!("Average value of my_second_array is: {}", sum2 / my_second_array.len());

خب ما برای اینکه از تکرار شدن این کد مشابه در تمام برنامه‌مان جلوگیری کنیم چه کاری می‌کنیم؟ می‌آییم و یک تابع برای گرفتن میانگین یک آرایه می‌نویسیم:

fn get_average(array: &[usize]) -> usize {
    let mut count = 0;
    for array_item in array.iter() {
        count += *array_item;
    }
    return count / array.len();
}

fn main() {
    println!("Average value of my_first_array is: {}", get_average(&my_first_array));
    println!("Average value of my_secondP_array is: {}", get_average(&my_second_array));
}

الان این کد دقیقاً همان کار قبلی را می‌کند، با این تفاوت که به جای آنکه دو بار تمام کد مرتبط با محاسبه‌ی میانگین را بنویسیم، با یک بار نوشتن آن در قالب یک تابع همان کار را انجام می‌دهیم.

اینطوری چون کد کمتری نوشته‌ایم، هم برنامه سریع‌تر نوشته می‌شود و هم احتمال خطا در آن کاهش می‌یابد. چون هرچقدر که کدهای بیشتری بنویسیم احتمال وجود باگ لابه‌لای آن خط‌ها بیشتر می‌شود.

خب حالا تابعی که ما نوشتیم تنها می‌تواند میانگین آرایه‌هایی که داده‌هایشان از نوع usize است را به دست بیاورد.

اگر بخواهیم میانگین آرایه‌هایی که از نوع i32 هستند را محاسبه کنیم باید چه کار کنیم؟ مجبوریم که یک تابع جدید عیناً شبیه به همین تابع بنویسیم که تنها نوع ورودی‌اش متفاوت است.

fn get_average_of_i32(array: &[i32]) -> i32 {
    let count = 0;
    for array_item in array.iter() {
        count += *array_item;
    }
    return count / array.len() as i32;
}

این تابع دقیقاً شبیه به همان تابع قبلی است. تفاوتش با آن تابع در دو تا چیز است. اوّل اینکه ورودی و خروجی آن از جنس i32 است. دومی هم اینکه در خط آخر تابع، ما مقدار ()array.len را به نوع i32 تبدیل (cast) کرده‌ایم.

هرچند که این کار ربطی به مسئله‌ی Generic ندارد، ولی قبل از اینکه به سراغ همان بحث اصلی‌مان برویم این موضوع را هم با هم بررسی می‌کنیم.

فعلاً مسئله‌ی اصلی این است که ما دوباره به همان مشکلی برخوردیم که برای رهایی از آن به تعریف توابع روی آورده بودیم.

ما داریم کد تکراری می‌نویسیم چون نوع ورودی تابع هر بار تغییر می‌کند. حالا نمی‌شود که همان کاری که دفعه‌ی پیش برای جلوگیری از تکرار کردیم را تکرار کنیم؟ یعنی توابع را طوری بنویسیم که برای انواع مختلف داده‌ها کار کنند؟ معلوم است که می‌شود. اسم این کار Generic است.

تبدیل داده‌ها به انواع مختلف (Type Casting)

قبل از اینکه حرف‌زدن درمورد Generic ها را شروع کنیم برویم سراغ آن کلمه‌ی as آخر مثال قبلی. ما در Rust تبدیل نوع ضمنی نداریم. یعنی اینطوری نیست که شما بتوانید یک کاراکتر را در یک u32 بریزید. اگر می‌خواهید کاری شبیه به این انجام بدهید، باید به صورت دقیق در کد مشخّص‌کنید که قرار است چه نوع داده‌ای به چه نوعی تبدیل شود.

برای این کار در زبان Rust کلمه‌ی کلیدی as قرار داده شده است. برای تبدیل یک نوع به نوع دیگر کافی است پس از مقدار، کلمه‌ی as را قرار بدهیم و نوعی را که می‌خواهیم داده‌مان به آن تبدیل شود بنویسم.

مثلاً شما فرض‌کنید که می‌خواهیم یک مقدار u8 را به یک عدد i32 تبدیل کنیم:

fn main() {
	let a: u8 = 10;
    let b: i32 = a as i32;
    println!("a value is: {}", a);
    println!("b value is: {}", b);
}

اگر این برنامه را اجرا کنیم می‌بینم که هر دو متغیّر مقدار ۱۰ را در خود دارند.

محدودیّت‌های تبدیل نوع

تبدیل نوع‌های مختلف داده به هم همیشه اینقدر آسان نیست. خیلی وقت‌ها داده پس از تغییر نوع خراب می‌شود. پس ما باید حواسمان موقع تبدیل نوع خیلی جمع باشد.

ما چند محدودیّت عمده هنگام تبدیل نوع داریم:

۱- تبدیل اعداد علامت‌دار به عدد بدون علامت

شما فرض‌کنید که می‌خواهید یک عدد منفی را به یک نوع‌داده‌ی بدون علامت تبدیل کنید. حالا تکلیف آن علامت منفی چه می‌شود؟

fn main() {
	let a: i8 = -10;
    let b: u8 = a as u8;
    println!("a value is: {}", a);
    println!("b value is: {}", b);
}

اگر این برنامه را اجرا کنیم خروجی چه چیزی خواهد بود؟

a value is: -10
b value is: 246

همانطوری که می‌بیند مقداری که پس از تبدیل نوع (type casting) در متغیّر b ریخته شده است اشتباه است. حالا این مقدار اشتباه از کجا می‌آید؟

ما اعداد منفی را به صورت مکمّل ۲ نمایش می‌دهیم. یعنی اوّل معادل مثبت آن عدد را به صورت باینری می‌نویسیم. بعد تمام بیت‌های آن عدد را برعکس می‌کنیم (یعنی ۱ ها را به ۰ و ۰ ها را به ۱ تبدیل می‌کنیم). بعدش هم مقدار جدید را با ۱ جمع می‌کنیم.

معادل دودویی عدد ۱۰ می‌شود مقدار زیر:

00001010

حالا ما باید تمام بیت‌ها را برعکس کنیم:

11110101

و حالا باید این مقدار را با ۱ جمع کنیم:

11110110

این مقدار آخر چیزی است که به عنوان عدد −10 در حافظه قرار می‌گیرد. حالا ما می‌خواهیم این مقدار را به یک عدد بدون علامت تبدیل کنیم. چه اتّفاقی می‌افتد؟ بیت آخر (سمت چپ‌ترین بیت) به جای اینکه علامت را نشان بدهد، خودش هم بخشی از عدد به حساب می‌آید. پس عدد به جای اینکه ۱۰− درنظر گرفته شود، تبدیل می‌شود به عدد ۲۴۶+.

۲-تبدیل از نوع‌داده‌ی بزرگ‌تر به نوع‌داده‌ی کوچک‌تر

مشکل دیگری که ممکن است رخ‌بدهد زمانی است که ما می‌خواهیم از یک نوع‌داده‌ی بزرگ مقدارمان را به نوع داده‌ای کوچک تبدیل کنیم. مثلاً عددی از نوع u64 را به u8 تبدیل کنیم. اگر عدد در بازه‌ی اعداد قابل نمایش برای u8 نباشد، داده‌ی ما خراب می‌شود و پس از تبدیل مقدار عدد تغییر می‌کند.

fn main() {    
	let a: u64 = 1_000_000;
    let b: u8 = a as u8;
    println!("a value is: {}", a);
    println!("b value is: {}", b);
}

اگر این برنامه را اجرا کنیم خروجی زیر را دریافت می‌کنیم:

a value is: 1000000
b value is: 64

دلیل این تغییر چیست؟ ما صرفاً ۸ بیت داده را می‌توانیم در نوع u8 نگهداری‌کنیم. این یعنی بزرگترین عدد در این نوع عدد ۲۵۵ است. حالا اگر بخواهیم عددی که برای نمایشش به بیشتر از ۸ بیت داده احتیاج است را در آن بریزیم، باید بیت‌های اضافی دور ریخته بشوند(اگر نحوه‌ی نمایش عدد یک میلیون برایتان عجیب به نظر می‌رسد، یعنی قسمت مربوط به مقادیر عددی را فراموش‌کرده‌اید. با کلیک روی این نوشته خیلی سریع آن را مرور کنید).

۳ تبدیل انواع غیر بنیادین

ما با کلمه‌ی کلیدی as تنها می‌توانیم نوع‌داده‌هایی را تبدیل کنیم که به عنوان انواع بنیادین (Primitive Types) شناخته می‌شوند. هرچند که در اصطلاحات زبان Rust انواع بنیادین بیشتر از این‌ها هستند، امّا برای تبدیل نوع ما صرفاً می‌توانیم با اعداد و مقادیر بولی کار کنیم.

اگر بخواهیم از انواع دیگر استفاده کنیم، یا باید با raw pointer کار کنیم یا با ویژگی From. بعداً به این دو حالت هم می‌پردازیم.

خب حالا وقت این است که برویم سراغ مبحث اصلی خودمان.

ساخت یک ساختار Generic

شما فرض‌کنید که ما می‌خواهیم یک ساختار (struct) به نام Point داشته باشیم. این ساختار گاهی اوقات برای نمایش یک نقطه روی صفحه‌ی نمایش استفاده می‌شود و گاهی اوقات برای نمایش یک نقطه روی کاغذ توسّط چاپگر.

طول و عرض نقطه‌هایی که چاپگر آن‌ها را می‌فهمد در مقیاس سانتی‌متر نوشته‌شده‌اند. بنابراین مقادیر x و y آن می‌توانند اعداد اعشاری هم باشند. امّا نقطه‌هایی که برای صفحه‌ی نمایش استفاده می‌شوند در مقیاس پیکسل هستند، پس یک عدد صحیح بدون علامت هم می‌تواند آن‌ها را مشخّص کند.

طبق چیزی که تا الان یادگرفته‌ایم باید برای این کار دو struct مجزا تعیین کنیم:

struct PointForScreen {
    x: u16,
    y: u16
}

struct PointForPrinter {
    x: f32,
    y: f32
}

هر دوی این ساختارها برای ما یک مفهوم واحد، یعنی نقطه، را نمایش می‌دهند و نحوه‌ی رفتار ما با موجودیّت‌هایی که از این دو نوع ساخته شده اند یکسان است.

به همین خاطر ما باید متدهای مربوط به نقطه را برای هر دو ساختار عیناً تکرار کنیم. مثلاً فرض‌کنید که ما یک متد داریم که جای مقادیر x و y را عوض می‌کند. برای کد فعلی باید یک کد ثابت را برای هردو ساختار تکرار کنیم:

impl PointForPrinter {
    fn swap_coordinates(&mut self) {
        let old_y = self.y;
        self.y = self.x;
        self.x = old_y;
    }
}

impl PointForScreen {
    fn swap_coordinates(&mut self) {
        let old_y = self.y;
        self.y = self.x;
        self.x = old_y;
    }
}

گفتیم که ما از Generic برای جلوگیری از همین تکرار می‌خواهیم استفاده کنیم. امّا چطوری؟

بیایید اوّل ساختار Generic را ببینیم و بعد درمورد بخش‌های مختلفش صحبت کنیم:

struct Point<T> {
    x: T,
    y: T
}

همانطوری که می‌بینید درست مثل یک ساختار عادی ابتدا کلمه‌ی کلیدی struct را نوشته‌ایم. بعد از آن هم اسم ساختار را قرار داده‌ایم. تفاوت اصلی درست بعد از نام ساختار (struct) شروع می‌شود.

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

مثلاً ما اگر یک نمونه از ساختار Point را مثل کد زیر بسازیم، به جای T مقدار u8 قرار می‌گیرد. این یعنی اینکه نوع مقادیر x و y می‌شود u8.

let pointer_u8 = Point::<u8> {
    x: 10,
    y: 12
};

همانطوری که می‌بینید برای مشخّص کردن T کافی است که بعد از اسم ساختار علامت :: بگذاریم و بعد داخل علامت‌های کوچک‌تر و بزرگ‌تر نوع دلخواه‌مان را بنویسیم.

شما الان می‌توانید هر مدل نقطه‌ای که دوست‌دارید را با همین ساختار (struct) پیاده‌سازی کنید.

let float_pointer = Point::<f32> {
        x: 0.0,
        y: 666.32
};

حالا ما می‌توانیم اصلاً نوع نقطه را هم مشخّص نکنیم و اجازه بدهیم که خود کامپایلر از مقادیری که می‌دهیم آن را تشخیص بدهد.

مثلاً در کد زیر نوع مقادیر x و y می‌شود i32:

let detect_my_type = Point {
        x: 10,
        y: 11
};

فقط حواس‌تان باشد که T یک مثال است و شما هر اسمی که دوست‌دارید را می‌توانید بنویسید. ولی خب استفاده از حروف بزرگ الفبای انگلیسی (عموماً آن آخری‌ها مثل T و W و…) معمول‌تر است.

ساخت توابع Generic

حالا ما عین همین کار را می‌توانیم با توابع هم انجام بدهیم. یعنی تابعی داشته باشیم که پارامترها و خروجی‌اش نوع‌های مختلفی داشته باشند.

فرض‌کنید که می‌خواهیم یک تابع بنویسیم که یک نقطه از جنس ‍Point را بگیرد و به عنوان خروجی یک نقطه‌ی دیگر را بدهد که جای x و y آن برعکس نقطه‌ی ورودی است.

کد ما این شکلی خواهد بود:

fn swap_point<T>(original_point: Point<T>) -> Point<T> {
    let Point {x, y} = original_point;
    return Point {
        x: y,
        y: x
    }
}

ما ابتدا مثل توابع معمولی کلمه‌ی کلیدی fn و نام تابع را نوشته‌ایم. سپس مقابل نام و بین علامت‌های کوچک‌تر و بزرگ‌تر، پارامتر مشخّص‌کننده‌ی نوع (Type Parameter) را قرار داده ایم. ما از این پارامتر می‌توانیم برای تعیین نوع پارامترهای ورودی، مقدار خروجی و یا مقادیری که درون بدنه‌ی تابع تعریف می‌شوند استفاده کنیم.

مثلاً در همین مثال، ما گفته‌ایم که پارامتر ورودی ما یک نمونه از ساختار Point است که جنس مقادیرش از نوع T است. درست مثل همان چیزی که در مثال بخش قبل در مورد ساختارها دیدیم.

بعدش هم مشخّص کرده‌ایم که خروجی تابع ما هم یک نمونه از ساختار Point است که مقادیرش از جنس T هستند.

داخل تابع هم ابتدا با یک الگو (Pattern) مقادیر x و y پارامتر ورودی را استخراج کرده‌ایم و درون دو تا متغیّر محلّی با نام‌های x و y ریخته‌ایم. حواستان باشد که این x و y را با مقادیر هم‌نام در ساختار Point اشتباه نگیرید (اگر نحوه‌ی استخراج مقادیر درون ساختارها را با استفاده از الگوها از یاد برده‌اید، با کلیک روی این نوشته خیلی سریع همه چیز را به خاطر بیاورید).

آخر سر هم یک نمونه‌ی جدید از Point ساخته‌ایم. مقدار x آن را برابر متغیّر y درون الگو قرار داده‌ایم و برای مقدار y هم عکس این کار را انجام داده‌ایم. اینطوری هر نمونه‌ای از ساختار Point را که به این تابع بدهیم به ما یک نقطه‌ی دیگر را بر می‌گردد که مقادیر x و y آن حاصل جابه‌جا کردن مقادیر مشابه در نقطه‌ی ورودی است.

شما می‌توانید با همین ساختار هر تابعی که خواستید را به صورت Generic بنویسید.

حالا چطوری از این تابع استفاده کنیم؟

fn main() {
    let my_point = Point::<u8> {
        x: 10,
        y: 12
    };

    let swapped_point = swap_point(my_point);
    println!("swapped point x: {}, y: {}", swapped_point.x, swapped_point.y);
}

همانطوری که می‌بینید ما ابتدا یک نمونه از ساختار Point را از نوع u8 تعریف کرده‌ایم. سپس نقطه‌ای که ساخته‌ایم را به تابع swap_point داده‌ایم تا مقادیرش را برایمان جابه‌جا کند و یک نقطه‌ی جدید بسازد. حالا اگر برنامه را اجرا کنیم می‌توانیم مقادیر نقطه‌ی جدید را ببینیم:

swapped point x: 12, y: 10

(نکته: فقط حواستان باشد که ما با فرستادن خود مقدار my_point به تابع در حقیقت مقدار این متغیّر را move کرده‌ایم و دیگر نمی‌توانیم از آن استفاده کنیم. اگر این موضوع را فراموش کرده‌اید با کلیک روی این نوشته به قسمت مربوط به آن بروید و خیلی سریع این مفهوم را مرور کنید.)

ما توی این مثال از حالت خلاصه‌ی فراخوانی یک تابع Generic استفاده کرده‌ایم. اگر به هر دلیلی مجبور شدید که به صورت دقیق نوع پارامتر تعیینِ نوع را مشخّص کنید، می‌توانید از این سینتکس استفاده کنید:

swap_point::<u8>(my_point)

داخل علامت‌های کوچک‌تر و بزرگ‌تر نوعی که جایگزین T می‌شود قرار می‌گیرد.

ساخت متدهای Generic

ساخت یک متد Generic خیلی خیلی شبیه به ساخت یک تابع Generic است. تنها تفاوتش در این است که ما باید تمام پارامترهای تعیین نوع را هنگام تعریف بخش imp هم معرّفی کنیم. (نحوه‌ی تعریف متدها را فراموش کرده‌اید؟)

impl<Type> Point<Type> {
    fn swap_coordinates(&mut self) {
        mem::swap(&mut self.x, &mut self.y);
    }
}

خب اینجا برای اینکه ببینید می‌شود به جای T هر عبارت دیگری را هم قرار داد از کلمه‌ی Type استفاده کرده‌ایم. بعد داخل متد swap_coordinates می‌خواهیم که مقادیر x و y ساختار فعلی را عوض کنیم.

کاری که اینجا داریم می‌کنیم با قبلی متفاوت است. ما داریم از تابع swap برای جابه‌جایی این دو مقدار استفاده می‌کنیم که درون کتابخانه‌ی استاندارد Rust از قبل وجود دارد. کاری که این تابع می‌کند این است که دو تا رفرنس به عنوان ورودی می‌گیرد و مقادیر این دو رفرنس را با هم عوض می‌کند. یعنی اگر یک رفرنس به خانه‌ی شماره‌ی ۱۰ اشاره می‌کند و یکی به ۱۱، این تابع مقداری که درون خانه‌ی شماره‌ی ۱۱ است را در خانه‌ی ۱۰ می‌ریزد و مقداری که درون خانه‌ی ۱۰ قرار دارد را درون خانه‌ی ۱۱. استفاده از این تابع به جای اینکه خودمان دستی این کد را بنویسیم به خوانایی و نگهداری کد کمک می‌کند.

اگر پیاده‌سازی این تابع را نگاه کنید می‌بینید که پیاده‌سازی‌اش دقیقاً همان کاری است که ما در متدهایی که ابتدای این قسمت نوشته‌ایم کرده‌ایم. حالا هروقت به کدهای unsafe رسیدیم با هم پیاده‌سازی این تابع را هم بررسی می‌کنیم.

موقع استفاده از این تابع صرفاً‌ باید دو مورد را حتماً به خاطر داشته باشید:

اوّل اینکه برای استفاده از این تابع باید با نوشتن این خط در ابتدای فایلتان به کامپایلر بفهمانید که از کجا باید این تابع را پیدا کند (بعداً که به ماژول‌ها و … رسیدیم بیشتر در مورد این می‌فهمیم):

use std::mem;

دوم اینکه ورودی‌های این تابع باید دو رفرنس mutable آن هم از یک نوع باشند.

خب حالا بیایید کدی که نوشتیم را امتحان کنیم:

fn main() {
    let mut my_point = Point::<u8> {
        x: 10,
        y: 12
    };
    println!("Before swap x: {}, y: {}", my_point.x, my_point.y);
    my_point.swap_coordinates();
    println!("Before swap x: {}, y: {}", my_point.x, my_point.y);
}

حالا اگر این برنامه را اجرا کنیم می‌بینم که خروجی زیر را به ما می‌دهد:

Before swap x: 10, y: 12
Before swap x: 12, y: 10

ساخت enum های Generic

ساخت enum به صورت Generic هم درست مثل قبلی‌ها است. بیایید مثلاً یک نمونه‌ی ساده از enum پرکاربرد Result را که در قسمت‌های آینده با آن آشنا می‌شویم بسازیم.

enum Result<T> {
    OK(T),
    Err(())
}

قرار است که اگر اجرای تابع موفّقیّت‌آمیز بود، نتیجه‌ی اجرای آن درون مقدار OK قرار بگیرد و اگر هم ناموفّق بود، مقدار Err این enum برگردانده شود.

همانطوری که می‌بینید ما نوعی که درون OK قرار می‌گیرد را به صورت Generic تعریف کرده‌ایم تا هرچیزی بتواند درون آن قرار بگید.

حالا بیایید یک مثال به ظاهر پیچیده را ببینیم:

fn main() {
    let mut my_point = Point::<u8> {
        x: 10,
        y: 12
    };

    let that_is_ok = Result::OK::<Point<u8>>(my_point);

    match that_is_ok {
        Result::OK(Point {x, y}) => println!("x: {}, y: {}", x, y),
        Result::Err(_) => println!("An Error happened"),
        _ => {}
    };
}

ما ابتدا مثل قبل یک نقطه از نوع ساختار Point ساخته‌ایم. سپس یک مقدار OK ساخته‌ایم. برای این کار، بعد از Result::OK ما یک علامت :: گذاشته‌ایم و مقابلش نوعی را که قرار است جایگزین پارامتر تعیینِ نوعِ T بشود مشخّص کرده‌ایم.

قرار است OK حامل یک نمونه از ساختار Point باشد که مقادیرش از نوع u8 هستند. سپس مقدار my_point را هم درون OK ریخته‌ایم تا بعداً بتوانیم به وسیله‌ی یک الگو (pattern) آن را استخراج کنیم.

حالا به بخش match می‌رسیم. در الگوی اوّل، اگر مقداری که درون that_is_ok قرار داشت با الگو هم‌خوانی داشت، مقادیر x و y نقطه‌ای که درون OK نگهداری می‌شود را درون دو متغیّر جدید با نام‌های x و y استخراج می‌کنیم. سپس این مقادیر را چاپ می‌کنیم.

اگر مقدار that_is_ok از نوع Err بود پیام خطا چاپ می‌شود و اگر مقدار هرچیز دیگری بود هیچ اتّفاقی نمی‌افتد.

این حالت‌های تو در تو در یک کد خوب و تمیز که هر روز دست و رویش را می‌شوید و مادرش ازش راضی است کمتر رخ می‌دهند. امّا مطمئناً با این شرایط سر و کله خواهید زد. مهم این است که بدانید در اوایل کار قاطی کردن بعضی از این ترتیب‌ها عادی است و با یکم تمرین خیلی زود به آن‌ها عادت می‌کنید.

خب این هم از این قسمت ما. در قسمت بعدی باقی مباحث مربوط به Generic ها را با هم یادمی‌گیریم.

دریافت کدهای این قسمت

برای دریافت کدهای این قسمت کافی است که با کلیک روی این نوشته به بخش مربوطه در مخزن کدهای این مجموعه‌ی آموزشی روی گیت‌هاب بروید.

در قسمت قبلی نگاه خیلی دقیقی به ویژگی (trait) ها انداختیم. اگر آن قسمت را از دست داده اید با کلیک روی این نوشته به همان‌جا بروید.

دفعه‌ی اوّل است که این مجموعه‌ی آموزشی را می‌بینید؟ با کلیک روی این نوشته به قسمت ابتدایی این آموزش بروید.

«نوشته‌های مرتبط»

پاسخی بگذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

«نوشته‌های ویژه»

«نوشته‌های محبوب»

«دیدگاه کاربران»