اندازهی قلم متن
تخمین مدت زمان مطالعهی مطلب:
ده دقیقه
روشهای زیادی برای بازگشت چندین مقدار از یک متد وجود دارند؛ مانند استفادهی از آرایهها برای بازگشت اشیایی از یک جنس، ایجاد یک کلاس سفارشی با خواص متفاوت و استفاده از پارامترهای out و ref همانند روشهای متداول در C و ++C. در این بین روش دیگری نیز به نام Tuples از زمان NET 4.0. برای بازگشت چندین شیء با نوعهای مختلف، ارائه شدهاست که در C# 7 نحوهی تعریف و استفادهی از آنها بهبود قابل ملاحظهای یافتهاست.
Tuple چیست؟
هدف از کار با Tupleها، عدم تعریف یک کلاس جدید به همراه خواص آن، جهت بازگشت بیش از یک مقدار از یک متد، توسط وهلهای از این کلاس جدید میباشد. برای مثال اگر بخواهیم از متدی، دو مقدار شهر و ناحیه را بازگشت دهیم، یک روش آن، ایجاد کلاس مکان زیر است:
و سپس، وهله سازی و بازگشت آن:
اما توسط Tuples، بدون نیاز به تعریف یک کلاس جدید، باز هم میتوان به همین دو خروجی، دسترسی یافت:
مشکلات نوع Tuple در نگارشهای قبلی دات نت
هرچند Tuples از زمان دات نت 4 در دسترس هستند، اما دارای این کمبودها و مشکلات میباشند:
الف) پارامترهای خروجی آنها ثابت و با نامهایی مانند Item1، Item2 و امثال آن هستند که در حین استفاده، به علت ضعف نامگذاری، کاربرد آنها دقیقا مشخص نیست و کاملا بیمعنا هستند:
ب) Reference Type هستند (کلاس هستند) و در زمان وهله سازی، میزان مصرف حافظهی بیشتری را نسبت به Value Types (معادل Tuples در C# 7) دارند.
ج) Tuples در دات نت 4، صرفا یک کتابخانهی اضافه شدهی به فریم ورک بوده و زبانهای دات نتی، پشتیبانی توکاری را از آنها جهت بهبود و یا ساده سازی تعریف آنها، ارائه نمیدهند.
ایجاد Tuples در C# 7
برای ایجاد Tuples در سی شارپ 7، از پرانتزها به همراه ذکر نام و نوع پارامترها استفاده میشود.
در مثال فوق، یک Tuple ایجاد شدهاست و در آن مقدار 3 به x1 و مقدار "one" به s1 انتساب داده شدهاند. به این عملیات deconstruction هم میگویند.
دسترسی به این مقادیر نیز همانند متغیرهای معمولی است.
اگر سعی کنیم این قطعه کد را کامپایل نمائیم، با خطای ذیل متوقف خواهیم شد:
برای رفع این مشکل نیاز است بستهی نیوگت ذیل را نیز نصب کرد:
تعاریف متغیرهای بازگشتی، خارج از پرانتزها هم میتوانند صورت گیرند:
بازگشت Tuples از متدها
متد ذیل، دو خروجی نتیجه و باقیماندهی تقسیم دو عدد صحیح را باز میگرداند:
برای این منظور، نوع خروجی متد به صورت (int, int) و همچنین مقدار بازگشتی نیز به صورت یک Tuple از نتیجه و باقیماندهی تقسیم، تعریف شدهاست.
در ادامه نحوهی استفادهی از این متد را مشاهده میکنید:
در اینجا امکان استفادهی از var نیز برای تعریف نوع متغیرهای دریافتی از یک Tuple نیز وجود دارد و کامپایلر به صورت خودکار نوع آنها را بر اساس نوع خروجی tuple مشخص میکند:
و یا حتی چون نوع var پارامترها در اینجا یکی است و در هر دو حالت به int اشاره میکند، میتوان این var را در خارج از پرانتز هم قرار داد:
و یا برای نمونه متد GetHumanData دات نت 4 ابتدای بحث را به صورت ذیل میتوان در C# 7 بازنویسی کرد:
و سپس به نحو واضحتری از آن استفاده نمود؛ بدون استفادهی اجباری از Item1 و غیره (هرچند هنوز هم میتوان از آنها استفاده کرد):
پشت صحنهی Tuples در C# 7
همانطور که عنوان شد، برای اینکه بتوانید قطعه کدهای فوق را کامپایل کنید، نیاز به بستهی نیوگت System.ValueTuple است. در حقیقت کامپایلر خروجی متد فوق را به نحو ذیل تفسیر میکند:
برای مثال قطعه کد
توسط کامپایلر به قطعه کد ذیل ترجمه میشود:
- برخلاف نگارشهای پیشین دات نت که Tuples در آنها reference type بودند، این ValueTuple یک struct است و به همین جهت سربار تخصیص حافظهی کمتری را به همراه داشته و از لحاظ کارآیی و میزان مصرف حافظه بهینهتر عمل میکند.
- همچنین در اینجا محدودیتی از لحاظ تعداد پارامترهای ذکر شدهی در یک Tuple وجود ندارد.
در اینجا هم مانند قبل (دات نت 4) 8 آیتم را میتوان تعریف کرد؛ اما چون آخرین آیتم ValueTuple تعریف شده نیز یک Tuple است، در عمل محدودیتی از نظر تعداد پارامتر نخواهیم داشت.
مفهوم Tuple Literals
همانند نگارشهای پیشین دات نت، خروجی یک Tuple را میتوان به یک متغیر از نوع var و یا ValueType نیز نسبت داد:
در این حالت برای دسترسی به مقادیر Tuple همانند قبل باید از فیلدهای Item1 و Item2 و ... استفاده کرد.
به علاوه در سی شارپ 7 میتوان برای اعضای یک Tuple نام نیز تعریف کرد که به آنها Tuple literals گویند:
در این حالت زمانیکه Tuple به یک متغیر از نوع var نسبت داده میشود، میتوان به خروجی آن بر اساس نامهای اعضای Tuple، بجای ذکر Item1 و ... دسترسی یافت که خوانایی بیشتری دارند.
و یا هنگام تعریف نوع خروجی، میتوان نام پارامترهای متناظر را نیز ذکر کرد که به آن named elements هم میگویند:
و نمونهای از کاربرد آن به صورت ذیل است که در اینجا خروجی Tuple صرفا به یک متغیر از نوع var نسبت داده شدهاست و توسط نام پارامترهای خروجی متد، میتوان به اعضای Tuple دسترسی یافت.
مفهوم Deconstructing Tuples
مفهوم deconstruction که در ابتدای بحث عنوان شد صرفا مختص به Tuples نیست. در C# 7 میتوان مشخص کرد که چگونه یک نوع خاص، به اجزای آن تجزیه شود. برای مثال کلاس شخص ذیل را درنظر بگیرید:
- در اینجا یک متد جدید را به نام Deconstruct مشاهده میکنید. کار این متد جدید که توسط کامپایلر استفاده خواهد شد، ارائهی روشی است برای «تجزیهی» یک نوع، به یک Tuple. متد Deconstruct تعریف شدهی در اینجا توسط پارامترهایی از نوع out، دو خروجی را مشخص میکنند. امکان تعریف این متد ویژه، به صورتیکه یک Tuple را بازگرداند، وجود ندارد.
- علت تعریف این دو خروجی هم به constructor و یا سازندهی کلاس بر میگردد که دو ورودی را دریافت میکند. اگر یک کلاس چندین سازنده داشته باشد، به همان تعداد میتوان متد Deconstruct تعریف کرد؛ به همراه خروجیهایی متناظر با نوع پارامترهای سازندهها.
- علت استفادهی از نوع خروجی out نیز این است که در #C نمیتوان چندین overload را صرفا بر اساس نوع خروجیهای متفاوت متدها تعریف کرد.
- متد Deconstruct به صورت خودکار در زمان تجزیهی یک شیء به یک tuple فراخوانی میشود. در مثال زیر، شیء p1 به یک Tuple تجزیه شدهاست و این تجزیه بر اساس متد Deconstruct این کلاس مفهوم پیدا میکند:
امکان تعریف متد Deconstruct، به صورت یک متد الحاقی
روش اول تعریف متد ویژهی Deconstruct را در مثال قبل، در داخل کلاس اصلی مشاهده کردید. روش دیگر آن، استفادهی از متدهای الحاقی است که در این مورد خاص نیز مجاز است:
در اینجا کلاس مستطیل دارای سازندهای با دو پارامتر است؛ اما متد Deconstruct آن به صورت یک متد الحاقی، خارج از کلاس اصلی تعریف شدهاست.
اکنون امکان انتساب وهلهای از این کلاس به یک Tuple وجود دارد:
امکان جایگزین کردن Anonymous types با Tuples
قطعه کد ذیل را در نظر بگیرید:
در اینجا خروجی LINQ تهیه شده یک لیست anonymously typed است؛ با محدودیتهایی مانند عدم امکان استفادهی از خروجی آن در سایر اسمبلیها. این نوعهای ویژه تنها محدود هستند به همان اسمبلی که در آن تعریف میشوند. اما در C# 7 میتوان قطعه کد فوق را با Tuples به صورت ذیل بازنویسی کرد که این محدودیتها را هم ندارد (با هدف به حداقل رساندن تعداد ViewModelهای تعریفی یک برنامه):
سایر کاربردهای Tuples
از Tuples صرفا برای تعریف چندین خروجی از یک متد استفاده نمیشود. در ذیل نحوهی استفادهی از آنها را جهت تعریف کلید ترکیبی یک شیء دیکشنری و یا استفادهی از آنها را در آرگومان جنریک یک متد async هم مشاهده میکنید:
Tuple چیست؟
هدف از کار با Tupleها، عدم تعریف یک کلاس جدید به همراه خواص آن، جهت بازگشت بیش از یک مقدار از یک متد، توسط وهلهای از این کلاس جدید میباشد. برای مثال اگر بخواهیم از متدی، دو مقدار شهر و ناحیه را بازگشت دهیم، یک روش آن، ایجاد کلاس مکان زیر است:
public class Location { public string City { get; set; } public string State { get; set; } public Location(string city, string state) { City = city; State = state; } }
var location = new Location("Lake Charles","LA");
var location = new Tuple<string,string>("Lake Charles","LA"); // Print out the address var address = $"{location.Item1}, {location.Item2}";
مشکلات نوع Tuple در نگارشهای قبلی دات نت
هرچند Tuples از زمان دات نت 4 در دسترس هستند، اما دارای این کمبودها و مشکلات میباشند:
static Tuple<int, string, string> GetHumanData() { return Tuple.Create(10, "Marcus", "Miller"); }
var data = GetHumanData(); Console.WriteLine("What is this value {0} or this {1}", data.Item1, data.Item3);
ج) Tuples در دات نت 4، صرفا یک کتابخانهی اضافه شدهی به فریم ورک بوده و زبانهای دات نتی، پشتیبانی توکاری را از آنها جهت بهبود و یا ساده سازی تعریف آنها، ارائه نمیدهند.
ایجاد Tuples در C# 7
برای ایجاد Tuples در سی شارپ 7، از پرانتزها به همراه ذکر نام و نوع پارامترها استفاده میشود.
(int x1, string s1) = (3, "one"); Console.WriteLine($"{x1} {s1}");
دسترسی به این مقادیر نیز همانند متغیرهای معمولی است.
اگر سعی کنیم این قطعه کد را کامپایل نمائیم، با خطای ذیل متوقف خواهیم شد:
error CS8179: Predefined type 'System.ValueTuple`2' is not defined or imported
PM> install-package System.ValueTuple
تعاریف متغیرهای بازگشتی، خارج از پرانتزها هم میتوانند صورت گیرند:
int x2; string s2; (x2, s2) = (42, "two"); Console.WriteLine($"{x2} {s2}");
بازگشت Tuples از متدها
متد ذیل، دو خروجی نتیجه و باقیماندهی تقسیم دو عدد صحیح را باز میگرداند:
static (int, int) Divide(int x, int y) { int result = x / y; int reminder = x % y; return (result, reminder); }
در ادامه نحوهی استفادهی از این متد را مشاهده میکنید:
(int result, int reminder) = Divide(11, 3); Console.WriteLine($"{result} {reminder}");
در اینجا امکان استفادهی از var نیز برای تعریف نوع متغیرهای دریافتی از یک Tuple نیز وجود دارد و کامپایلر به صورت خودکار نوع آنها را بر اساس نوع خروجی tuple مشخص میکند:
(var result1, var reminder1) = Divide(11, 3); Console.WriteLine($"{result1} {reminder1}");
var (result1, reminder1) = Divide(11, 3);
و یا برای نمونه متد GetHumanData دات نت 4 ابتدای بحث را به صورت ذیل میتوان در C# 7 بازنویسی کرد:
static (int, string, string) GetHumanData() { return (10, "Marcus", "Miller"); }
(int Age, string FirstName, string LastName) results = GetHumanData(); Console.WriteLine(results.Age); Console.WriteLine(results.FirstName); Console.WriteLine(results.LastName);
پشت صحنهی Tuples در C# 7
همانطور که عنوان شد، برای اینکه بتوانید قطعه کدهای فوق را کامپایل کنید، نیاز به بستهی نیوگت System.ValueTuple است. در حقیقت کامپایلر خروجی متد فوق را به نحو ذیل تفسیر میکند:
ValueTuple<int, int> tuple1 = Divide(11, 3);
(int, int) n = (1,1); System.Console.WriteLine(n.Item1);
ValueTuple<int, int> n = new ValueTuple<int, int>(1, 1); System.Console.WriteLine(n.Item1);
- همچنین در اینجا محدودیتی از لحاظ تعداد پارامترهای ذکر شدهی در یک Tuple وجود ندارد.
(int,int,int,int,int,int,int,(int,int))
مفهوم Tuple Literals
همانند نگارشهای پیشین دات نت، خروجی یک Tuple را میتوان به یک متغیر از نوع var و یا ValueType نیز نسبت داد:
var tuple2 = ("Stephanie", 7); Console.WriteLine($"{tuple2.Item1}, {tuple2.Item2}");
به علاوه در سی شارپ 7 میتوان برای اعضای یک Tuple نام نیز تعریف کرد که به آنها Tuple literals گویند:
var tuple3 = (Name: "Matthias", Age: 6); Console.WriteLine($"{tuple3.Name} {tuple3.Age}");
و یا هنگام تعریف نوع خروجی، میتوان نام پارامترهای متناظر را نیز ذکر کرد که به آن named elements هم میگویند:
static (int radius, double area) CalculateAreaOfCircle(int radius) { return (radius, Math.PI * Math.Pow(radius, 2)); }
var circle = CalculateAreaOfCircle(2); Console.WriteLine($"A circle of radius, {circle.radius}," + $" has an area of {circle.area:N2}.");
مفهوم Deconstructing Tuples
مفهوم deconstruction که در ابتدای بحث عنوان شد صرفا مختص به Tuples نیست. در C# 7 میتوان مشخص کرد که چگونه یک نوع خاص، به اجزای آن تجزیه شود. برای مثال کلاس شخص ذیل را درنظر بگیرید:
class Person { private readonly string _firstName; private readonly string _lastName; public Person(string firstname, string lastname) { _firstName = firstname; _lastName = lastname; } public override String ToString() => $"{_firstName} {_lastName}"; public void Deconstruct(out string firstname, out string lastname) { firstname = _firstName; lastname = _lastName; } }
- علت تعریف این دو خروجی هم به constructor و یا سازندهی کلاس بر میگردد که دو ورودی را دریافت میکند. اگر یک کلاس چندین سازنده داشته باشد، به همان تعداد میتوان متد Deconstruct تعریف کرد؛ به همراه خروجیهایی متناظر با نوع پارامترهای سازندهها.
- علت استفادهی از نوع خروجی out نیز این است که در #C نمیتوان چندین overload را صرفا بر اساس نوع خروجیهای متفاوت متدها تعریف کرد.
- متد Deconstruct به صورت خودکار در زمان تجزیهی یک شیء به یک tuple فراخوانی میشود. در مثال زیر، شیء p1 به یک Tuple تجزیه شدهاست و این تجزیه بر اساس متد Deconstruct این کلاس مفهوم پیدا میکند:
var p1 = new Person("Katharina", "Nagel"); (string first, string last) = p1; Console.WriteLine($"{first} {last}");
امکان تعریف متد Deconstruct، به صورت یک متد الحاقی
روش اول تعریف متد ویژهی Deconstruct را در مثال قبل، در داخل کلاس اصلی مشاهده کردید. روش دیگر آن، استفادهی از متدهای الحاقی است که در این مورد خاص نیز مجاز است:
public class Rectangle { public Rectangle(int height, int width) { Height = height; Width = width; } public int Width { get; } public int Height { get; } } public static class RectangleExtensions { public static void Deconstruct(this Rectangle rectangle, out int height, out int width) { height = rectangle.Height; width = rectangle.Width; } }
اکنون امکان انتساب وهلهای از این کلاس به یک Tuple وجود دارد:
var r1 = new Rectangle(100, 200); (int height, int width) = r1; Console.WriteLine($"height: {height}, width: {width}");
امکان جایگزین کردن Anonymous types با Tuples
قطعه کد ذیل را در نظر بگیرید:
List<Employee> allEmployees = new List<Employee>() { new Employee { ID = 1L, Name = "Fred", Salary = 50000M }, new Employee { ID = 2L, Name = "Sally", Salary = 60000M }, new Employee { ID = 3L, Name = "George", Salary = 70000M } }; var wellPaid = from oneEmployee in allEmployees where oneEmployee.Salary > 50000M select new { EmpName = oneEmployee.Name, Income = oneEmployee.Salary };
var wellPaid = from oneEmployee in allEmployees where oneEmployee.Salary > 50000M orderby oneEmployee.Salary descending select (EmpName: oneEmployee.Name, Income: oneEmployee.Salary); var highestPaid = wellPaid.First().EmpName;
سایر کاربردهای Tuples
از Tuples صرفا برای تعریف چندین خروجی از یک متد استفاده نمیشود. در ذیل نحوهی استفادهی از آنها را جهت تعریف کلید ترکیبی یک شیء دیکشنری و یا استفادهی از آنها را در آرگومان جنریک یک متد async هم مشاهده میکنید:
public Task<(int index, T item)> FindAsync<T>(IEnumerable<T> input, Predicate<T> match) { var dictionary = new Dictionary<(int, int), string>(); throw new NotSupportedException(); }