مقدمه

struct یا structure یک نوع داده‌ی سفارشی (custom datatype) است که به ما امکان می‌دهد که مقادیر مرتبط با هم را در قالب یک ساختار، پکیج کنیم، به نحوی که این مقادیر یک گروه یا ساختار بامعنا را شکل دهند. قبلاً دیدیم که تاپل‌ها هم می‌توانند این کار را انجام دهند اما تفاوت بین tuple و struct را در این فصل درک خواهیم کرد و خواهیم دید که هر یک از اینها در چه شرایطی نسبت به دیگری بهتر عمل می‌کنند. علاوه بر struct، یک نوع سفارشی دیگر هم در Rust داریم که enum نام دارد و در فصل بعد در مورد آن صحبت می‌کنیم.

تعریف و نمونه‌سازی از struct

یادآوری می‌کنم که تاپل‌ها می‌توانند مقادیر مرتبط با هم را نگه دارند و این مقادیر می‌توانند از نوع‌های مختلفی باشند. struct هم این قابیت‌ها را دارد اما علاوه بر اینها، نام‌هایی را هم به مقادیرش اختصاص می‌دهد و به این ترتیب، به آنها معنا می‌دهد. پس، struct نسبت به tuple انعطاف‌پذیرتر است، چون برای تعیین مقادیر یا دسترسی به آنها نیازی به پایبند بودن به ترتیب آنها نیست. به هر آیتم یا مقدار می‌توانیم با استفاده از نامش دسترسی داشته باشیم.

برای تعریف یک ساختار از کلمه کلیدی struct استفاده می‌شود و بلافاصله بعد از آن، نام ساختار کلی که قصد ایجاد آن را داریم، آورده می‌شود. سپس، یک بلاک برای این ساختار ایجاد می‌کنیم و فیلدهای مربوط به ساختار را تعریف می‌کنیم. هر فیلد (field) شامل اعلام نام و نوع یک مقدار است که با استفاده از سینتکس name: type ایجاد می‌شود. فیلدها را با کاما از هم جدا می‌کنیم. برای مثال، فرض کنید قصد داریم ساختاری تعریف کنیم که اطلاعات مربوط به حساب کاربری را نگه دارد.

Copy Icon src/main.rs
fn main() {}

struct User {
  active: bool,
  username: String,
  email: String,
  sign_in_count: u64,
}

بعد از تعریف یک ساختار، برای استفاده از آن، با فراهم کردن مقدار برای هر یک از فیلدها، یک نمونه (instance) از آن ساختار ایجاد می‌کنیم. نمونه‌سازی از ساختار به این صورت است که ابتدا نام ساختار و سپس یک بلاک برای مقداردهی به فیلدها ایجاد می‌کنیم. یعنی فیلدهایی را که با استفاده از سینتکس name: type ایجاد کردیم، با استفاده از سینتکس name: value مقداردهی می‌کنیم و به این ترتیب، یک نمونه از ساختار بدست می‌آید. نیازی هم نیست که فیلدها به همان ترتیبی که تعریف شده‌اند، مقداردهی شوند. بنابراین، می‌توان گفت که تعریف یک ساختار حکم یک تمپلت کلی را برای یک نوع دارد و نمونه‌ها با استفاده از این تمپلت، مقادیری از آن نوع را ایجاد می‌کنند. برای مثال، در اینجا با ایجاد یک نمونه از ساختار User یک یوزر خاص را ایجاد می‌کنیم.

Copy Icon src/main.rs
fn main() {
  let user1 = User {
    active: true,
    username: String::from("tom"),
    email: String::from("tom@example.com"),
    sign_in_count: 1,
  };
}
          
struct User {
  active: bool,
  username: String,
  email: String,
  sign_in_count: u64,
}

دسترسی به مقادیر فیلدهای یک ساختار با استفاده از سینتکس dot notaion امکانپذیر است. برای مثال، با استفاده از user1.email می‌توانیم به آدرس ایمیل یوزر دسترسی داشته باشیم. اگر نمونه به صورت mutable تعریف شده باشد، می‌توانیم مقدار فیلدها را هم تغییر دهیم.

Copy Icon src/main.rs
fn main() {
  let mut user1 = User {
    active: true,
    username: String::from("tom"),
    email: String::from("tom@example.com"),
    sign_in_count: 1,
  };
          
  user1.email = String::from("anotheremail@example.com");
}

دقت داشته باشید که خود ساختار باید mutable باشد و Rust به ما اجازه نمی‌دهد که یک فیلد را به صورت mutable تعریف کنیم.

یک نوع سفارشی مثل User می‌تواند مثل نوع‌های Built-in به عنوان نوع پارامترهای توابع یا به عنوان نوع بازگشتی توابع، تعیین شود. در کد زیر، تابعی تعریف شده که نوع بازگشتی آن User است. یعنی باید یک نمونه از User را به عنوان خروجی برگرداند. بدنه‌ی تابع شامل یک عبارت (expression) است و می‌دانیم که توابع، آخرین عبارت موجود در بدنه‌ی خود را به عنوان خروجی برمی‌گردانند.

Copy Icon src/main.rs
fn build_user(email: String, username: String) -> User {
  User {
    active: true,
    username: username,
    email: email,
    sign_in_count: 1,
  }
}

برای نمونه‌ای که این تابع برمی‌گرداند، مقدار فیلد active برابر با true و مقدار فیلد sign_in_count برابر با 1 خواهد بود اما فیلدهای username و email از روی ورودی‌های تابع مشخص می‌شوند. در مواردی مثل این که نام فیلدها و پارامترها یکی هستند، می توانیم با تکیه بر سینتکسی به نام field init shorthand تابع را به شکل مختصرترِ زیر بنویسیم.

Copy Icon src/main.rs
fn build_user(email: String, username: String) -> User {
  User {
    active: true,
    username,
    email,
    sign_in_count: 1,
  }
}

ساخت نمونه با سینتکس Struct Update

گاهی اوقات می‌خواهیم نمونه‌ای از یک ساختار ایجاد کنیم که اکثر مقادیرش در یک نمونه‌ی دیگر از آن ساختار، وجود دارد. در این موارد، می‌توانیم به جای اینکه نمونه‌ی دوم را از صفر ایجاد کنیم، از سینتکس Stuct Update استفاده کنیم. بدون استفاده از سینتکس struct update می‌توانیم به شکل زیر یک نمونه بسازیم که برخی مقادیرش را از یک نمونه‌ی موجود، دریافت می‌کند.

Copy Icon src/main.rs
fn main() {
  let user1 = User {
    active: true,
    username: String::from("tom"),
    email: String::from("tom@example.com"),
    sign_in_count: 1,
  };
          
  let user2 = User {
    active: user1.active,
    username: user1.username,
    email: String::from("another@example.com"),
    sign_in_count: user1.sign_in_count,
  };
}

اما با استفاده از سینتکس struct update می‌توانیم نتیجه‌ی یکسانی را با کد کمتر بدست آوریم. سینتکس .. به این معناست که فیلدهایی که صراحتاً مقداردهی نشده‌اند، باید مقدار خود را از نمونه‌ی مورد نظر (در اینجا user1) دریافت کنند.

Copy Icon src/main.rs
fn main() {
  let user1 = User {
    active: true,
    username: String::from("tom"),
    email: String::from("tom@example.com"),
    sign_in_count: 1,
  };
          
  let user2 = User {
    email: String::from("another@example.com"),
    ..user1
  };
}

دقت داشته باشید که در این مثال، بعد از ایجاد user2 ما دیگر به user1 دسترسی نخواهیم داشت. چون مقدار String مربوط به فیلد username در user1 به user2 منتقل (move) شده است. سیستم مالکیت را که فراموش نکره‌اید! سینتکس struct update مثل عمل تخصیص (assignment) کار می‌کند و بنابراین، مقادیر نوع‌هایی مانند String را نه کپی، بلکه move می‌کند. توجه داشته باشید که این مسئله در مورد فیلدهای active و sign_in_count صادق نیست. چون اینها دارای نوع‌های i32 و u64 هستند که Stack-only هستند و مقادیرشان در هنگام تخصیص، کپی می‌شوند.

کاربرد Tuple Struct

Rust نوع دیگری از ساختارها هم دارد که به خاطر شباهتی که به تاپل‌ها دارند، Tuple Struct نامیده می‌شوند. این ساختارها مثل ساختارهای معمولی، دارای نام هستند و مثل تاپل‌ها مقادیرشان نامی ندارند و صرفاً نوع آنهاست که هنگام تعریف، مشخص می‌شود. پس، اینها در واقع، چیزی هستند بین تاپل و ساختار و به همین دلیل است که Tuple Struct نامیده می‌شوند. یک tuple struct وقتی به کار می‌آید که می‌خواهیم به یک تاپل نامی بدهیم تا یک نوع متفاوت تلقی شود اما از طرفی هم اینکه یک ساختار معمولی تعریف کنیم، کمی زیاده‌کاری است؛ چون نیاز نداریم که فیلدها نامی داشته باشد. مثال زیر، قضیه را روشن می‌کند.

Copy Icon src/main.rs
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
            
fn main() {
  let black = Color(0, 0, 0);
  let origin = Point(0, 0, 0);
}

کد بالا اولاً روش تعریف tuple struct را نشان می‌دهد. کلمه کلیدی struct و نام و سپس، نوع پارامترها درون پرانتز دیده می‌شوند. اما علاوه بر آن، این کد نشان می‌دهد که چرا این نوع ساختارها مفید هستند. در اینجا Color و Point هر دو ساختارهایی هستند که سه مقدار از نوع i32 را نگه می‌دارند. استفاده از تاپل برای تعریف اینها کار درستی نیست، چون نوع یکسانی به آنها می‌دهد در حالی که Color و Point با هم تفاوت معنایی دارند و باید نوع‌های مجزایی باشند. اما استفاده از ساختارهای معمولی هم گزینه‌ی مناسبی نیست چون با اضافه‌کاری همراه است. موجودیت‌های Color و Point برای مقادیرشان نیازی به نام ندارند. در مورد Color مشخص است که مقدار اول به کانال رنگی R، مقدار دوم به کانال رنگی G و مقدار سوم به کانال رنگی B اختصاص دارد. پس، لزومی ندارد که ما سه فیلد مثلاً‌ با نام‌های red، green و blue تعریف کنیم. همین داستان در مورد موجودیت Point نیز وجود دارد. یعنی مقدار اول به مختص X، مقدار دوم به مختص Y و مقدار سوم به مختص Z تعلق دارد. در اینگونه موارد، یک tuple struct گزینه‌ی مناسب محسوب می‌شود. الان Color و Point هم نوع‌های مجزایی هستند و هم اضافه‌کاری ندارند.

ساختارهای Unit-Like

امکان تعریف ساختارهای بدون فیلد هم وجود دارد. این ساختارها به خاطر شباهتشان به تاپل خالی که unit نامیده می‌شود و با () نمایش داده می‌شود، ساختارهای unit-like گفته می‌شوند. این ساختارها زمانی مفید واقع می‌شوند که بخواهیم یک trait را روی یک نوع پیاده‌سازی کنیم اما دیتایی برای ذخیره در خود نوع نداشته باشیم. در مورد مفهوم trait در فصل دهم صحبت خواهیم کرد اما نحوه‌ی تعریف یک ساختار unit-like به صورت زیر است.

Copy Icon src/main.rs
struct AlwaysEqual;

fn main() {
  let subject = AlwaysEqual;
}

همانطور که می‌بینید، چه برای تعریف و چه برای نمونه‌سازی، نیازی به پرانتز یا آکلاد نیست. فرض کنید بعداً بخواهیم رفتاری را برای این نوع پیاده‌سازی کنیم که باعث شود هر نمونه از AlwaysEqual همواره با هر نمونه از هر نوع دیگر برابر باشد. این کار را می‌توانیم بدون نیاز به هیچ داده‌ای انجام دهیم. برای درک این موضوع باید تا فصل دهم صبر کنید.

مالکیت داده‌های struct

در تعریف ساختار User برای فیلدهای رشته‌ای از نوع String استفاده کردیم نه &str. علت این کار روشن است: طبیعتاً ما می‌خواهیم هر نمونه از این ساختار، مالک داده‌های خودش باشد و این داده‌ها تا زمانی که خود ساختار معتبر است، معتبر باشند. البته ساختارها می‌توانند رفرنس‌هایی به داده‌هایی که مالکشان دیگری است، داشته باشند اما این کار مستلزم استفاده از lifetime است؛ یک ویژگی کلیدی که در فصل دهم به آن خواهیم پرداخت. اما اگر بدون استفاده از lifetime، از نوعی مثل &str برای فیلدهای یک ساختار استفاده کنیم، با خطا مواجه خواهیم شد.