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(); }