مقدمه

در بسیاری از سناریوهای برنامه‌نویسی، هدف اصلی ما از تعریف یک نوع، صرفاً نگهداری داده است. برای مثال، یک نوع برای نگهداری اطلاعات یک کاربر، یک محصول یا یک نقطه در فضای سه‌بعدی. تا قبل از C# 9، برای این کار معمولاً از class یا struct استفاده می‌کردیم که اغلب منجر به نوشتن کدهای تکراری زیادی برای سازنده‌ها، پراپرتی‌ها و متدهایی مانند Equals و ToString می‌شد. C# 9 با معرفی رکوردها (Records)، یک راهکار مدرن، مختصر و قدرتمند برای تعریف انواعی که ماهیت داده‌محور دارند، ارائه داد. رکوردها به طور پیش‌فرض تغییرناپذیر (immutable) هستند و کامپایلر بسیاری از متدهای مفید را به صورت خودکار برای آن‌ها تولید می‌کند.

رکورد (Record) چیست؟

یک رکورد، نوع خاصی از class یا struct است که برای ساده‌سازی کار با مدل‌های داده طراحی شده. هدف اصلی رکوردها کپسوله کردن داده است. وقتی یک نوع را به عنوان رکورد تعریف می‌کنید، کامپایلر به طور خودکار قابلیت‌های زیر را برای شما فراهم می‌کند:

  • سینتکس بسیار مختصر برای تعریف نوع و پراپرتی‌های آن.
  • تغییرناپذیری به صورت پیش‌فرض.
  • مقایسه‌ی مبتنی بر مقدار (Value-based equality) به جای مقایسه‌ی مبتنی بر ارجاع.
  • یک پیاده‌سازی مفید و خوانا برای متد ToString().
  • پشتیبانی از "تغییر غیرمخرب" با استفاده از عبارت with.

رکوردهای جایگاهی (Positional Records)

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

Copy Icon Program.cs
// A record defined in a single line!
public record Person(string FirstName, string LastName);

// --- Usage ---
Person person1 = new Person("John", "Doe");

// The compiler provides a useful ToString() implementation automatically.
Console.WriteLine(person1);

خروجی این کد به شکل زیر خواهد بود:

Person { FirstName = John, LastName = Doe }

این یک خط کد public record Person(...) کارهای بسیار زیادی را در پشت صحنه انجام می‌دهد:

  • یک کلاس Person ایجاد می‌کند.
  • یک سازنده‌ی عمومی تعریف می‌کند که FirstName و LastName را به عنوان پارامتر می‌پذیرد.
  • دو پراپرتی عمومی و init-only به نام‌های FirstName و LastName ایجاد می‌کند. (یعنی پس از ساخت شیء، قابل تغییر نیستند).
  • یک متد Deconstruct برای تجزیه‌ی رکورد ایجاد می‌کند.
  • متدهای Equals، GetHashCode و ToString را به صورت بهینه پیاده‌سازی می‌کند.

ویژگی‌های تولید شده توسط کامپایلر

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

برابری مبتنی بر مقدار (Value-Based Equality)

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

Copy Icon Program.cs
public record Person(string FirstName, string LastName);

Person person1 = new("Jane", "Doe");
Person person2 = new("Jane", "Doe");

// For records, this compares the values of the properties.
Console.WriteLine($"Are they equal? {person1 == person2}"); // Output: True

// For regular classes, this would be False because they are different objects.

این رفتار برای کار با اشیاء داده‌محور (Data Transfer Objects - DTOs) و در سناریوهایی مانند تست‌نویسی بسیار مفید است، زیرا دیگر نیازی به پیاده‌سازی دستی متد Equals نداریم.

تغییر غیرمخرب با عبارت with

از آنجایی که رکوردها به طور پیش‌فرض تغییرناپذیر هستند، شما نمی‌توانید پس از ایجاد یک شیء، پراپرتی‌های آن را تغییر دهید. اما اگر نیاز داشته باشید یک نسخه‌ی جدید از شیء با چند تغییر جزئی بسازید چه؟ برای این کار از عبارت with استفاده می‌کنیم. این عبارت یک کپی از رکورد اصلی ایجاد کرده و به شما اجازه می‌دهد مقادیر پراپرتی‌های مورد نظرتان را در نسخه‌ی جدید تغییر دهید.

Copy Icon Program.cs
public record Book(string Title, string Author);

Book originalBook = new("Pride and Prejudice", "Jane Austen");

// Create a copy with a different title. The original object is unchanged.
Book updatedBook = originalBook with { Title = "Sense and Sensibility" };

Console.WriteLine(originalBook);
Console.WriteLine(updatedBook);

خروجی این کد نشان می‌دهد که شیء اصلی دست‌نخورده باقی مانده است:

Book { Title = Pride and Prejudice, Author = Jane Austen }
Book { Title = Sense and Sensibility, Author = Jane Austen }

این ویژگی که به آن تغییر غیرمخرب (non-destructive mutation) گفته می‌شود، یکی از اصول کلیدی در برنامه‌نویسی تابعی و کار با داده‌های تغییرناپذیر است.

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

انتخاب بین رکورد و کلاس به هدف شما بستگی دارد.

  • از رکورد (record) استفاده کنید: زمانی که تمرکز اصلی نوع شما بر روی داده‌هایی است که حمل می‌کند و این داده‌ها قرار نیست پس از ایجاد تغییر کنند. رکوردها برای DTO ها، پیام‌ها، و مدل‌های داده‌ای که در سراسر برنامه منتقل می‌شوند، گزینه‌ی ایده‌آلی هستند. ویژگی‌هایی مانند برابری مبتنی بر مقدار و تغییر غیرمخرب، کار با این سناریوها را بسیار ساده می‌کنند.
  • از کلاس (class) استفاده کنید: زمانی که نوع شما یک هویت و رفتار مشخص دارد و وضعیت داخلی آن در طول عمرش تغییر می‌کند. کلاس‌ها برای مدل‌سازی موجودیت‌هایی با عمر طولانی مانند سرویس‌ها، کنترلرها، پنجره‌های UI، یا هر شیء دیگری که وضعیت داخلی قابل تغییر دارد، مناسب‌تر هستند.