فصل ۷: کلاس Array؛ ساخت، جستوجو، مرتبسازی و کپی
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
کلاس Array
Class با نام Array Base Class ضمنی برای تمام Arrayهای تکبعدی و چندبعدی است و یکی از بنیادیترین Typeهایی محسوب میشود که Interfaceهای استاندارد Collection را پیادهسازی میکنند. Array Type Unification فراهم میکند؛ بنابراین مجموعهای مشترک از Methodها برای همهٔ Arrayها، صرفنظر از نحوهٔ Declaration یا Type عنصر زیرین آنها، در دسترس است.
چون Arrayها بسیار بنیادی هستند، C# برای Declaration و Initialization آنها Syntax صریح دارد که در فصلهای ۲ و ۳ توضیح دادیم. وقتی Array با Syntax زبان C# Declare میشود، CLR بهطور ضمنی از Class با نام Array Subtype میسازد و یک Pseudotype متناسب با Dimensionها و Type عنصر Array تولید میکند. این Pseudotype Interfaceهای Typed و Generic Collection مانند IList<string> را پیادهسازی میکند.
CLR هنگام ساخت Array Typeها رفتار ویژهای نیز دارد و فضای پیوستهای در Memory به آنها اختصاص میدهد. این ویژگی Index کردن Array را بسیار کارآمد میکند، اما مانع تغییر اندازهٔ آن در ادامه میشود.
Array Interfaceهای Collection را تا سطح IList<T>، هم در فرم Generic و هم Nongeneric، پیادهسازی میکند. خود IList<T> بهصورت Explicit پیادهسازی شده است تا Public Interface مربوط به Array با Methodهایی مانند Add یا Remove شلوغ نشود؛ این Methodها برای Collectionهای Fixed-length مانند Array در هر صورت Exception پرتاب میکنند. Class با نام Array واقعاً یک Method Static با نام Resize دارد، اما این Method با ساختن Array جدید و سپس کپی هر Element کار میکند. علاوه بر ناکارآمدبودن، Referenceهای دیگری که در Program به Array وجود دارند همچنان به نسخهٔ Original اشاره خواهند کرد. برای Collectionهای قابل تغییر اندازه، راهحل بهتر استفاده از List<T> است که در بخش بعدی شرح داده میشود.
یک Array میتواند Elementهای Value Type یا Reference Type داشته باشد. Elementهای Value Type مستقیماً در خود Array ذخیره میشوند؛ بنابراین Arrayای از سه Integer نوع long که هرکدام 8 Byte هستند، 24 Byte Memory پیوسته اشغال میکند. در مقابل، Element از نوع Reference Type فقط بهاندازهٔ یک Reference در Array جا میگیرد: 4 Byte در محیط 32-bit یا 8 Byte در محیط 64-bit. شکل 7-2 اثر Program زیر را در Memory نشان میدهد:
StringBuilder[] builders = new StringBuilder [5];
builders [0] = new StringBuilder ("builder1");
builders [1] = new StringBuilder ("builder2");
builders [2] = new StringBuilder ("builder3");
long[] numbers = new long [3];
numbers [0] = 12345;
numbers [1] = 54321;
شکل 7-2 — Arrayها در Memoryچون Array یک Class است، خود Arrayها همیشه Reference Type هستند؛ فارغ از Type عنصر Array. بنابراین Statement با نام arrayB = arrayA باعث میشود دو Variable به همان Array واحد Reference بدهند. به همین شکل، دو Array متمایز همیشه در Equality Test شکست میخورند، مگر اینکه از Structural Equality Comparer استفاده کنید که تمام Elementهای Array را مقایسه میکند:
object[] a1 = { "string", 123, true };
object[] a2 = { "string", 123, true };
Console.WriteLine (a1 == a2); // False
Console.WriteLine (a1.Equals (a2)); // False
IStructuralEquatable se1 = a1;
Console.WriteLine (se1.Equals (a2,
StructuralComparisons.StructuralEqualityComparer)); // True
میتوان Arrayها را با Method با نام Clone Duplicate کرد: arrayB = arrayA.Clone(). بااینحال، نتیجه یک Shallow Clone است؛ یعنی فقط Memory نمایندهٔ خود Array کپی میشود. اگر Array شامل Objectهای Value Type باشد، خود Valueها کپی میشوند؛ اگر شامل Objectهای Reference Type باشد، فقط Referenceها کپی میشوند و در نتیجه دو Array خواهیم داشت که Memberهای آنها به همان Objectها اشاره میکنند. شکل 7-3 اثر افزودن کد زیر به مثال را نشان میدهد:
StringBuilder[] builders2 = builders;
StringBuilder[] shallowClone = (StringBuilder[]) builders.Clone();
شکل 7-3 — Shallow-cloning یک Arrayبرای ساخت Deep Copy — یعنی حالتی که Subobjectهای Reference Type نیز Duplicate میشوند — باید در Array Loop بزنید و هر Element را بهصورت دستی Clone کنید. همین قواعد برای سایر Typeهای Collection در .NET نیز برقرار است.
اگرچه Array عمدتاً برای Indexerهای 32-bit طراحی شده، از Indexerهای 64-bit نیز بهطور محدود پشتیبانی میکند؛ چند Method هم Parameterهای Int32 و هم Int64 میپذیرند و بهصورت نظری اجازه میدهند Array تا 264 Element Address کند. این Overloadها در عمل بیفایدهاند، زیرا CLR اجازه نمیدهد هیچ Objectی — از جمله Array — بیش از دو Gigabyte اندازه داشته باشد، چه در محیط 32-bit و چه 64-bit.
بسیاری از Methodهای Class با نام Array که انتظار دارید Instance Method باشند، در واقع Static Method هستند. این یک تصمیم طراحی عجیب است و به این معناست که هنگام جستوجوی Method روی Array باید هم Static Methodها و هم Instance Methodها را بررسی کنید.
Construction و Indexing
آسانترین روش ساخت و Index کردن Arrayها استفاده از Constructهای زبان C# است:
int[] myArray = { 1, 2, 3 };
int first = myArray [0];
int last = myArray [myArray.Length - 1];
راه دیگر این است که با فراخوانی Array.CreateInstance در Runtime یک Array بهصورت Dynamic بسازید. این کار اجازه میدهد Type عنصر و Rank — تعداد Dimensionها — را در Runtime مشخص کنید و با تعیین Lower Bound، Arrayهای Nonzero-based نیز بسازید. Arrayهای Nonzero-based با .NET Common Language Specification یا CLS سازگار نیستند و نباید بهصورت Public Member در Libraryای ارائه شوند که ممکن است Program نوشتهشده با F# یا Visual Basic آن را مصرف کند.
Methodهای GetValue و SetValue اجازه میدهند به Elementهای Arrayای که بهصورت Dynamic ساخته شده دسترسی داشته باشید؛ این Methodها روی Arrayهای معمولی هم کار میکنند:
// Create a string array 2 elements in length:
Array a = Array.CreateInstance (typeof(string), 2);
a.SetValue ("hi", 0); // → a[0] = "hi";
a.SetValue ("there", 1); // → a[1] = "there";
string s = (string) a.GetValue (0); // → s = a[0];
// We can also cast to a C# array as follows:
string[] cSharpArray = (string[]) a;
string s2 = cSharpArray [0];
Arrayهای Zero-indexed که بهصورت Dynamic ساخته شدهاند، میتوانند به Array C# با Type Matching یا Compatible Cast شوند؛ سازگاری براساس قواعد استاندارد Array Variance تعیین میشود. برای مثال، اگر Apple از Fruit مشتق شود، Apple[] را میتوان به Fruit[] Cast کرد. در اینجا سؤال پیش میآید که چرا بهجای Class با نام Array، از object[] بهعنوان Type یکپارچهکنندهٔ Array استفاده نشده است. پاسخ این است که object[] با Arrayهای چندبعدی و Arrayهای Value Type — و نیز Nonzero-based — سازگار نیست. یک int[] را نمیتوان به object[] Cast کرد. بنابراین برای Type Unification کامل به Class با نام Array نیاز داریم.
GetValue و SetValue روی Arrayهای ساختهشده توسط Compiler نیز کار میکنند و هنگام نوشتن Methodهایی مفیدند که باید با Array از هر Type و Rank کار کنند. برای Arrayهای چندبعدی، آنها Arrayای از Indexerها میپذیرند:
public object GetValue (params int[] indices)
public void SetValue (object value, params int[] indices)
Method زیر نخستین Element هر Array را، صرفنظر از Rank آن، چاپ میکند:
void WriteFirstValue (Array a)
{
Console.Write (a.Rank + "-dimensional; ");
// The indexers array will automatically initialize to all zeros, so
// passing it into GetValue or SetValue will get/set the zero-based
// (i.e., first) element in the array.
int[] indexers = new int[a.Rank];
Console.WriteLine ("First value is " + a.GetValue (indexers));
}
void Demo()
{
int[] oneD = { 1, 2, 3 };
int[,] twoD = { {5,6}, {8,9} };
WriteFirstValue (oneD); // 1-dimensional; first value is 1
WriteFirstValue (twoD); // 2-dimensional; first value is 5
}
برای کار با Arrayهایی با Type ناشناخته اما Rank معلوم، Generics راهحل آسانتر و کارآمدتری ارائه میدهند:
void WriteFirstValue<T> (T[] array)
{
Console.WriteLine (array[0]);
}
SetValue اگر Element از Type ناسازگار با Array باشد Exception پرتاب میکند.
هنگامی که Array، چه از طریق Syntax زبان و چه با Array.CreateInstance، Instantiate میشود، Elementهای آن خودکار با Default Valueهایشان Initialize میشوند. برای Arrayهای دارای Element از Reference Type این یعنی نوشتن null؛ برای Arrayهای دارای Element از Value Type یعنی Memberها از نظر Bitwise «صفر» میشوند. Class با نام Array این قابلیت را در صورت نیاز از طریق Method با نام Clear نیز فراهم میکند:
public static void Clear (Array array, int index, int length);
این Method اندازهٔ Array را تغییر نمیدهد. این رفتار با کاربرد معمول Clear — مثلاً ICollection<T>.Clear — متفاوت است، جایی که Collection به صفر Element کاهش مییابد.
Enumeration
Arrayها را بهسادگی میتوان با Statement با نام foreach Enumerate کرد:
int[] myArray = { 1, 2, 3};
foreach (int val in myArray)
Console.WriteLine (val);
همچنین میتوانید از Method Static با نام Array.ForEach استفاده کنید که چنین تعریف شده است:
public static void ForEach<T> (T[] array, Action<T> action);
این Method از Delegate با نام Action و Signature زیر استفاده میکند:
public delegate void Action<T> (T obj);
مثال نخست با Array.ForEach چنین بازنویسی میشود:
Array.ForEach (new[] { 1, 2, 3 }, Console.WriteLine);
با Collection Expression در C# 12 میتوان باز هم آن را سادهتر کرد:
Array.ForEach ([ 1, 2, 3 ], Console.WriteLine);
Length و Rank
Array برای Query کردن Length و Rank Methodها و Propertyهای زیر را فراهم میکند:
public int GetLength (int dimension);
public long GetLongLength (int dimension);
public int Length { get; }
public long LongLength { get; }
public int GetLowerBound (int dimension);
public int GetUpperBound (int dimension);
public int Rank { get; } // Returns number of dimensions in array
GetLength و GetLongLength طول یک Dimension مشخص را برمیگردانند — برای Array تکبعدی Dimension برابر 0 است — و Length و LongLength تعداد کل Elementهای Array را در تمام Dimensionها برمیگردانند.
GetLowerBound و GetUpperBound برای Arrayهای دارای Index غیرصفر مفیدند. GetUpperBound برای هر Dimension همان نتیجهای را میدهد که از جمع GetLowerBound با GetLength و کسر 1 به دست میآید.
Searching
Class با نام Array مجموعهای از Methodها را برای یافتن Elementها در Array تکبعدی ارائه میکند:
BinarySearch- برای جستوجوی سریع یک Item مشخص در Array مرتبشده.
IndexOf/LastIndexOf- برای جستوجوی یک Item مشخص در Array نامرتب.
Find/FindLast/FindIndex/FindLastIndex/FindAll/Exists/TrueForAll- برای جستوجوی Item یا Itemهایی در Array نامرتب که یک
Predicate<T> مشخص را ارضا میکنند.
هیچیک از Methodهای جستوجوی Array وقتی Value مورد نظر پیدا نشود Exception پرتاب نمیکند. در عوض، Methodهایی که Integer برمیگردانند در صورت نبود Item مقدار −1 میدهند — با فرض Array صفرمبنا — و Methodهایی که Generic Type برمیگردانند Default Value آن Type را میدهند؛ مثلاً 0 برای int یا null برای string.
Methodهای Binary Search سریعاند، اما فقط روی Array مرتبشده کار میکنند و لازم است Elementها از نظر Order، نه صرفاً Equality، مقایسه شوند. برای این منظور Methodهای Binary Search میتوانند Object از نوع IComparer یا IComparer<T> بپذیرند تا دربارهٔ Ordering تصمیم بگیرد؛ بخش «Plugging in Equality and Order» در صفحهٔ 411 کتاب را ببینید. این Comparer باید با Comparerای که در ابتدا برای Sort کردن Array استفاده شده سازگار باشد. اگر Comparer ارائه نشود، Algorithm پیشفرض Ordering آن Type براساس پیادهسازی IComparable / IComparable<T> اعمال میشود.
Methodهای IndexOf و LastIndexOf یک Enumeration ساده روی Array انجام میدهند و Position اولین یا آخرین Element Matching را برمیگردانند.
Methodهای جستوجوی مبتنی بر Predicate اجازه میدهند Method Delegate یا Lambda Expression تعیین کند یک Element «Match» محسوب میشود یا نه. Predicate صرفاً Delegateای است که یک Object میپذیرد و true یا false برمیگرداند:
public delegate bool Predicate<T> (T object);
در مثال بعدی، در Arrayای از Stringها بهدنبال Nameای میگردیم که حرف a را داشته باشد:
string[] names = { "Rodney", "Jack", "Jill" };
string match = Array.Find (names, ContainsA);
Console.WriteLine (match); // Jack
ContainsA (string name) { return name.Contains ("a"); }
همان کد با Lambda Expression کوتاهتر میشود:
string[] names = { "Rodney", "Jack", "Jill" };
string match = Array.Find (names, n => n.Contains ("a")); // Jack
FindAll Arrayای از تمام Itemهایی برمیگرداند که Predicate را ارضا میکنند. در واقع معادل Enumerable.Where در Namespace با نام System.Linq است، با این تفاوت که FindAll یک Array از Itemهای Matching برمیگرداند، نه IEnumerable<T>.
Exists اگر هر Member از Array Predicate را ارضا کند true برمیگرداند و معادل Any در System.Linq.Enumerable است.
TrueForAll اگر همهٔ Itemها Predicate را ارضا کنند true برمیگرداند و معادل All در System.Linq.Enumerable است.
Sorting
Array Methodهای Built-in زیر را برای Sort کردن دارد:
// For sorting a single array:
public static void Sort<T> (T[] array);
public static void Sort (Array array);
// For sorting a pair of arrays:
public static void Sort<TKey,TValue> (TKey[] keys, TValue[] items);
public static void Sort (Array keys, Array items);
هر یک از این Methodها Overloadهایی نیز دارند که موارد زیر را میپذیرند:
int index // Starting index at which to begin sorting
int length // Number of elements to sort
IComparer<T> comparer // Object making ordering decisions
Comparison<T> comparison // Delegate making ordering decisions
سادهترین استفاده از Sort:
int[] numbers = { 3, 2, 1 };
Array.Sort (numbers); // Array is now { 1, 2, 3 }
Methodهایی که یک جفت Array میپذیرند، Itemهای هر دو Array را همزمان Rearrange میکنند و تصمیمهای Ordering را براساس Array اول میگیرند. در مثال بعدی هم Numberها و هم Word متناظر آنها به ترتیب عددی Sort میشوند:
int[] numbers = { 3, 2, 1 };
string[] words = { "three", "two", "one" };
Array.Sort (numbers, words);
// numbers array is now { 1, 2, 3 }
// words array is now { "one", "two", "three" }
Array.Sort نیاز دارد Elementهای Array IComparable را پیادهسازی کنند؛ بخش «Order Comparison» در صفحهٔ 355 را ببینید. بنابراین بیشتر Typeهای Built-in در C# — مانند Integerها در مثال قبل — قابل Sort هستند. اگر Elementها ذاتاً Comparable نباشند یا بخواهید Ordering پیشفرض را Override کنید، باید یک Comparison Provider سفارشی به Sort بدهید که Position نسبی دو Element را گزارش کند. دو راه برای این کار وجود دارد:
- Object کمکی که
IComparer / IComparer<T> را پیادهسازی کند؛ بخش «Plugging in Equality and Order» در صفحهٔ 411. - یک Delegate از نوع
Comparison:
public delegate int Comparison<T> (T x, T y);
Delegate با نام Comparison همان Semantics مربوط به IComparer<T>.CompareTo را دنبال میکند: اگر x قبل از y باشد Integer منفی، اگر بعد از آن باشد Integer مثبت، و اگر هر دو در Position یکسان Sort باشند 0 برمیگرداند.
مثال زیر Arrayای از Integerها را طوری Sort میکند که Numberهای Odd ابتدا بیایند:
int[] numbers = { 1, 2, 3, 4, 5 };
Array.Sort (numbers, (x, y) => x % 2 == y % 2 ? 0 : x % 2 == 1 ? -1 : 1);
// numbers array is now { 1, 3, 5, 2, 4 }
بهجای فراخوانی Sort میتوانید از Operatorهای OrderBy و ThenBy در LINQ استفاده کنید. برخلاف Array.Sort، Operatorهای LINQ Array اصلی را تغییر نمیدهند و Result مرتبشده را در Sequence تازهای از نوع IEnumerable<T> تولید میکنند.
Reversing Elements
Methodهای زیر در Array ترتیب همهٔ Elementها — یا بخشی از آنها — را Reverse میکنند:
public static void Reverse (Array array);
public static void Reverse (Array array, int index, int length);
Copying
Array چهار Method برای Shallow Copy دارد: Clone، CopyTo، Copy و ConstrainedCopy. دو مورد اول Instance Method و دو مورد آخر Static Method هستند.
Clone یک Array کاملاً جدید — با Shallow Copy — برمیگرداند. CopyTo و Copy یک Subset پیوسته از Array را کپی میکنند. برای کپی یک Array مستطیلی چندبعدی باید Index چندبعدی را به Index خطی Map کنید. برای مثال، خانهٔ وسط یعنی position[1,1] در Array سه در سه با Index شمارهٔ 4 نمایش داده میشود.
Index شمارهٔ 4 از محاسبهٔ 1 * 3 + 1 به دست میآید. Rangeهای Source و Destination میتوانند بدون مشکل Overlap داشته باشند.
ConstrainedCopy یک Operation اتمی انجام میدهد: اگر همهٔ Elementهای درخواستشده نتوانند با موفقیت کپی شوند — مثلاً بهعلت Type Error — Operation Rollback میشود.
Array همچنین Method با نام AsReadOnly فراهم میکند که Wrapperای برمیگرداند و از Reassign شدن Elementها جلوگیری میکند.
Converting و Resizing
Array.ConvertAll یک Array جدید با Element Type از نوع TOutput میسازد و برمیگرداند و برای کپی Elementها Delegate ارائهشدهٔ Converter را فراخوانی میکند. Converter چنین تعریف شده است:
public delegate TOutput Converter<TInput,TOutput> (TInput input)
مثال زیر Arrayای از float را به Arrayای از Integer تبدیل میکند:
float[] reals = { 1.3f, 1.5f, 1.8f };
int[] wholes = Array.ConvertAll (reals, r => Convert.ToInt32 (r));
// wholes array is { 1, 2, 2 }
Method با نام Resize با ساخت Array جدید و کپی Elementها کار میکند و Array جدید را از طریق Reference Parameter برمیگرداند. بااینحال، هر Reference به Array اصلی که در Objectهای دیگر نگهداری شده باشد بدون تغییر باقی میماند.
Namespace با نام System.Linq مجموعهٔ بزرگ دیگری از Extension Methodها برای تبدیل Array فراهم میکند. این Methodها IEnumerable<T> برمیگردانند که میتوانید با Method با نام ToArray در Enumerable دوباره آن را به Array تبدیل کنید.
Listها، Queueها، Stackها و Setها
.NET مجموعهای پایه از Concrete Collection Classها را فراهم میکند که Interfaceهای توصیفشده در این فصل را پیادهسازی میکنند. این بخش روی Collectionهای شبیه List تمرکز دارد، در مقابل Collectionهای شبیه Dictionary که در بخش «Dictionaries» صفحهٔ 394 کتاب پوشش داده میشوند. همانند Interfaceهای قبلی، معمولاً برای هر Type میان نسخهٔ Generic و Nongeneric انتخاب دارید. از نظر Flexibility و Performance، Classهای Generic برندهاند و همتایان Nongeneric را جز برای Backward Compatibility تقریباً زائد میکنند. این وضعیت با Interfaceهای Collection متفاوت است، چون نسخههای Nongeneric Interfaceها هنوز گاهی مفیدند.
در میان Classهای این بخش، List<T> Generic پرکاربردترین است.