اندازهی قلم متن
تخمین مدت زمان مطالعهی مطلب:
ده دقیقه
نوع Span به همراه NET Core 2.1. ارائه شد. یکی از مهمترین مزایای آن امکان دسترسی به قسمتی از حافظه (توسط متد Split آن)، بدون ایجاد سربار کپی یا تخصیص مجدد حافظهای برای دسترسی به آن است. قدم بعدی، بسط این قابلیت به امکانات ذاتی زبان #C است؛ تحت عنوان ویژگی Ranges که امکان دسترسی مستقیم به بازهای/قسمتی از آرایهها، رشتهها و یا Spanها را میسر میکند.
معرفی عملگر Hat
برای دسترسی به آخرین عضو یک آرایه عموما از روش زیر استفاده میشود:
یعنی از آخر شروع به شمارش کرده و 1 واحد از آن کم میکنیم (این عدد 1 را بهخاطر داشته باشید) و یا اگر بخواهیم از LINQ استفاده کنیم میتوان از متد Last آن استفاده کرد:
همچنین اگر بخواهیم دومین عنصر از آخر آنرا دریافت کنیم:
نیز مجددا از آخر شروع به شمارش کرده و 2 واحد، از آن کم میکنیم (این عدد 2 را نیز بهخاطر داشته باشید).
این شمردنهای از آخر در C# 8.0 توسط ارائهی عملگر hat یا همان ^ که پیشتر کار xor را انجام میداد (و البته هنوز هم در جای خودش همین مفهوم را دارد)، میسر شدهاست:
این قطعه کد یعنی به دنبال ایندکس X، از آخر آرایه هستیم.
نکتهی مهم: کسانیکه شروع به آموزش برنامه نویسی میکنند، مدتی طول میکشد تا عادت کنند که اولین ایندکس یک آرایه از صفر شروع میشود. در اینجا باید درنظر داشت که با بکارگیری «عملگر کلاه»، آخرین ایندکس یک آرایه از «یک» شروع میشود و نه از صفر. برای نمونه در مثال زیر به خوبی تفاوت بین ایندکس از ابتدا و ایندکس از انتها را میتوانید مشاهده کنید:
آرایهی فوق، 9 عضو دارد. در این حالت اولین عنصر آن با ایندکس صفر قابل دسترسی است. در همین حالت همین ایندکس را اگر از آخر محاسبه کنیم، 9 خواهد بود و همینطور برای مابقی.
در حالت کلی ایندکس n^ معادل sequence.Length - n است. بنابراین sequence[^0] به معنای sequence[sequence.Length] است و هر دو مورد یک index out of range exception را صادر میکنند.
IDE نیز با فعال سازی C# 8.0، زمانیکه به قطعه کد زیر میرسد، زیر words.Length - 1 خط کشیده و پیشنهاد میدهد که بهتر است از 1^ استفاده کنید:
معرفی نوع جدید Index
در C# 8.0 زمانیکه مینویسم 1^، در حقیقت قطعه کد زیر را ایجاد کردهایم:
Index یک struct و نوع جدید در C# 8.0 میباشد که در فضای نام System قرار گرفتهاست. سه سطر فوق دقیقا به یک معنا هستند و هر کدام خلاصه شده و ساده شدهی سطر قبلی است.
در سطر اول، پارامتر fromEnd نیز قابل تعریف است. این fromEnd با مقدار true، همان عملگر ^ در اینجا است و عدم ذکر این عملگر به معنای false بودن آن است:
در این مثال دو نمونه کاربرد fromEnd با مقدار false و سپس true را ملاحظه میکنید. در حالتیکه Index i5 = 5 تعریف شدهاست، دسترسی به عناصر آرایه همانند قبل از ایندکس صفر و از آغاز شروع میشود و نه از ایندکس یک.
روش دسترسی به بازهای از اعضای یک آرایه تا پیش از C# 8.0
فرض کنید آرایهای از اعداد بین 1 تا 10 را به صورت زیر ایجاد کردهاید:
اکنون اگر بخواهیم به بازهی مشخصی درون این آرایه دسترسی پیدا کنیم، میتوان حداقل به یکی از دو روش زیر عمل کرد:
یا میتوان برای مثال توسط LINQ، از متدهای Skip و Take آن برای جدا کردن بازهای از آرایهی numbers استفاده کرد و یا حتی میتوان از روش کپی کردن آرایهها به آرایهای جدید نیز کمک گرفت که در هر دو حالت، مراحلی که باید طی شوند قابل توجه است. با ارائهی C# 8.0، این نوع دسترسیها جزئی از قابلیتهای زبان شدهاند.
روش دسترسی به بازهای از اعضای یک آرایه در C# 8.0
در C# 8.0 برای دسترسی به بازهای از عناصر یک آرایه میتوان از range expression که به صورت x..y نوشته میشود، استفاده کرد. در ادامه، مثالهایی را از کاربردهای عبارت .. ملاحظه میکنید:
3..1 به معنای انتخاب بازهای از المان 2 تا المان 3 است. در اینجا به واژهی «المان» دقت کنید که معادل ایندکس آن در آرایه نیست. یعنی عدد ابتدای یک بازه دقیقا به ایندکس آن در آرایه اشاره میکند و عدد انتهای بازه، به ایندکس ماقبل آن (از این جهت که بتوان توسط 0^، انتهای بازه را مشخص کرد؛ بدون دریافت استثنای index out of range). به همین جهت به ابتدای بازه میگویند inclusive و به انتهای آن exclusive:
1^..1 به معنای انتخاب بازهای از المان 2، تا المان یکی مانده به آخر است:
همچنین [0^..0] نیز به معنای کل بازه است.
مثالی دیگر: بازنویسی یک حلقهی for با foreach
حلقهی for زیر را
توسط range expression میتوان به صورت زیر بازنویسی کرد:
بنابراین همانطور که مشاهده میکنید، ذکر بازهی 4..1 به صورت حلقهی for (int i = 1; i < 4; i++) تفسیر میشود و نه حلقهی for (int i = 1; i <= 4; i++)
یعنی ابتدای آن inclusive است و انتهای آن exclusive
چند مثال کاربردی و متداول از بازهها
معرفی نوع جدید Range
در C# 8.0 زمانیکه مینویسم 4..1، در حقیقت قطعه کد زیر را ایجاد کردهایم:
Range نیز یک struct و نوع جدید در C# 8.0 میباشد که در فضای نام System قرار گرفتهاست. سه سطر فوق دقیقا به یک معنا هستند و هر کدام خلاصه شده و ساده شدهی سطر قبلی است.
یک مثال: استفاده از نوع جدید Range به عنوان پارامتر یک متد
همانطور که ملاحظه میکنید، Range را میتوان به عنوان پارامتر متدها نیز استفاده و بر روی آرایهها اعمال کرد؛ اما با <List<T سازگار نیست.
مثالی دیگر: استفاده از Range به عنوان جایگزینی برای متد String.Substring
از Range میتوان برای کار بر روی رشتهها و انتخاب قسمتی از آنها نیز استفاده کرد:
چند مثال دیگر:
سؤال: زمانیکه بازهای از یک آرایه را انتخاب میکنیم، آیا یک آرایهی جدید ایجاد میشود، یا هنوز به همان آرایهی قبلی اشاره میکند؟
پاسخ: یک آرایهی جدید ایجاد میشود؛ اما میتوان با فراخوانی متد ()array.AsSpan پیش از انتخاب یک بازه، بازهای را تولید کرد که دقیقا به همان آرایهی اصلی اشاره میکند و یک کپی جدید نیست:
در این مثال، آرایهی اصلی را ابتدا تبدیل به یک Span کردهایم و سپس بازهای از روی آن انتخاب شدهاست. به همین جهت است که زمانیکه از ref locals برای تغییر عضوی از این بازه استفاده میشود، این تغییر بر روی آرایهی اصلی نیز تاثیر میگذارد.
معرفی عملگر Hat
برای دسترسی به آخرین عضو یک آرایه عموما از روش زیر استفاده میشود:
var integerArray = new int[3]; var lastItem = integerArray[integerArray.Length - 1];
var integerList = integerArray.ToList(); integerList.Last();
var secondToLast = integerArray[integerArray.Length - 2];
این شمردنهای از آخر در C# 8.0 توسط ارائهی عملگر hat یا همان ^ که پیشتر کار xor را انجام میداد (و البته هنوز هم در جای خودش همین مفهوم را دارد)، میسر شدهاست:
var lastItem = integerArray[^1];
نکتهی مهم: کسانیکه شروع به آموزش برنامه نویسی میکنند، مدتی طول میکشد تا عادت کنند که اولین ایندکس یک آرایه از صفر شروع میشود. در اینجا باید درنظر داشت که با بکارگیری «عملگر کلاه»، آخرین ایندکس یک آرایه از «یک» شروع میشود و نه از صفر. برای نمونه در مثال زیر به خوبی تفاوت بین ایندکس از ابتدا و ایندکس از انتها را میتوانید مشاهده کنید:
var words = new string[] { // index from start index from end "The", // 0 ^9 "quick", // 1 ^8 "brown", // 2 ^7 "fox", // 3 ^6 "jumped", // 4 ^5 "over", // 5 ^4 "the", // 6 ^3 "lazy", // 7 ^2 "dog" // 8 ^1 }; // 9 (or words.Length) ^0
در حالت کلی ایندکس n^ معادل sequence.Length - n است. بنابراین sequence[^0] به معنای sequence[sequence.Length] است و هر دو مورد یک index out of range exception را صادر میکنند.
IDE نیز با فعال سازی C# 8.0، زمانیکه به قطعه کد زیر میرسد، زیر words.Length - 1 خط کشیده و پیشنهاد میدهد که بهتر است از 1^ استفاده کنید:
Console.WriteLine($"The last word is {words[words.Length - 1]}");
معرفی نوع جدید Index
در C# 8.0 زمانیکه مینویسم 1^، در حقیقت قطعه کد زیر را ایجاد کردهایم:
var index = new Index(value: 1, fromEnd: true); Index indexStruct = ^1; var indexShortHand = ^1;
در سطر اول، پارامتر fromEnd نیز قابل تعریف است. این fromEnd با مقدار true، همان عملگر ^ در اینجا است و عدم ذکر این عملگر به معنای false بودن آن است:
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; Console.WriteLine(a[a.Length – 2]); // will write 8 on console. Console.WriteLine(a[^2]); // will write 8 on console. Index i5 = 5; Console.WriteLine(a[i5]); //will write 5 on console. Index i2fromEnd = ^2; Console.WriteLine(a[i2fromEnd]); // will write 8 on console.
روش دسترسی به بازهای از اعضای یک آرایه تا پیش از C# 8.0
فرض کنید آرایهای از اعداد بین 1 تا 10 را به صورت زیر ایجاد کردهاید:
var numbers = Enumerable.Range(1, 10).ToArray();
var (start, end) = (1, 7); var length = end - start; // Using LINQ var subset1 = numbers.Skip(start).Take(length); // Or using Array.Copy var subset2 = new int[length]; Array.Copy(numbers, start, subset2, 0, length);
روش دسترسی به بازهای از اعضای یک آرایه در C# 8.0
در C# 8.0 برای دسترسی به بازهای از عناصر یک آرایه میتوان از range expression که به صورت x..y نوشته میشود، استفاده کرد. در ادامه، مثالهایی را از کاربردهای عبارت .. ملاحظه میکنید:
var myArray = new string[] { "Item1", "Item2", "Item3", "Item4", "Item5" };
var fromIndexToX = myArray[1..3]; // = [Item2, Item3]
var fromIndexToXFromTheEnd = myArray[1..^1]; // = [ "Item2", "Item3", "Item4" ]
ذکر انتهای بازه اجباری نیست و اگر ذکر نشود به معنای 0^ است. برای مثال ..1 به معنای انتخاب بازهای از المان 2، تا آخرین المان است:
var fromAnIndexToTheEnd = myArray[1..]; // = [ "Item2", "Item3", "Item4", "Item5" ]
ذکر ابتدای بازه نیز اجباری نیست و اگر ذکر نشود به معنای عدد صفر است. برای مثال 3.. به معنای انتخاب بازهای از اولین المان، تا سومین المان است:
var fromTheStartToAnIndex = myArray[..3]; // = [ "Item1", "Item2", "Item3" ]
اگر ابتدا و انتهای بازه ذکر نشوند، کل بازه و تمام عناصر آن بازگشت داده میشوند:
var entireRange = myArray[..]; // = [ "Item1", "Item2", "Item3", "Item4", "Item5" ]
مثالی دیگر: بازنویسی یک حلقهی for با foreach
حلقهی for زیر را
var myArray = new string[] { "Item1", "Item2", "Item3", "Item4", "Item5" }; for (int i = 1; i <= 3; i++) { Console.WriteLine(myArray[i]); }
foreach (var item in myArray[1..4]) // = [ "Item2", "Item3", "Item4" ] { Console.WriteLine(item); }
یعنی ابتدای آن inclusive است و انتهای آن exclusive
چند مثال کاربردی و متداول از بازهها
using System; using System.Linq; namespace ConsoleApp { class Program { private static readonly int[] _numbers = Enumerable.Range(1, 10).ToArray(); static void Main() { var skip2CharactersAndTake2Characters = _numbers[2..4]; // صرفنظر کردن از دو عنصر اول و سپس انتخاب دو عنصر var skipFirstAndLastCharacter = _numbers[1..^1]; // صرفنظر کردن از دو عنصر اول و آخر var last3Characters = _numbers[^3..]; // انتخاب بازهای شامل سه عنصر آخر var first4Characters = _numbers[0..4]; // دریافت بازهای از 4 عنصر اول var rangeStartFrom2 = _numbers[2..]; // دریافت بازهای شروع شده از المان دوم تا آخر var skipLast3Characters = _numbers[..^3]; // صرفنظر کردن از سه المان آخر var rangeAll = _numbers[..]; // انتخاب کل بازه } } }
معرفی نوع جدید Range
در C# 8.0 زمانیکه مینویسم 4..1، در حقیقت قطعه کد زیر را ایجاد کردهایم:
var range = new Range(1, 4); Range rangeStruct = 1..4; var rangeShortHand = 1..4;
یک مثال: استفاده از نوع جدید Range به عنوان پارامتر یک متد
using System; using System.Linq; namespace ConsoleApp { class Program { private static readonly int[] _numbers = Enumerable.Range(1, 10).ToArray(); static void Print(Range range) => Console.WriteLine($"{range} => {string.Join(", ", _numbers[range])}"); static void Main() { Print(1..3); // 1..3 => 2, 3 Print(..3); // 0..3 => 1, 2, 3 Print(3..); // 3..^0 => 4, 5, 6, 7, 8, 9, 10 Print(1..^1); // 1..^1 => 2, 3, 4, 5, 6, 7, 8, 9 Print(^2..^1); // ^2..^1 => 9 } } }
مثالی دیگر: استفاده از Range به عنوان جایگزینی برای متد String.Substring
از Range میتوان برای کار بر روی رشتهها و انتخاب قسمتی از آنها نیز استفاده کرد:
Console.WriteLine("123456789"[1..4]); // Would output 234
var helloWorldStr = "Hello, World!"; var hello = helloWorldStr[..5]; Console.WriteLine(hello); // Output: Hello var world = helloWorldStr[7..]; Console.WriteLine(world); // Output: World! var world2 = helloWorldStr[^6..]; // Take the last 6 characters Console.WriteLine(world); // Output: World!
سؤال: زمانیکه بازهای از یک آرایه را انتخاب میکنیم، آیا یک آرایهی جدید ایجاد میشود، یا هنوز به همان آرایهی قبلی اشاره میکند؟
پاسخ: یک آرایهی جدید ایجاد میشود؛ اما میتوان با فراخوانی متد ()array.AsSpan پیش از انتخاب یک بازه، بازهای را تولید کرد که دقیقا به همان آرایهی اصلی اشاره میکند و یک کپی جدید نیست:
var arr = (new[] { 1, 4, 8, 11, 19, 31 }).AsSpan(); var range = arr[2..5]; ref int elt1 = ref range[1]; elt1 = -1; int copiedElement = range[2]; copiedElement = -2; Console.WriteLine($"range[1]: {range[1]}, range[2]: {range[2]}"); // output: range[1]: -1, range[2]: 19 Console.WriteLine($"arr[3]: {arr[3]}, arr[4]: {arr[4]}"); // output: arr[3]: -1, arr[4]: 19