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
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

When would one not want to return an interface?

+7
−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. ↩︎

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

2 answers

+4
−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. ↩︎

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

0 comment threads

+2
−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.

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

1 comment thread

IReadOnlyList<T> (1 comment)

Sign up to answer this question »