IEnumerable, ICollection and IReadOnlyList
Choosing the weakest collection interface that does the job, and knowing exactly what each one promises a caller.
static int Total(IEnumerable<int> values)An interface is a list of members a type promises to provide, written down with no code behind it. A type that provides them can be used anywhere that interface is asked for. You write your own later in the course; here you only need the ones the built-in collections already provide.
What each one promises
| Interface | Promises |
|---|---|
IEnumerable<T> | You can walk through the items, forwards, with foreach. Nothing else: no count, no positions. |
IReadOnlyCollection<T> | Everything above, plus Count. |
IReadOnlyList<T> | Everything above, plus reading by position with items[i]. |
ICollection<T> | Count, Add, Remove, Contains, Clear and IsReadOnly. |
List<T> provides all four, so one list can be passed as any of them.
The rule for parameters
Ask for the weakest interface that does the job. A method that only reads every item should take IEnumerable<T>, because then an array, a List<T>, a HashSet<T> and a dictionary's Values all fit it with no conversion. Ask for ICollection<T> only when you really do add or remove.
The rule for return types
Return the weakest type the caller actually needs. IReadOnlyList<T> is the usual answer for a finished sequence: the caller gets Count and positions, and no way to change your collection. Returning your internal List<T> hands callers the ability to edit it.
Where it goes wrong
IReadOnlyList<T> means the caller cannot change the collection through that reference. It does not mean the data cannot change: whoever holds the original list can still add to it, and the caller sees that.
Example
List<string> names = new List<string> { "ana", "bo", "cy" };
// Describe only reads, so it asks for the weakest thing it can read:
// IEnumerable<string>. A List and an array both satisfy it.
Console.WriteLine(Describe(names));
Console.WriteLine(Describe(new[] { "dee" }));
// Shout returns IReadOnlyList<string>: Count and positions, no Add or Remove.
IReadOnlyList<string> loud = Shout(names);
Console.WriteLine($"first: {loud[0]}, count: {loud.Count}");
static string Describe(IEnumerable<string> items)
{
int count = 0;
foreach (string item in items)
{
count++;
}
return $"{count} item(s)";
}
static IReadOnlyList<string> Shout(IEnumerable<string> items)
{
List<string> result = new List<string>();
foreach (string item in items)
{
result.Add(item.ToUpperInvariant());
}
return result; // a List<T> is an IReadOnlyList<T>, so no copy is needed
}
// Output:
// 3 item(s)
// 1 item(s)
// first: ANA, count: 3When to use it
- Writing a method that sums or formats items so it accepts an array, a List and a dictionary's Values unchanged.
- Returning a finished result set as IReadOnlyList<T> so callers cannot add to the collection you kept.
- Deciding whether a parameter really needs Add, which is usually the sign that it should be a return value instead.
More examples
Reading by position, adding, and the array that cannot
This is the concrete reason for asking for the weakest interface. ICollection<T> promises that an Add method exists, not that it works, so a parameter typed that way quietly accepts arrays that will throw.
int[] numbers = { 3, 1, 2 };
List<int> list = new List<int> { 3, 1, 2 };
// IReadOnlyList<T> is the weakest interface that gives positions.
Console.WriteLine(Second(numbers));
Console.WriteLine(Second(list));
// ICollection<T> is the weakest one that allows adding.
Fill(list, 4);
Console.WriteLine($"list count: {list.Count}");
// An array provides ICollection<T>, but reports that it is read-only, and
// its Add throws NotSupportedException. Check IsReadOnly before trusting Add.
Console.WriteLine($"array is read-only: {((ICollection<int>)numbers).IsReadOnly}");
static int Second(IReadOnlyList<int> values)
{
return values[1];
}
static void Fill(ICollection<int> target, int value)
{
target.Add(value);
}
// Output:
// 1
// 1
// list count: 4
// array is read-only: TrueOne parameter, many callers
Had Total asked for List<int>, both calls would need a copy at the call site. Taking IEnumerable<T> costs nothing and removes those copies.
Dictionary<string, int> stock = new Dictionary<string, int>
{
{ "apple", 4 },
{ "pear", 7 }
};
// Values is a collection of ints. Because Total asks only for
// IEnumerable<int>, the same method also takes a List or an array.
Console.WriteLine($"total: {Total(stock.Values)}");
Console.WriteLine($"total: {Total(new int[] { 1, 2 })}");
static int Total(IEnumerable<int> values)
{
int sum = 0;
foreach (int value in values)
{
sum += value;
}
return sum;
}
// Output:
// total: 11
// total: 3AsReadOnly gives a view, not a copy
Use this when you want to hand out a list that callers must not edit. Remember what it does not give you: if the caller needs a frozen snapshot, copy the items instead, with ToArray or a new List.
List<string> names = new List<string> { "ana", "bo" };
// AsReadOnly wraps the same list. The caller cannot add through the wrapper,
// but changes to the original list do show up in it.
IReadOnlyList<string> view = names.AsReadOnly();
Console.WriteLine($"view count before: {view.Count}");
names.Add("cy");
Console.WriteLine($"view count after: {view.Count}");
Console.WriteLine($"last: {view[view.Count - 1]}");
// Output:
// view count before: 2
// view count after: 3
// last: cy
Discussion