Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

When would one not want to return an interface?

+11
−0

Consider the following method as an example:

List<int> Foo() {
    // ...
}

Are there any disadvantages of returning an interface instead of a concrete implementation in C#?

IList<int> Foo() {
    // ...
}

From the caller's perspective, it doesn't really matter what the type is as long as it has the correct methods and behaviors (That's the point of interfaces, after all). Taken another way, if I in the future want to change what I return[1], the IList implementation seems superior in that I don't have to change the return type (as long as my new return value also implements IList). Why then would I prefer to return by concrete type?


  1. Setting aside considerations such as API changes; you can assume this is an internal method not consumed by anyone else. ↩︎

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

3 answers

+9
−0

From simple subtyping concerns, you want the arguments of your methods to be as abstract/imprecise as possible while still allowing you to efficiently accomplish your goals. This allows your method to be used in the broadest range possible, and it also makes it clear what exactly your method depends upon. Dually, you want the return type of your methods to be as concrete/precise as possible while not providing any guarantees you aren't willing to ensure over time. Again, this allows your method to be used in the broadest range possible. It also communicates which guarantees you are providing.

For your example, the only reason to return an IList<int> rather than a List<int> is if you want to reserve the right to return some other implementation of IList<int> in the future. The disadvantage of returning IList<int> rather than List<int> is it gives less information and fewer guarantees to the consumer. The consumer can't exploit any List-specific methods or other functions that consume specifically Lists and not ILists in general. They also lose guarantees in behavior. There are very many possible implementations of IList that observably don't behave like a List. In practice, I think it is pretty common that there's no realistic expectation that we'd change the return type for a method especially for more data-like objects as opposed to service-like objects.


I wrote the remainder before I decided to take the tack articulated in the above paragraphs. I think the above paragraphs make the case on their own, so reading the following paragraphs isn't necessary, but I didn't feel like just throwing them away. They are essentially more examples of the importance and value of precise types and the guarantees they provide.


Structural types like sum and product (i.e. tuple) types tend to have particularly important guarantees and looser interfaces will lead to the loss of our ability to rely on these guarantees. Values of structural types are arguably "generic" data-like objects.

Starting with (immutable) tuples and specifically pairs first, the idea of a pair type like (A, B) is that its values contain exactly a value of type A and a value of type B. For the purposes of consuming a tuple type, an interface with getA and getB methods would suffice (ignoring potential side-effects), but such an interface doesn't preclude a getC, etc. If we don't ignore side-effects, then the interface isn't even adequate for consumption. The point being that the pair type communicates that all of the relevant information is contained in the two components. This is represented by a (weakened[1]) universal property of the form: (p.getA(), p.getB()) = p, (a, b).getA() = a, and (a, b).getB() = b where the equalities are value equalities, though the latter two should be true even at the level of object identity. The first equation really drives home the fact that the components of a pair contain all the information of that pair. When you're transforming these pairs and not just reading out their components, these universal properties matter.

The above is why types like Point2D representing a point on a plane and data transfer objects (DTOs) and, more generally, value objects are often passed around directly. Before moving on to sums, I'll say that the above does not preclude using an interface conceptually. There are differing implementations of pairs, especially for concrete cases like Point2D which we may represent as polar or cartesian coordinates. Theoretically, the implementations would nevertheless be isomorphic and thus not differ in any observable behavior, e.g. they may have different performance characteristics but they always behave the same. Unfortunately, C#, like most languages, lacks a way to express, let alone enforce, contracts on interfaces that would guarantee this.

Sum types are dual to product types and thus have dual concerns. See below for one possible encoding of a 2-variant sum type called Either (following the name Haskell uses). The concerns are arguably much sharper in this case. The universal property in this case (slightly simplifying the notation) is: new Left(a).Match<C>(a => x, b => y) = x, new Right(b).Match<C>(a => x, b => y) = y, and e.Match<Either<A, B>>(a => new Left(a), b => new Right(b)) = e. One of the key benefits of (closed) sum types is that they are exhaustive. Our Either type has two cases, Left and Right, and once we've specified what to do in those two cases, then we've covered all cases. If Either was an interface that allowed other implementations, we would not have this guarantee. Further, sum types allow us to decrease precision. Clearly, a type that could be either A or B is less precise than one that is always A. To this end, it can sometimes be beneficial to have a return type like Either<A, B>.Right rather than just Either<A, B> as that provides more precision. Consumers no longer have to handle a Left case that can't happen.

In general, concrete types are more precise than interfaces. Most of the time, we don't need or want that level of precision, but sometimes we do, or at least we want more precision than can be captured by C#'s type system without going all the way to a concrete type. Structural types have structural properties that we do tend to care about and are not captured by analogous interfaces. Many languages build these types in the form of tuples, records, "generalized" enumerations, and algebraic data types.

public abstract class Either<A, B> {
  private Either() {} // Trick to disallow any other subclasses.

  public abstract C Match<C>(Func<A, C> left, Func<B, C> right);

  public sealed class Left : Either<A, B> {
    public readonly A Value;
    public Left(A a) { this.Value = a; }
    public override C Match<C>(Func<A, C> left, Func<B, C> right) {
      return left(this.Value);
    }
  }    

  public sealed class Right : Either<A, B> {
    public readonly B Value;
    public Right(B b) { this.Value = b; }
    public override C Match<C>(Func<A, C> left, Func<B, C> right) {
      return right(this.Value);
    }
  }
}

  1. The unweakened version would have p, a, and b be expressions and not just variables, but then these equations wouldn't hold due to side-effects. ↩︎

History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

+5
−0

I think the main reason to do this is when the interfaces fail to account for some subtlety of the contract between caller and implementation.

For example, let's pretend for a moment that your use case is a performance edge case, and List in particular can handle it due to a peculiarity of its implementation, while other implementations catastrophically fail.

In this case, the authors of List assumed their performance optimization is a merely an implementation detail and left it out of the interface. However, it has turned out not so. It would take you a lot of time to chase down authors of the standard library and get them to update their interfaces, so you need some way in the meanwhile - you can return the concrete type to tell the callers that it has to be exactly List and not just any IList. We can further assume the callers are also aware of the performance issue and want to be guaranteed that you avoided it by using List and not some other IList implementer.

Performance is an easy example because C# interfaces don't explicitly deal with performance expectations. However you can always do things like IFastList, IMyList.MemoryEfficientSort() or IMyList.Sort(Int32) (the Int32 is maxSortSeconds) to introduce a sort of performance constraint to the contract.

However, performance is not the only way. For example, there is a List.Capacity but not IList.Capacity. Normally you shouldn't need to care about capacity, but if your caller happens to have such a need (and simply converting your return value to a new list is not an option) you would have to return List not IList.

So the critical point here is that you would return concrete types when it turns out that some aspect of the API is germane to the caller-implementation contract, and yet was not included in the interface. That means the interface is now unusable, and you must resort to the concrete type as a short term solution. The long term solution is to create a new interface or update the existing one to fully and parsimoniously describe the contract you are trying to set.

However, I would say that 99% of the time people use the concrete type because they know the concrete type and are more familiar with it than the interface, and it is easier. So it's an antipattern, much like using a single God-object instead of many minimal classes.

History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

+4
−0

IList<T> is not necessarily representative of the general case; it's an interface that is (A) widely implemented by a variety of classes from a variety of sources, which themselves (B) tend to add additional functionality or constraints not captured by the signature of IList<T>. There is, in my opinion, very little if any [see below for one] reason to ever return a List<T> instead of an IList<T>, but you could very well want to return a SortedSet<T> in order to get at its Min and Max properties.

Ideally, for that example, there'd be some sort of ISortedSet<T> interface that would abstract over those properties. But there isn't, as of .NET 7.0, which historically is representative of the story with these abstract collection interfaces. Good ideas, 80% execution. So in practice, sometimes you want to type things concretely.


Peter Taylor adds: ‘Another issue specific to IList<T> is that for legacy reasons it doesn't inherit from IReadOnlyList<T>. Returning List<T> allows callers to assign to IReadOnlyList<T>, which can make it easier to reason about the calling code.’


For interfaces that don't get around as much, like ye olde IMyInternalApplicationService, you won't go wrong always favoring those over their concrete implementations everywhere in your code except the place where you tie off your dependency injection knots.

History
Why does this post require moderator attention?
You might want to add some details to your flag.

2 comment threads

The converse of (A) (1 comment)
IReadOnlyList<T> (1 comment)

Sign up to answer this question »