فصل ۷: Collectionها، Enumeration و رابطهای مجموعه
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
۷. Collectionها
.NET مجموعهای استاندارد از Typeها را برای ذخیرهسازی و مدیریت Collectionهای Objectها فراهم میکند. این Typeها شامل Listهای قابل تغییر اندازه، Linked Listها، Dictionaryهای مرتب و نامرتب، و نیز Arrayها هستند. از میان اینها، فقط Arrayها بخشی از خود زبان C# هستند؛ سایر Collectionها صرفاً Classهایی هستند که مانند هر Class دیگری از آنها Instance میسازید.
Typeهای موجود در BCL داتنت برای Collectionها را میتوان به دستههای زیر تقسیم کرد:
- Interfaceهایی که Protocolهای استاندارد Collection را تعریف میکنند.
- Classهای آمادهبهاستفادهٔ Collection، مانند Listها و Dictionaryها.
- Base Classهایی برای نوشتن Collectionهای مخصوص یک Application.
این فصل هر یک از این دستهها را پوشش میدهد و علاوه بر آن، بخشی را به Typeهایی اختصاص میدهد که برای تعیین Equality و Order عناصر استفاده میشوند.
Namespaceهای مربوط به Collectionها عبارتاند از:
| Namespace | شامل |
|---|
System.Collections | Classها و Interfaceهای Collection غیرGeneric |
System.Collections.Specialized | Classهای Collection غیرGeneric با Type مشخص |
System.Collections.Generic | Classها و Interfaceهای Collection Generic |
System.Collections.ObjectModel | Proxyها و Baseها برای Collectionهای سفارشی |
System.Collections.Concurrent | Collectionهای Thread-safe؛ فصل ۲۳ را ببینید |
Enumeration
در محاسبات، انواع بسیار متفاوتی از Collectionها وجود دارند؛ از Data Structureهای سادهای مانند Arrayها یا Linked Listها گرفته تا ساختارهای پیچیدهتری مانند Red/Black Treeها و Hashtableها. با اینکه پیادهسازی داخلی و ویژگیهای بیرونی این Data Structureها تفاوت زیادی دارند، نیاز به پیمایش محتوای Collection تقریباً همگانی است. BCL داتنت این نیاز را با مجموعهای از Interfaceها — IEnumerable، IEnumerator و همتایان Generic آنها — پشتیبانی میکند تا Data Structureهای مختلف بتوانند یک API مشترک برای Traversal ارائه دهند. اینها بخشی از مجموعهٔ بزرگتر Interfaceهای Collection هستند که در شکل 7-1 نشان داده شدهاند.
شکل 7-1 — Interfaceهای CollectionIEnumerable و IEnumerator
Interface با نام IEnumerator Protocol سطحپایین و پایهای را تعریف میکند که عناصر یک Collection براساس آن بهشکل روبهجلو پیمایش یا Enumerate میشوند. تعریف آن چنین است:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
MoveNext عنصر جاری یا «Cursor» را به موقعیت بعدی میبرد و اگر عنصر دیگری در Collection وجود نداشته باشد، false برمیگرداند. Current عنصر موجود در موقعیت جاری را برمیگرداند؛ معمولاً این مقدار از object به Type مشخصتری Cast میشود. پیش از دریافت اولین عنصر باید MoveNext فراخوانی شود؛ این کار امکان خالیبودن Collection را فراهم میکند. Method با نام Reset، اگر پیادهسازی شده باشد، Cursor را به آغاز برمیگرداند تا Collection دوباره Enumerate شود. Reset عمدتاً برای سازگاری با Component Object Model یا COM وجود دارد؛ فراخوانی مستقیم آن معمولاً انجام نمیشود، زیرا در همهجا پشتیبانی نمیشود و معمولاً ساختن یک Enumerator جدید به همان اندازه آسان است.
Collectionها معمولاً خودشان Enumerator را پیادهسازی نمیکنند؛ در عوض، از طریق Interface با نام IEnumerable Enumerator فراهم میکنند:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
با تعریف یک Method واحد که Enumerator را برمیگرداند، IEnumerable انعطافپذیری ایجاد میکند، زیرا Logic مربوط به Iteration میتواند به Class دیگری واگذار شود. همچنین چند Consumer میتوانند همزمان و بدون دخالت در کار یکدیگر Collection را Enumerate کنند. میتوانید IEnumerable را چیزی شبیه «IEnumeratorProvider» در نظر بگیرید؛ این پایهایترین Interfaceای است که Classهای Collection پیادهسازی میکنند.
مثال زیر استفادهٔ سطحپایین از IEnumerable و IEnumerator را نشان میدهد:
string s = "Hello";
// Because string implements IEnumerable, we can call GetEnumerator():
IEnumerator rator = s.GetEnumerator();
while (rator.MoveNext())
{
char c = (char) rator.Current;
Console.Write (c + ".");
}
// Output: H.e.l.l.o.
بااینحال، بهندرت Methodهای Enumerator را مستقیماً به این شکل فراخوانی میکنیم، زیرا C# یک میانبُر نحوی فراهم کرده است: Statement با نام foreach. همان مثال با foreach چنین بازنویسی میشود:
string s = "Hello"; // The String class implements IEnumerable
foreach (char c in s)
Console.Write (c + ".");
IEnumerable<T> و IEnumerator<T>
IEnumerator و IEnumerable تقریباً همیشه همراه نسخههای Generic توسعهیافتهٔ خود پیادهسازی میشوند:
public interface IEnumerator<T> : IEnumerator, IDisposable
{
T Current { get; }
}
public interface IEnumerable<T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
با تعریف نسخهٔ Typed از Current و GetEnumerator، این Interfaceها Static Type Safety را تقویت میکنند، برای عناصر Value Type هزینهٔ Boxing را حذف میکنند و استفاده برای Consumer را سادهتر میسازند. Arrayها بهطور خودکار IEnumerable<T> را پیادهسازی میکنند؛ در اینجا T Type عضو Array است.
بهدلیل Static Type Safety بهتر، اگر Method زیر را با یک Array از Characterها فراخوانی کنید، Compile-time Error خواهید گرفت:
void Test (IEnumerable<int> numbers) { ... }
رویهٔ استاندارد برای Classهای Collection این است که IEnumerable<T> را بهشکل Public ارائه کنند و IEnumerable غیرGeneric را از طریق Explicit Interface Implementation «پنهان» کنند. به این ترتیب، اگر مستقیماً GetEnumerator() را فراخوانی کنید، IEnumerator<T> Generic و Type-safe دریافت میکنید. بااینحال، گاهی برای حفظ Backward Compatibility این قاعده شکسته میشود؛ Generics پیش از C# 2.0 وجود نداشت. Arrayها مثال خوبی هستند: آنها برای نشکستن کدهای قدیمی باید IEnumerator غیرGeneric — یا به بیان مؤدبانهتر، «Classic» — را برگردانند. برای گرفتن IEnumerator<T> Generic باید Cast کنید تا Interface صریح آشکار شود:
int[] data = { 1, 2, 3 };
var rator = ((IEnumerable <int>)data).GetEnumerator();
خوشبختانه بهلطف Statement با نام foreach، بهندرت لازم است چنین کدی بنویسید.
IEnumerable<T> و IDisposable
IEnumerator<T> از IDisposable ارث میبرد. بنابراین Enumeratorها میتوانند Referenceهایی به Resourceهایی مانند Database Connection نگه دارند و تضمین کنند که پس از پایان Enumeration — یا رهاشدن آن در میانهٔ راه — این Resourceها آزاد شوند. Statement با نام foreach این نکته را میشناسد و عبارت زیر را:
foreach (var element in somethingEnumerable) { ... }
از نظر منطقی به معادل زیر تبدیل میکند:
using (var rator = somethingEnumerable.GetEnumerator())
while (rator.MoveNext())
{
var element = rator.Current;
...
}
Block با نام using انجام Dispose را تضمین میکند؛ دربارهٔ IDisposable در فصل ۱۲ بیشتر توضیح میدهیم.
چه زمانی از Interfaceهای Nongeneric استفاده کنیم
با توجه به Type Safety بیشتر Interfaceهای Generic Collection مانند IEnumerable<T>، این سؤال پیش میآید: آیا اصلاً لازم است از IEnumerable غیرGeneric — یا ICollection یا IList — استفاده کنیم؟
در مورد IEnumerable، باید این Interface را همراه IEnumerable<T> پیادهسازی کنید، زیرا دومی از اولی مشتق میشود. بااینحال، بسیار بهندرت این Interfaceها را از صفر پیادهسازی میکنید؛ تقریباً در همهٔ موارد میتوانید از رویکرد سطحبالاتری مانند Iterator Methodها، Collection<T> و LINQ استفاده کنید.
اما از دید Consumer چطور؟ تقریباً در همهٔ موارد میتوانید کاملاً با Interfaceهای Generic کار کنید. بااینحال، Interfaceهای غیرGeneric گاهی هنوز مفیدند، چون برای Collectionهایی با هر Type عنصر، Type Unification فراهم میکنند. برای نمونه، Method زیر عناصر هر Collection را بهصورت بازگشتی میشمارد:
public static int Count (IEnumerable e)
{
int count = 0;
foreach (object element in e)
{
var subCollection = element as IEnumerable;
if (subCollection != null)
count += Count (subCollection);
else
count++;
}
return count;
}
از آنجا که C# برای Interfaceهای Generic قابلیت Covariance دارد، ممکن است به نظر برسد که این Method میتواند بهجای آن IEnumerable<object> بپذیرد. اما این کار برای عناصر Value Type و Collectionهای Legacy که IEnumerable<T> را پیادهسازی نمیکنند شکست میخورد؛ نمونهٔ آن ControlCollection در Windows Forms است.
در حاشیه، شاید متوجه یک Bug بالقوه در مثال شده باشید: Referenceهای Cyclic باعث Recursion بینهایت و Crash شدن Method میشوند. سادهترین اصلاح میتواند استفاده از HashSet باشد؛ بخش «HashSet<T> و SortedSet<T>» در صفحهٔ 392 کتاب را ببینید.
پیادهسازی Interfaceهای Enumeration
ممکن است به یک یا چند دلیل زیر بخواهید IEnumerable یا IEnumerable<T> را پیادهسازی کنید:
- برای پشتیبانی از Statement با نام
foreach. - برای Interoperate کردن با هر چیزی که انتظار یک Collection استاندارد دارد.
- برای برآوردهکردن نیازهای یک Interface پیشرفتهتر Collection.
- برای پشتیبانی از Collection Initializerها.
برای پیادهسازی IEnumerable/IEnumerable<T> باید یک Enumerator فراهم کنید. این کار را به یکی از سه روش میتوان انجام داد:
- اگر Class یک Collection دیگر را Wrap کرده است، Enumerator همان Collection داخلی را برگردانید.
- از Iterator و
yield return استفاده کنید. - از پیادهسازی
IEnumerator/IEnumerator<T> خودتان Instance بسازید.
همچنین میتوانید از یک Collection موجود Subclass بسازید: Collection<T> دقیقاً برای همین هدف طراحی شده است؛ بخش «Customizable Collections and Proxies» در صفحهٔ 401 را ببینید. راه دیگر استفاده از LINQ Query Operatorهاست که در فصل ۸ بررسی میشوند.
برگرداندن Enumerator یک Collection دیگر فقط مستلزم فراخوانی GetEnumerator روی Collection داخلی است. اما این روش تنها در سادهترین حالتها مناسب است؛ یعنی وقتی Itemهای Collection داخلی دقیقاً همان چیزی هستند که لازم دارید. رویکرد انعطافپذیرتر نوشتن یک Iterator با Statement با نام yield return در C# است. Iterator یک قابلیت زبان C# برای کمک به نوشتن Collectionهاست، همانطور که foreach به مصرف Collection کمک میکند. Iterator بهصورت خودکار پیادهسازی IEnumerable و IEnumerator — یا نسخههای Generic آنها — را مدیریت میکند. مثال ساده:
public class MyCollection : IEnumerable
{
int[] data = { 1, 2, 3 };
public IEnumerator GetEnumerator()
{
foreach (int i in data)
yield return i;
}
}
به «جادوی سیاه» دقت کنید: ظاهراً GetEnumerator اصلاً Enumerator برنمیگرداند! Compiler هنگام دیدن yield return پشت صحنه یک Enumerator Class تودرتو و پنهان مینویسد و سپس GetEnumerator را Refactor میکند تا از آن Class Instance بسازد و آن را برگرداند. Iteratorها هم قدرتمندند و هم ساده، و در پیادهسازی Standard Query Operatorهای LINQ-to-Objects بهطور گسترده استفاده میشوند.
با ادامهٔ همین رویکرد، میتوانیم Interface Generic با نام IEnumerable<T> را نیز پیادهسازی کنیم:
public class MyGenCollection : IEnumerable<int>
{
int[] data = { 1, 2, 3 };
public IEnumerator<int> GetEnumerator()
{
foreach (int i in data)
yield return i;
}
// Explicit implementation keeps it hidden:
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
چون IEnumerable<T> از IEnumerable ارث میبرد، باید هر دو نسخهٔ Generic و Nongeneric از GetEnumerator را پیادهسازی کنیم. مطابق رویهٔ استاندارد، نسخهٔ Nongeneric را بهصورت Explicit پیادهسازی کردهایم. این نسخه میتواند صرفاً GetEnumerator Generic را فراخوانی کند، چون IEnumerator<T> از IEnumerator ارث میبرد.
Classی که نوشتیم میتواند پایهای برای یک Collection پیچیدهتر باشد. اما اگر چیزی بیش از پیادهسازی سادهٔ IEnumerable<T> لازم نداریم، yield return راه سادهتری میدهد. بهجای نوشتن Class، Logic مربوط به Iteration را داخل Methodی قرار دهید که IEnumerable<T> Generic برمیگرداند و باقی کار را به Compiler بسپارید:
public static IEnumerable <int> GetSomeIntegers()
{
yield return 1;
yield return 2;
yield return 3;
}
استفاده:
foreach (int i in Test.GetSomeIntegers())
Console.WriteLine (i);
آخرین روش برای نوشتن GetEnumerator، نوشتن Classی است که مستقیماً IEnumerator را پیادهسازی کند. این دقیقاً همان کاری است که Compiler برای Resolve کردن Iteratorها در پشت صحنه انجام میدهد. خوشبختانه بهندرت لازم است خودتان تا این حد پایین بروید. مثال زیر Collectionی تعریف میکند که بهصورت Hard-coded شامل اعداد 1، 2 و 3 است:
public class MyIntList : IEnumerable
{
int[] data = { 1, 2, 3 };
public IEnumerator GetEnumerator() => new Enumerator (this);
class Enumerator : IEnumerator // Define an inner class
{ // for the enumerator.
MyIntList collection;
int currentIndex = -1;
public Enumerator (MyIntList items) => this.collection = items;
public object Current
{
get
{
if (currentIndex == -1)
throw new InvalidOperationException ("Enumeration not started!");
if (currentIndex == collection.data.Length)
throw new InvalidOperationException ("Past end of list!");
return collection.data [currentIndex];
}
}
public bool MoveNext()
{
if (currentIndex >= collection.data.Length - 1) return false;
return ++currentIndex < collection.data.Length;
}
public void Reset() => currentIndex = -1;
}
}
پیادهسازی Reset اختیاری است؛ میتوانید بهجای آن NotSupportedException پرتاب کنید.
توجه کنید نخستین فراخوانی MoveNext باید به اولین Item فهرست برود، نه دومین Item.
برای رسیدن به قابلیتهایی همسطح یک Iterator، باید IEnumerator<T> را نیز پیادهسازی کنیم. مثال زیر برای اختصار Bounds Checking را حذف کرده است:
class MyIntList : IEnumerable<int>
{
int[] data = { 1, 2, 3 };
// The generic enumerator is compatible with both IEnumerable and
// IEnumerable<T>. We implement the nongeneric GetEnumerator method
// explicitly to avoid a naming conflict.
public IEnumerator<int> GetEnumerator() => new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
class Enumerator : IEnumerator<int>
{
int currentIndex = -1;
MyIntList collection;
public Enumerator (MyIntList items) => collection = items;
public int Current => collection.data [currentIndex];
object IEnumerator.Current => Current;
public bool MoveNext() => ++currentIndex < collection.data.Length;
public void Reset() => currentIndex = -1;
// Given we don't need a Dispose method, it's good practice to
// implement it explicitly, so it's hidden from the public interface.
void IDisposable.Dispose() {}
}
}
مثال Generic سریعتر است، زیرا IEnumerator<int>.Current نیاز ندارد int را به object Cast کند و در نتیجه هزینهٔ Boxing را حذف میکند.
Interfaceهای ICollection و IList
Interfaceهای Enumeration Protocolی برای Iteration روبهجلو روی Collection فراهم میکنند، اما راهی برای تعیین اندازهٔ Collection، دسترسی به Member براساس Index یا تغییر Collection نمیدهند. برای این قابلیتها، .NET Interfaceهای ICollection، IList و IDictionary را تعریف میکند. هرکدام نسخهٔ Generic و Nongeneric دارند؛ بااینحال، نسخههای Nongeneric عمدتاً برای پشتیبانی Legacy باقی ماندهاند.
شکل 7-1 سلسلهمراتب ارثبری این Interfaceها را نشان داد. سادهترین خلاصهٔ آنها چنین است:
IEnumerable<T> و IEnumerable- حداقل قابلیتها را فراهم میکنند؛ فقط Enumeration.
ICollection<T> و ICollection- قابلیتهای میانی را فراهم میکنند؛ برای مثال Property با نام
Count. IList<T>/IDictionary<K,V> و نسخههای Nongeneric آنها- بیشترین قابلیتها را فراهم میکنند؛ از جمله دسترسی «Random» براساس Index یا Key.
بهندرت لازم است خودتان هر یک از این Interfaceها را پیادهسازی کنید. تقریباً همیشه وقتی نیاز دارید یک Collection Class بنویسید، میتوانید بهجای آن از Collection<T> Subclass بسازید؛ بخش «Customizable Collections and Proxies» در صفحهٔ 401 را ببینید. LINQ هم گزینهٔ دیگری است که بسیاری از سناریوها را پوشش میدهد.
نسخههای Generic و Nongeneric تفاوتهایی فراتر از انتظار دارند، بهویژه در مورد ICollection. دلیل عمدتاً تاریخی است: چون Generics دیرتر اضافه شدند، Interfaceهای Generic با تجربهٔ بیشتر و انتخاب Memberهای متفاوت — و بهتر — طراحی شدند. به همین دلیل ICollection<T> از ICollection، IList<T> از IList و IDictionary<TKey,TValue> از IDictionary ارث نمیبرند. البته خود یک Collection Class در صورت سودمندبودن میتواند هر دو نسخهٔ Interface را پیادهسازی کند؛ و اغلب چنین میکند.
دلیل ظریف دیگری برای اینکه IList<T> از IList ارث نمیبرد این است که Cast کردن به IList<T> در آن صورت Interfaceای با هر دو Member با نامهای Add(T) و Add(object) برمیگرداند. این موضوع عملاً Static Type Safety را از بین میبرد، زیرا میشد Add را با Objectی از هر Type فراخوانی کرد.
این بخش ICollection<T> و IList<T> و نسخههای Nongeneric آنها را پوشش میدهد؛ Interfaceهای Dictionary در بخش «Dictionaries» صفحهٔ 394 کتاب بررسی میشوند.
در کتابخانههای .NET منطق یکدستی برای کاربرد واژههای «Collection» و «List» وجود ندارد. برای مثال، چون IList<T> نسخهای دارای قابلیت بیشتر از ICollection<T> است، ممکن است انتظار داشته باشید Class با نام List<T> هم به همان نسبت از Class با نام Collection<T> قابلیت بیشتری داشته باشد. چنین نیست. بهتر است این دو واژه را بهطور کلی مترادف در نظر بگیرید، مگر زمانی که Type مشخصی مورد بحث است.
ICollection<T> و ICollection
ICollection<T> Interface استاندارد برای Collectionهای قابلشمارش از Objectهاست. این Interface امکان تعیین اندازهٔ Collection با Count، بررسی وجود یک Item با Contains، کپی Collection به یک Array با CopyTo و تعیین Read-only بودن Collection با IsReadOnly را فراهم میکند. برای Collectionهای قابل نوشتن، میتوانید با Add، Remove و Clear Itemها را نیز تغییر دهید. و چون از IEnumerable<T> توسعه مییابد، با Statement با نام foreach هم قابل پیمایش است:
public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
int Count { get; }
bool Contains (T item);
void CopyTo (T[] array, int arrayIndex);
bool IsReadOnly { get; }
void Add(T item);
bool Remove (T item);
void Clear();
}
ICollection غیرGeneric نیز Collectionی قابلشمارش فراهم میکند، اما قابلیت تغییر List یا بررسی عضویت عنصر را ندارد:
public interface ICollection : IEnumerable
{
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
void CopyTo (Array array, int index);
}
Interface غیرGeneric همچنین Propertyهایی برای کمک به Synchronization تعریف میکند؛ فصل ۱۴ را ببینید. این Propertyها در نسخهٔ Generic کنار گذاشته شدند، زیرا Thread Safety دیگر ویژگی ذاتی یک Collection محسوب نمیشود.
پیادهسازی هر دو Interface نسبتاً ساده است. اگر ICollection<T> را بهصورت Read-only پیادهسازی میکنید، Methodهای Add، Remove و Clear باید NotSupportedException پرتاب کنند.
این Interfaceها معمولاً همراه IList یا IDictionary پیادهسازی میشوند.
IList<T> و IList
IList<T> Interface استاندارد برای Collectionهایی است که براساس Position قابل Index هستند. علاوه بر قابلیتهای بهارثرسیده از ICollection<T> و IEnumerable<T>، امکان خواندن یا نوشتن عنصر براساس Position از طریق Indexer و نیز Insert/Remove براساس Position را فراهم میکند:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this [int index] { get; set; }
int IndexOf (T item);
void Insert (int index, T item);
void RemoveAt (int index);
}
Methodهای IndexOf یک Linear Search روی List انجام میدهند و اگر Item مورد نظر پیدا نشود، −1 برمیگردانند.
نسخهٔ Nongeneric با نام IList Memberهای بیشتری دارد، چون از ICollection قابلیت کمتری به ارث میبرد:
public interface IList : ICollection, IEnumerable
{
object this [int index] { get; set }
bool IsFixedSize { get; }
bool IsReadOnly { get; }
int Add (object value);
void Clear();
bool Contains (object value);
int IndexOf (object value);
void Insert (int index, object value);
void Remove (object value);
void RemoveAt (int index);
}
Method با نام Add روی Interface غیرGeneric با نام IList یک Integer برمیگرداند؛ این Integer همان Index مربوط به Item تازهاضافهشده است. در مقابل، Add روی ICollection<T> Return Type از نوع void دارد.
Class عمومی List<T> نمونهٔ شاخص پیادهسازی هم IList<T> و هم IList است. Arrayهای C# نیز هر دو IList Generic و Nongeneric را پیادهسازی میکنند، هرچند Methodهای افزودن یا حذف Element از طریق Explicit Interface Implementation پنهان شدهاند و در صورت فراخوانی NotSupportedException پرتاب میکنند.
اگر تلاش کنید از طریق Indexer مربوط به IList به یک Array چندبعدی دسترسی پیدا کنید، ArgumentException پرتاب میشود. این نکته هنگام نوشتن Methodهایی مانند نمونهٔ زیر یک دام است:
public object FirstOrNull (IList list)
{
if (list == null || list.Count == 0) return null;
return list[0];
}
ممکن است این کد کاملاً مقاوم به نظر برسد، اما اگر با یک Array چندبعدی فراخوانی شود Exception میدهد. در Runtime میتوانید با عبارت زیر چندبعدیبودن Array را بررسی کنید؛ در فصل ۱۹ بیشتر توضیح داده میشود:
list.GetType().IsArray && list.GetType().GetArrayRank()>1
IReadOnlyCollection<T> و IReadOnlyList<T>
.NET همچنین Interfaceهای Collection و Listای تعریف میکند که فقط Memberهای لازم برای عملیات Read-only را در معرض قرار میدهند:
public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
{
int Count { get; }
}
public interface IReadOnlyList<out T> : IReadOnlyCollection<T>,
IEnumerable<T>, IEnumerable
{
T this[int index] { get; }
}
چون Type Parameter در این Interfaceها فقط در Output Position استفاده میشود، با out بهصورت Covariant علامتگذاری شده است. بنابراین برای مثال میتوان Listی از Catها را بهصورت یک Read-only List از Animalها در نظر گرفت. در مقابل، T در ICollection<T> و IList<T> Covariant نیست، چون هم در Input Position و هم در Output Position استفاده میشود.
این Interfaceها یک View فقطخواندنی از Collection یا List ارائه میکنند؛ پیادهسازی زیرین همچنان ممکن است قابلنوشتن باشد. بیشتر Collectionهای Writable یا Mutable هم Interfaceهای Read-only و هم Interfaceهای Read/Write را پیادهسازی میکنند.
علاوه بر اینکه Interfaceهای Read-only اجازه میدهند با Collectionها بهشکل Covariant کار کنید، به یک Class امکان میدهند View فقطخواندنی از یک Collection خصوصی و قابلنوشتن را بهصورت Public ارائه کند. این موضوع — همراه با راهحل بهتر — در بخش ReadOnlyCollection<T> در صفحهٔ 406 نشان داده میشود.
IReadOnlyList<T> به Type با نام IVectorView<T> در Windows Runtime نگاشت میشود.