مقدمه

در برنامه‌نویسی، بارها پیش می‌آید که نیاز داریم چند مقدار مرتبط را به صورت موقت با هم گروه‌بندی کنیم. یک نمونه‌ی کلاسیک، بازگرداندن چند مقدار از یک متد است. تا قبل از نسخه‌ی 7.0 زبان C#، برای این کار مجبور بودیم از راهکارهایی مانند پارامترهای out استفاده کنیم یا یک class یا struct جدید فقط برای این منظور تعریف کنیم که هر دو روش می‌توانند باعث پیچیدگی و افزایش حجم کد شوند. C# 7.0 با معرفی تاپل‌ها (Tuples) یک راهکار بسیار سبک، خوانا و کارآمد برای این مشکل ارائه داد. تاپل به شما اجازه می‌دهد یک ساختار داده‌ی سبک و بدون نیاز به تعریف نوع جداگانه، ایجاد کنید.

تاپل (Tuple) چیست؟

تاپل یک ساختار داده است که دنباله‌ای از عناصر با تعداد ثابت را در خود نگه می‌دارد. هر عنصر در تاپل می‌تواند نوع داده‌ی متفاوتی داشته باشد. برای مثال، شما می‌توانید یک تاپل برای نگهداری نام یک شخص (رشته) و سن او (عدد صحیح) ایجاد کنید. تاپل‌ها در C# از نوع مقداری (Value Type) هستند، دقیقاً مانند structها، که این به معنی کارایی بالای آن‌ها در مدیریت حافظه است.

ایجاد و استفاده از تاپل‌ها

ایجاد یک تاپل در C# بسیار ساده است. کافی است مقادیر مورد نظر را داخل یک جفت پرانتز قرار داده و با کاما از هم جدا کنید.

Copy Icon Program.cs
// Creating a simple tuple
var person = ("John Doe", 34);

// Accessing elements using default names: Item1, Item2, ...
Console.WriteLine($"Name: {person.Item1}");
Console.WriteLine($"Age: {person.Item2}");

در کد بالا، ما یک تاپل با دو عنصر ایجاد کرده‌ایم. کامپایلر به طور خودکار نوع متغیر person را (string, int) تشخیص می‌دهد. برای دسترسی به عناصر این تاپل، از نام‌های پیش‌فرض Item1، Item2 و به همین ترتیب استفاده می‌کنیم. هرچند این روش کار می‌کند، اما نام‌هایی مانند Item1 توصیفی نیستند و خوانایی کد را کاهش می‌دهند.

تاپل‌های نام‌گذاری شده (Named Tuples)

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

Copy Icon Program.cs
// Creating a named tuple
var namedPerson = (Name: "Jane Doe", Age: 29);

// Accessing elements using their given names
Console.WriteLine($"Name: {namedPerson.Name}");
Console.WriteLine($"Age: {namedPerson.Age}");

// You can still access them by Item1, Item2 if you want
Console.WriteLine($"Item1 is still: {namedPerson.Item1}");

همانطور که می‌بینید، با نام‌گذاری عناصر، کد ما بسیار خواناتر و قابل فهم‌تر شد. اکنون به جای Item1 از Name استفاده می‌کنیم که دقیقاً مشخص می‌کند این عنصر چیست. این نام‌ها فقط در زمان کامپایل وجود دارند و هیچ سربار اضافی در زمان اجرا به برنامه تحمیل نمی‌کنند.

کاربرد اصلی تاپل‌ها: بازگرداندن چند مقدار از متد

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

Copy Icon Program.cs
static (int sum, int count) Tally(int[] numbers)
{
    var result = (s: 0, c: 0); // Create a local named tuple
    foreach (var number in numbers)
    {
        result.s += number;
        result.c++;
    }
    return result;
}

int[] myNumbers = { 5, 10, 15, 20 };
var tallyResult = Tally(myNumbers);

Console.WriteLine($"Sum: {tallyResult.sum}, Count: {tallyResult.count}"); // Output: Sum: 50, Count: 4

در این مثال، متد Tally مجموع و تعداد اعداد یک آرایه را محاسبه کرده و هر دو مقدار را در قالب یک تاپل نام‌گذاری شده (int sum, int count) برمی‌گرداند. کد فراخواننده به سادگی این تاپل را دریافت کرده و به عناصر آن از طریق نام‌هایشان دسترسی پیدا می‌کند. این روش بسیار تمیزتر از استفاده از پارامترهای out است.

تجزیه کردن (Deconstruction) تاپل‌ها

یکی دیگر از ویژگی‌های قدرتمند مرتبط با تاپل‌ها، قابلیت تجزیه (Deconstruction) است. تجزیه به شما این امکان را می‌دهد که عناصر یک تاپل را به صورت مستقیم در متغیرهای جداگانه استخراج کنید.

Copy Icon Program.cs
// Deconstructing the tuple returned from a method into new variables
(int finalSum, int finalCount) = Tally(myNumbers);
Console.WriteLine($"The final sum is {finalSum}");

// Deconstructing a tuple literal
(string city, string country) = ("Tehran", "Iran");
Console.WriteLine($"City: {city}");

// If you don't need an element, you can use a discard (_)
(string name, _) = ("John Doe", 34); // We only care about the name
Console.WriteLine($"Name is {name}");

در بخش اول کد بالا، خروجی متد Tally مستقیماً در دو متغیر جدید finalSum و finalCount ریخته می‌شود. این سینتکس بسیار گویا و کارآمد است. همچنین اگر به یک یا چند عنصر از تاپل نیازی نداشته باشید، می‌توانید از علامت `_` (که به آن discard گفته می‌شود) برای نادیده گرفتن آن‌ها استفاده کنید.

چه زمانی از تاپل به جای Struct یا Class استفاده کنیم؟

با وجود شباهت‌ها، تاپل‌ها جایگزین struct و class نیستند. آن‌ها برای سناریوهای خاصی طراحی شده‌اند.

  • از تاپل استفاده کنید وقتی: به یک ساختار داده‌ی سبک و موقتی نیاز دارید، به خصوص برای بازگرداندن چند مقدار از یک متد. تاپل‌ها برای داده‌هایی که خارج از محدوده‌ی یک متد اهمیت زیادی ندارند، ایده‌آل هستند.
  • از Struct یا Class استفاده کنید وقتی: داده‌های شما هویت و معنای مشخصی در کل برنامه دارند. اگر ساختار داده‌ی شما نیاز به متدها، منطق داخلی یا رفتارهای پیچیده دارد، یا اگر قرار است به عنوان بخشی از یک API عمومی استفاده شود، تعریف یک struct یا class اختصاصی انتخاب صحیح‌تری است.

به طور خلاصه، تاپل‌ها ابزاری برای "گروه‌بندی موقت" هستند، در حالی که کلاس‌ها و ساختارها ابزارهایی برای "مدل‌سازی دائمی" مفاهیم برنامه شما هستند.