15

There are a couple of similar questions already on SO, but none of the ones I found really touches on this particular topic, so here it goes...

My understanding is that one should always attempt to return an interface over a concrete class. Won't go into the reasons behind it, plenty of things on SO about that already.

However in the case of an IReadOnlyCollection vs a ReadOnlyCollection I'm not sure if that rule should be followed.

An IReadOnlyCollection can be easily cast into a List, which... well... breaks the ReadOnly aspect that the contract promises.

ReadOnlyCollection however cannot be cast into a List, but it means returning a concrete class.

In the long run, does it actually matter? It seems to me like in most cases a ReadOnly*/IReadOnly* object is only returned returned by either a method or a read-only property.

So even if the user decides to cast it to something else (in the case of a IReadOnly* object) or use LINQ to create a collection of some kind out of it (in the case of ReadOnly* object), there's really no way that the class exposing the ReadOnly*/IReadOnly* object is going to accept that back.

So what's the recommendation here, return an IReadOnly* interface or a concrete ReadOnly* class instance?

cogumel0
  • 2,430
  • 5
  • 29
  • 45
  • What are you _afraid_ of happening when you promise to return an `IReadOnlyCollection` and return an `List`? – CodeCaster Jul 18 '17 at 11:32
  • The "cast" issue is impractical...For example LINQ returns tons of `IEnumerable` but everyone calls `ToList` to get a list representation. That's the caller's fault to cast it, the method promised an `IReadOnlyCollection` only. A naughty caller can even use reflections to hack the internal content of an `IReadOnlyCollection`, but that's their problem. – joe Feb 22 '21 at 03:21

3 Answers3

14

IReadOnlyCollection<T> can only be cast to List<T> if the underlying object is of that type. ReadOnlyCollection<T> for example also implements IReadOnlyCollection<T>.

So my recommendation, return IReadOnlyCollection<T> and if you are worried that caller would wrongly cast it to something it shouldn't, make sure the underlying type is ReadOnlyCollection<T>

public IReadOnlyCollection<User> GetUsers()
{
   return new ReadOnlyCollection<User>();
}


But returning IReadOnlyCollection<T> should be enough for caller of function to understand it is supposed to be read only.
Note that you can never completely secure your code with a ReadOnlyCollection<T>, caller can still use reflection to access the internal list and manipulate it.
The only option in that case would be to create a copy if the list and return that.

Magnus
  • 45,362
  • 8
  • 80
  • 118
  • But won't the caller assume that since `List` implements `IReadOnlyCollection` they can just cast it to `List`? There's nothing in an `IReadOnlyCollection` explicitly stating that they can't cast to `List` or that the object is immutable. Returning `ReadOnlyCollection` though is slightly different. – cogumel0 Jul 18 '17 at 11:52
  • 5
    `But won't the caller assume that since List implements IReadOnlyCollection they can just cast it to List` Why would they assume that? _`SqlConnection` implements `IDisposable`, but I don't assume that every `IDisposable` thing I receive can be cast `to SqlConnection`._ – mjwills Jul 18 '17 at 12:23
  • @mjwills is right. You would either need to investigate the source code of the function you are calling or using debugger check the type of the object you get back to know if you can cast it to `List`. So I would say the caller absolutely would not assume he would be able to cast the `IReadOnlyCollection` to `List`. – Magnus Jul 18 '17 at 12:34
  • @mjwills, you're correct. But you assume that all `IDisposible` things you receive are disposable objects (never mind what methods they implement) because the name says so. With an `IReadOnlyCollection` that's not the case. The name implies that whatever class implements this interface will result in some form of an object that's read only. But that's not true. I think my biggest gripe here is that the interface name (and it's MSDN description) does not properly reflect what it does or is then being used for. – cogumel0 Jul 18 '17 at 13:19
  • 1
    `The name implies that whatever class implements this interface will result in some form of an object that's read only.` That is one possible reading, yes. https://stackoverflow.com/a/15262988/34092 is another way of reading it though (it talks about `IReadOnlyList`, but the principle is the same). – mjwills Jul 18 '17 at 13:21
  • @mjwills nice link and a good interpretation, but it's also good to know I'm not the only one interpreting it this way and having an issue with the interface name (as per other comments and answers in the link you provided). Anyway, I'll let it rest, it seems like this has been debated before and I got my answer. – cogumel0 Jul 18 '17 at 13:26
9

You definitely should attempt to make your public methods return interfaces.

If you're afraid that your class's callers are going to cast and modify your internal structures, such as in this example, where a class's internal queue shouldn't be touched from the outside:

public class QueueThing
{
    private List<QueueItem> _cantTouchThis;

    public IReadOnlyCollection<QueueItem> GetQueue()
    {
        return _cantTouchThis;
    }
}

Then you could use AsReadOnly() to return a new ReadOnlyList<T>, sourced from the private List<T>:

public class QueueThing
{
    private List<QueueItem> _cantTouchThis;

    public IReadOnlyCollection<QueueItem> GetQueue()
    {
        return _cantTouchThis.AsReadOnly();
    }
}

Now the caller can cast the returned value all they want, they won't be able to modify the _cantTouchThis member (except of course when they're going to use reflection, but then all bets are off anyway).

Given many types can implement an interface, a user of such a method should definitely not assume that it's safe to cast the return value of the method to any concrete type.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
  • Right, but the question is whether to return `IReadOnlyCollection` or `ReadOnlyCollection`. You're returning a `IReadOnlyCollection` – cogumel0 Jul 18 '17 at 11:43
  • Your question is about the caller casting the returned value. I asked why you were concerned about that, and given you didn't respond, I assumed something. While this answer may not apply to your scenario, it may apply to others having the same concerns. – CodeCaster Jul 18 '17 at 11:44
  • Your assumption is correct, the example is perfect. Read the comment I added to @Magnus' answer to understand what I meant. – cogumel0 Jul 18 '17 at 11:52
  • So, did you read the part _"Then you could use AsReadOnly() to return a new ReadOnlyList"_ in my answer? If so, I don't understand what you don't understand. :) – CodeCaster Jul 18 '17 at 11:56
  • The caller is getting an `IReadOnlyCollection` back. Since `List` implements `IReadOnlyCollection` I believe it's fair for the caller to assume he can cast said `IReadOnlyCollection` to `List`, which, in this case, would fail. But returning a concrete type, i.e., `ReadOnlyCollection` makes it clear enough that casting to a `List` is not possible. – cogumel0 Jul 18 '17 at 11:59
  • No, it makes no sense to do so. Plenty of collections (either from the BCL or user-implemented) implement that interface, it's not a valid assumption you can cast any interface to any concrete type. That's not what interfaces are for. – CodeCaster Jul 18 '17 at 12:00
  • While this answer pretty much says the same as Magnus' answer does (and therefore both answers are correct), its examples are slightly clearer (with the usage of private fields) and it is more directly related to the question. I am therefore marking this one as the correct answer. – cogumel0 Jul 18 '17 at 12:14
4

Microsoft's guidelines here state:

✓ DO use ReadOnlyCollection, a subclass of ReadOnlyCollection<T>, or in rare cases IEnumerable<T> for properties or return values representing read-only collections.

So, basically, you should return ReadOnlyCollection<T>. It specifies interface IEnumerable in other cases, so if it intended interface IReadOnlyCollection<T>, it would have stated so.

Neo
  • 4,145
  • 6
  • 53
  • 76
  • 2
    The page referenced is a reprint of a 2008 book, but the interfaces didn't exist until 2012 as part of .NET 4.5. An Issue has been created to make this clear: https://github.com/dotnet/docs/issues/26464 – Steve Dunn Oct 25 '21 at 06:46