زندگی بدون Slice
برای روشن شدن نقش و کاربرد اسلایسها، مسئلهی برنامهنویسی زیر را در نظر بگیرید.
فرض کنید قصد داریم تابعی بنویسیم که یک رشته شامل چند کلمه که با space از هم جدا شدهاند را
دریافت کند و اولین کلمهی موجود در رشته را برگرداند. ظاهر مسئله ساده است اما اگر خوب
دقت کنید، میبینید که ما راهی برای تعیین نوع بازگشتی تابع نداریم. چون نمیدانیم بخشی
از یک رشته
از چه نوعی محسوب میشود.
fn first_word(s: &String) -> ?
نام تابع را first_word گذاشتهایم. در مورد پارامتر تابغ هم چون نمیخواهیم مالکیت آرگومان را دریافت
کنیم،
از نوع &String استفاده شده که ابهامی ندارد. اما در مورد نوع بازگشتی چطور؟
اجازه دهید اول صورت مسئله را سادهتر کنیم و تابعی بنویسیم که اندیس اولین کاراکتر
space موجود در رشته را برگرداند. در این صورت، ابهامی در مورد نوع بازگشتی نداریم و آن را usize تعیین میکنیم.
src/main.rs
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
در این تابع از ویژگیهایی استفاده شده که هنوز راجع به آنها صحبت نشده و
بررسی دقیق آنها در فصلهای آینده صورت میگیرد. اما حداقل چیزهایی را که باید
برای درک این کدها بدانید، در اینجا بیان میکنیم.
اولین کاری که انجام شده، این است که رشتهی ورودی را با استفاده از متدی به نام
as_bytes() به یک آرایه از بایتها تبدیل کردهایم. این کار با این هدف انجام شده که در
ادامه، بتوانیم
رشته را برای پیدا کردن کاراکتر space پیمایش کنیم.
سپس، با استفاده از متد iter() یک تکرارگر یا iterator روی این آرایه ایجاد کردهایم.
در مورد تکرارگرها در فصل سیزدهم صحبت خواهیم کرد اما فعلاً همینقدر بدانید که متد iter()
روی عناصر یا آیتمهای یک کالکشن پیمایش کرده و آنها را یکییکی برمیگرداند. در ادامه، متد
enumerate() این عناصر را دریافت کرده و هر عنصر را به یک تاپل دوتایی نبدیل میکند.
به این ترتیب، مجموعهای از تاپلها داریم که هر یک از دو عنصر تشکیل شدهاند: یک اندیس و یک رفرنس به عنصر
(کاراکتری)
از رشته که دارای آن اندیس است. یک حلقهی for روی این تاپلها پیمایش کرده و چک میکند که
آیا عنصر یا کاراکتر برابر با کاراکتر space هست یا نه. اگر پاسخ مثبت باشد، اندیس متناظر عنصر را برمیگرداند.
در نهایت، یک عبارت s.len() دیده میشود که در صورتی که هیچ کاراکتر space در رشته
موجود نباشد و نتیجتاً دستور return اجرا نشود، مقدار این عبارت که طول رشته است، برگردانده
میشود.
این کار باعث میشود که اگر رشته فقط از یک کلمه تشکیل شده باشد، کل رشته به عنوان یک کلمه در نظر گرفته شود.
دقت داشته باشید که علت اینکه برای حلقهی for از الگوی (i,
&item) استفاده کردهایم، متد
enumerate() است. این متد یک تاپل دوتایی برمیگرداند که عنصر اولش
اندیس است و عنصر دومش یک رفرنس به عنصر متناظر با آن اندیس.
تابع ما الان میتواند اندیس اولین کاراکتر space را پیدا کند و بنابراین، میتوانیم از آن برای
هدف اصلیمان که پیدا کردن اولین کلمه در رشته بود، استفاده کنیم. اما این تابع
مشکلاتی دارد که میتواند باعث ایجاد باگ شود. برای مثال، اگر مقدار رشته تغییر کند،
خروجی تابع اشتباه خواهد بود. روش درست برای حل این مسئله، استفاده از اسلایسهاست.
اسلایسهای رشتهای
یک اسلایس رشتهای (string slice) رفرنسی است به بخشی از یک رشته و با استفاده
از سینتکسی که در کد زیر میبینید، ایجاد میشود.
RUST
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
درون براکتها از سینتکس بازه (range) در Rust استفاده شده که دارای
فرم کلی [start..end] است که در آن، start اندیس ابتدایی و end
اندیس اتهایی در رشته است.
البته عنصر یا کاراکتر دارای اندیس end در اسلایس نخواهد بود و به عبارت دیگر، آخرین اندیس در اسلایس
end - 1 است.
از نظر داخلی، ساختار دادهی اسلایس به گونهای است که نقطهی شروع و طول اسلایس را ذخیره میکند.
طول اسلایس از رابطهی end - start به دست میآید.
برای مثال، اسلایسی که در کد بالا در متغیر world ذخیره شده، از اندیس 6 در رشتهی s شروع شده و
دارای طول 5 است.
سینتکس range در Rust به گونهای است که اگر بخواهیم از اندیس صفر شروع کنیم، میتوانیم start را حذف
کنیم.
یعنی دو گزارهی زیر همارز هستند.
RUST
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
به همین ترتیب، اگر بخواهیم تا پایان رشته ادامه دهیم، میتوانیم end را ننویسیم.
یعنی دو گزارهی پایانی در کد زیر، همارز هستند.
RUST
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
همچنین، میتوانیم با صرفنظر کردن از هر دوی start و end، یک اسلایس به
کل زشته ایجاد کنیم. یعنی دو گزارهی پایانی در کد زیر، معادل هستند.
RUST
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
هر کاراکتر ASCII معادل یک بایت است و بنابراین، تا زمانی که مثل مثالهای بالا، با
رشتههایی سر و کار داریم که تنها از کاراکترهای ASCII تشکیل شدهاند، با یک سری از مشکلات
که به کاراکترهای چند بایتی (multibyte characters) مربوطاند، مواجه نخواهیم شد.
فعلاً برای درگیر نشدن با این جزئیات، فقط از کاراکترهای ASCII در رشتهها استفاده کرده و میکنیم
تا فصل هشتم که جزئیات این موضوع بیان خواهد شد.
حالا میتوانیم تابع first_word() را طوری تعریف کنیم که یک اسلایس برگرداند.
نوع مربوط به اسلایسهای رشتهای &str نام دارد.
RUST
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
به این ترتیب، تابع first_word() همان کاری را انجام میدهد که میخواستیم.
یعنی اولین کلمهی رشتهی ورودی را برمیگرداند. در کد زیر، این تابع را آزمایش کردهایم.
src/main.rs
fn main() {
let s1 = String::from("Hello world");
println!("{}", first_word(&s1));
let s2 = String::from("HowAreYouToday?");
println!("{}", first_word(&s2));
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
لیترالهای رشتهای به عنوان اسلایس
حتماً یادتان هست که گفتیم زشتههای لیترال، یعنی رشتههایی که مستقیماً در برنامه وارد میشوند،
دارای نوع &str هستند. پس، رشتههای لیترال در واقع، اسلایس هستند.
نوع متغیر s در اینجا &str است که میدانیم نوع مربوط به اسلایسهای رشتهای است.
در حقیقت، یک لیترال رشتهای اسلایسی است که به بخش مشخصی از باینری اشاره میکند.
اسلایسهای رشتهای به عنوان پارامتر
اجازه دهید یک بار دیگر به تابع first_word() برگردیم و یک ویرایش مفید روی آن
انجام دهیم.
ویرایش مورد نظرمان این است که نوع پارامتر تابع را از &String به &str تغییر دهیم.
یعنی امضای تابع به صورت زیر باشد.
fn first_word(s: &str) -> &str {}
اما چرا این تغییر مفید است؟
این تابع میتواند فرمهای مختلفی از رشتهها را به عنوان پارامتر دریافت کند.
اگر یک رشتهی لیترال داشته باشیم، میتوانیم آن را مستقیماً به تابع پاس کنیم.
اگر یک String داشته باشیم، میتوانیم یک اسلایس از آن یا یک رفرنس از آن را به تابع پاس
کنیم.
اینکه چرا ما میتوانیم یک &String را به تابعی پاس کنیم که نوع پارامترش &str است، به خاطر یک
ویژگی به نام deref coercions است که در فصل پانزدهم معرفی خواهد شد. اما فعلاً فقط بدانید که چنین
انعطافی وجود دارد.
src/main.rs
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
let word = first_word(&my_string);
let my_string_literal = "hello world";
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
let word = first_word(my_string_literal);
}
تمام فراخوانیهای تابع first_word() در اینجا معتبر هستند و این به خاطر تغییر
نوع پارامتر تابع به
&str است.
سایر اسلایسها
رشتهها تنها مقادیری نیستند که میتوان اسلایسهایی از آنها را ایجاد کرد.
انواع دیگر کالکشنها هم این قابلیت را دارند. برای مثال، در کد زیر، اسلایسی از یک آرایه با نام a
در متغیر با نام slice ذخیره شده است.
src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}
متغیر slice دارای نوع &[i32] است.
در گزارهی آخر از یک ماکرو با نام assert_eq! برای مقایسهی دو مقدار
استفاده شده است. این ماکرو در صورت یکسان بودن مقادیر پارامترهایش، هیچ کاری انجام نمیدهد اما
در غیر این صورت، یک خطا تولید میکند.