4

I have read this question: IEnumerable vs IReadonlyCollection vs ReadonlyCollection for exposing a list member and this question: ReadOnlyCollection or IEnumerable for exposing member collections?. I am concious that these answers were written years ago.

The answer by Jon Skeet suggests that I should be using a IReadOnlyCollection rather than an IEnumerable as I am very cautious to prevent code like this:

ICollection<Order> evil = (ICollection<Order>)customer.Orders;
evil.Add(order3);

Now see the code below:

public class Customer
{
    private readonly List<Order> _orders = new List<Order>();

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public IEnumerable<Order> Orders
    {
        get { foreach (var order in _orders) yield return order; }
    }

    internal void AddOrder(Order order)
    {
        _orders.Add(order);
    }

    public void AddOrders(IEnumerable<Order> orders)
    {
        this._orders.AddRange(orders);
    }
}

I can use the code like this:

Order order1 = new Order();
Order order2 = new Order();
Order order3 = new Order();

Customer customer = new Customer();
customer.FirstName = "Fred";
customer.LastName = "Bloggs";
customer.AddOrder(order1);
customer.AddOrder(order2);

and like this:

Customer customer2 = new Customer();
customer2.FirstName = "Average";
customer2.LastName = "Joe";

List<Order> orders = new List<Order>();
Order order4 = new Order();
Order order5 = new Order();
orders.Add(order4);
orders.Add(order5);
customer2.AddOrders(orders);

Are there any risks using this code? I am trying to make it as water tight as possible as it is part of a public api. Is this more water tight than an IReadOnlyCollection? Is it less efficient? Please note that this is the final version of the code so there are no other scenarios to consider.

The concerns I have are:

  1. Exposing an IEnumerable instead of a readonly list.

  2. Using a List to hold the orders privately.

I am trying to follow the advice described here as best I can: https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/guidelines-for-collections.

Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
w0051977
  • 15,099
  • 32
  • 152
  • 329

2 Answers2

2

Is this more water tight than an IReadOnlyCollection?

I don't think so. I would say they serve the same purpose in another way.

Are there any risks using this code? Is it less efficient?

Risks? Not that I am aware of. Since the yield return compiles to an internal state machine, you lose the direct exposure of the list. That is a benefit of your approach, just like with a read-only collection.

Your code does have a performance impact though: getting the count and other relevant properties have a complexity of O(n) instead of O(1). This might hurt your performance.

Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
  • Could you clarify what you mean by 0(n) and 0(1)? I guess you would choose an IReadOnlyCollection in my specific case as Evk did? Thanks. – w0051977 Apr 16 '18 at 13:19
  • Yes, as I said, use `IReadOnlyCollection`. And `O(n)` is called the Big O notation. [Read more.](https://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions) – Patrick Hofman Apr 16 '18 at 13:22
  • for Big O notation. +1 – w0051977 Apr 16 '18 at 13:46
1

I'd say just do:

public IReadOnlyList<Order> Orders => _orders.AsReadOnly();

It wraps your List into read-only wrapper, so "evil" code like this:

ICollection<Order> evil = (ICollection<Order>)customer.Orders;
evil.Add(order3);

Will just throw an exception that colleciton is read-only. Using IEnumerable has both performance and usability implications for this scenario, without providing much benefits (at all I'd say).

For example I guess it's a common operation to get a number of customer orders. If you return IEnumerable - that operation is both less perfomant compared to IReadOnlyList and less convenient for the caller.

Less perfomant is because with IEnumerable the only way to get count of orders is Count() LINQ method, and this method, in your implementation with yield return, will have to go through the whole collection and count how much items are there. With IReadOnlyList there is Count property which just contains that information.

Less convenient, because multiple enumerations of single enumerable is discouraged and VS will warn user of your api about that. Suppose user of your api has code like this:

IEnumerable<Order> orders = customer2.Orders;
// just an example, there is actually no need to check for count
// before enumerating
if (orders.Count() > 0) {
    foreach (var order in orders) {

    }
}

On foreach line, VS will warn him about "Possible multiple enumerations of IEnumerable". Only reasonable thing caller can do with IEnumerable is, well, enumerate it. Once. So to avoid this warning, he will have to write some ugly code, or do var orders = customer.Orders.ToList(), creating unnecessary copy of your list and working with that.

There is no reason to limit user of your api like that, unless enumerating just once is the only correct way of using that api.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • @w0051977 `_orders` is still `List` like you have now, so you use `AddRange` as usual. Just instead of `IEnumerable` you now return `IReadOnlyCollection` as shown in answer. – Evk Apr 16 '18 at 12:54
  • Could you also confirm what the "performance and usability implications " you are referring to are? Thanks. – w0051977 Apr 16 '18 at 13:29
  • @w0051977 An `IEnumerable` has no information whatsoever of how many items it might contain, this is one of the compromises of its laziness. In order to know how many items your enumeration has you *need to enumerate it*; in other words, you need to count the items contained within, and unless you cache the result, you'll need to count the items *every* time. An `IList` doesn't have this problem because it always knows how many items it contains. – InBetween Apr 16 '18 at 13:35
  • @w0051977 I've expanded a bit on that. – Evk Apr 16 '18 at 13:42
  • One more thing - would I pass an IEnumerable to AddOrders (as I have done in the question)? Thanks. – w0051977 Apr 16 '18 at 14:09
  • @w0051977 yes there it's fine. All you need to do with `orders` inside `AddOrders` is to indeed enumerate them once and add each to your internal list collection. So here `IEnumerable` fits well. – Evk Apr 16 '18 at 14:12
  • One more thing. Do you think a List is the right choice for the private property? Other options include: Dictionary and Hashset. Please note that the class is complete - no more methods/properties will be added. – w0051977 Apr 16 '18 at 16:05
  • @w0051977 only you can answer that, since those structures are quite different. Well, `Dictionary` maybe is not a good fit here, but `HashSet` and `List` are on equal grounds. Without additional information, I'd choose `List`. – Evk Apr 16 '18 at 16:11
  • One more thing (I promise). Would you ever choose a collection type of HashSet/Dictionary just to stop other developers adding duplicates to the HashSet by accident? – w0051977 Apr 17 '18 at 14:18
  • @w0051977 just for this - I doubt. You can just as well check if item already exists in a List and throw an exception if it does. If duplicates are not allowed - you should do this (throw exception) with both list and set. Now, checking if item exists is "faster" in set than in list (usually irrelevant unless you perform a lot of such searches on long lists). But set is unoreded collection, so if order is relevant - it cannot be used. And so on. – Evk Apr 17 '18 at 14:27