مقدمه

در زبان Rust، کار با متن و رشته‌ها مفهومی عمیق‌تر از بسیاری از زبان‌های دیگر دارد. همانطور که در فصل چهارم دیدیم، Rust دو نوع اصلی رشته دارد: String و &str (که یک «اسلایس رشته‌ای» یا string slice است). نوع String که در این درس بر روی آن تمرکز می‌کنیم، یک نوع داده‌ی مالک (owned)، قابل رشد و تغییرپذیر است که در heap ذخیره می‌شود. این نوع داده به صورت یک پوشش (wrapper) روی یک وکتور از بایت‌ها (Vec<u8>) پیاده‌سازی شده و تضمین می‌کند که محتوای آن همیشه یک دنباله معتبر از بایت‌های UTF-8 است.

ایجاد و به‌روزرسانی یک String

روش‌های مختلفی برای ساخت یک String جدید وجود دارد. می‌توانیم با String::new() یک رشته خالی بسازیم یا با استفاده از متد .to_string() روی یک لیترال رشته‌ای (&str) یا با تابع String::from()، یک String با مقدار اولیه ایجاد کنیم.

الحاق به String

یک String می‌تواند با استفاده از متد push_str() (برای الحاق یک اسلایس رشته‌ای) یا متد push() (برای الحاق یک کاراکتر) رشد کند.

الحاق با عملگر `+` و ماکروی format!

همچنین می‌توان از عملگر `+` برای الحاق دو رشته استفاده کرد. اما این عملگر به دلیل قوانین مالکیت، رفتار خاصی دارد. عبارت s1 + &s2 مالکیت s1 را به خود منتقل می‌کند (آن را move می‌کند)، به آن محتوای s2 را اضافه کرده و مالکیت نتیجه را برمی‌گرداند. این یعنی پس از این عملیات، دیگر نمی‌توان از s1 استفاده کرد.

یک روش بهتر و خواناتر برای ترکیب چند رشته، استفاده از ماکروی format! است. این ماکرو مالکیت هیچ‌کدام از آرگومان‌های خود را نمی‌گیرد و برای ساخت رشته‌های پیچیده بسیار کارآمدتر است.

Copy Icon src/main.rs
fn main() {
    let mut s1 = String::from("foo");
    let s2 = "bar";
    s1.push_str(s2);
    println!("s1 is {}, s2 is {}", s1, s2); // s2 is still valid

    let s3 = String::from("Hello, ");
    let s4 = String::from("world!");
    let s5 = s3 + &s4; // note s3 has been moved here and can no longer be used
    // println!("{}", s3); // This would cause a compile error

    let t1 = String::from("tic");
    let t2 = String::from("tac");
    let t3 = String::from("toe");
    let t = format!("{}-{}-{}", t1, t2, t3); // All variables remain valid
    println!("{}", t);
}

همانطور که می‌بینید، متد push_str() مالکیت پارامتر خود را نمی‌گیرد، اما عملگر `+` مالکیت عملوند اول را به خود منتقل می‌کند. ماکروی format! بهترین گزینه برای ترکیب چندین متغیر رشته‌ای است.

چالش ایندکس‌گذاری در رشته‌های UTF-8

در بسیاری از زبان‌های برنامه‌نویسی، شما می‌توانید با استفاده از یک ایندکس عددی به کاراکترهای یک رشته دسترسی پیدا کنید (مانند s[0]). اما این کار در Rust مجاز نیست و تلاش برای انجام آن با خطای کامپایلر مواجه می‌شود. چرا؟

پاسخ در نحوه نمایش داخلی String نهفته است. یک String در Rust یک پوشش روی Vec<u8> (وکتوری از بایت‌ها) است و از انکودینگ UTF-8 استفاده می‌کند. در UTF-8، یک کاراکتر قابل مشاهده ممکن است از یک، دو، سه یا حتی چهار بایت تشکیل شده باشد. برای مثال، کاراکتر a یک بایت فضا اشغال می‌کند، اما کاراکتر é دو بایت، و یک اموجی ممکن است چهار بایت فضا بگیرد.

اگر Rust اجازه ایندکس‌گذاری بایتی را می‌داد، عملیاتی مانند s[0] چه چیزی باید برمی‌گرداند؟ یک بایت؟ یا یک کاراکتر؟ اگر یک بایت را برگرداند، ممکن است آن بایت به تنهایی یک کاراکتر معتبر نباشد (مثلاً نصف یک کاراکتر فارسی). از آنجایی که Rust ایمنی و صحت را در اولویت قرار می‌دهد، برای جلوگیری از این ابهام و خطاهای بالقوه، به طور کلی ایندکس‌گذاری مستقیم روی String را ممنوع کرده است.

دسترسی به بخش‌هایی از یک رشته

با وجود عدم امکان ایندکس‌گذاری، روش‌های امن و واضحی برای دسترسی به محتوای رشته‌ها وجود دارد.

اسلایس کردن رشته‌ها

شما می‌توانید با استفاده از سینتکس بازه، یک اسلایس رشته‌ای (&str) از یک String ایجاد کنید. اما این کار با یک شرط مهم همراه است: مرزهای بازه شما باید دقیقاً روی مرزهای کاراکترهای UTF-8 معتبر قرار بگیرند. اگر سعی کنید یک کاراکتر چند-بایتی را از وسط نصف کنید، برنامه شما panic خواهد کرد.

پیمایش روی رشته

بهترین و امن‌ترین روش برای پردازش کاراکتر به کاراکتر یک رشته، استفاده از متدهایی است که روی آن پیمایش (iterate) می‌کنند.

Copy Icon src/main.rs
fn main() {
    let hello = "Здравствуйте"; // A string in Cyrillic alphabet

    // Iterate over Unicode scalar values (the closest thing to "characters")
    println!("Chars:");
    for c in hello.chars() {
        print!("{} ", c);
    }
    println!("");

    // Iterate over raw bytes
    println!("Bytes:");
    for b in hello.bytes() {
        print!("{} ", b);
    }
    println!("");
}

متد .chars() یک تکرارگر بر روی مقادیر اسکالر یونیکد (که نزدیک‌ترین مفهوم به «کاراکتر» است) برمی‌گرداند و به درستی مرزهای کاراکترهای چند-بایتی را تشخیص می‌دهد. متد .bytes() نیز یک تکرارگر بر روی بایت‌های خام تشکیل‌دهنده رشته برمی‌گرداند. استفاده از این متدها، روش اصولی و امن برای کار با محتوای رشته‌ها در Rust است.

در این درس با جزئیات نوع String و چالش‌ها و روش‌های کار با متن‌های UTF-8 در Rust آشنا شدیم. دیدیم که چگونه می‌توان رشته‌ها را ساخت و ویرایش کرد و چرا پیمایش روی کاراکترها به جای ایندکس‌گذاری، رویکرد صحیح و امن است. در درس بعدی، به سراغ آخرین کالکشن اصلی در کتابخانه استاندارد، یعنی HashMap، خواهیم رفت و یاد می‌گیریم که چگونه داده‌ها را در یک ساختار کلید-مقدار ذخیره و بازیابی کنیم.