Generic types are for arguments, specific types are for return values

Today, we’ll discuss the following guideline: you should use the most generic types possible for arguments and the most specific types possible for return values.

1. Return the most specific type, accept the most generic type

I’ve already written an article about this topic but that was 7 years ago, so it’s a good idea to reiterate one more time. I’ll expand on this guideline with images and examples.

Here’s the guideline itself:

  • Use the most generic types possible for arguments,

  • Use the most specific types possible for return values.

We’ll use the following class hierarchy as an example:

public abstract class Pet
{
    /* ... */
}

public class Dog : Pet
{
    /* ... */
}

public class Cat : Pet
{
    /* ... */
}

1.1. Generic types are for arguments

I’ll start by demonstrating the first part of the guideline. Let’s say that we have a Feed method:

public static void Feed(Xxx xxx)
{
    /* ... */
}

What type the argument xxx should have here?

Assuming the Feed functionality is applicable to both dogs and cats, the method should accept the generic Pet:

public static void Feed(Pet pet)
{
    /* ... */
}

This is where the first part of the guideline kicks in:

  • Use the most generic types possible for arguments.

In our specific example, the most generic type is Pet, it doesn’t make sense to accept only dogs or only cats here.

Let’s now say that we have a Wash method that is applicable to dogs, but not cats:

public static void Wash(Dog dog)
{
    /* ... */
}

We can’t accept the generic Pet here. We must use the most generic type possible, meaning the most generic type to which this method applies — the Dog.

This is the same reason why we can’t use the Object type as the argument for the Feed method:

The first part of the guideline
The first part of the guideline

The Feed method only applies to pets, it doesn’t apply to all objects in our application. Similarly, Wash only applies to the subset of pets — dogs.

I hope this illustrates the first part of the guideline: that you should use the most generic types possible for arguments.

1.2. Specific types are for return values

The same logic applies to return values, but in reverse.

For example, let’s say we have this method that finds us someone we can feed:

public static Xxx FindNextOneToFeed()
{
    /* ... */
}

What type should we use in place of Xxx?

Pet is the most specific type here because FindNextOneToFeed can return either a cat or a dog:

public static Pet FindNextOneToFeed()
{
    /* ... */
}

But this method:

public static Dog FindNextOneToWash()
{
    /* ... */
}

can only return dogs, and so the Dog type is the one we should use.

In theory, we could use Pet as the return type here, but it would limit consumers of this method. Dog is a more specific type than Pet, and therefore you can do more things with it functionality-wise.

For example, Dog may have a Bark method that the generic Pet doesn’t have:

The second part of the guideline
The second part of the guideline

If the method returns a Pet, the consumers wouldn’t be able to use that additional functionality.

This is what the second part of the guideline is about:

  • Use the most specific types possible for return values.

1.3. Return the most specific type, accept the most generic type

Overall, this guideline is all about maximizing convenience for consumers of your code. This picture is a good summary of the guideline:

The guideline in one picture
The guideline in one picture

Here’s the reasoning behind its two parts:

  • On the one hand, argument types provide the most utility when they are as generic as possible, because the method starts to apply to a wider range of input values.

  • On the other hand, return types provide the most utility when they are as specific as possible, because a specific return value provides more functionality than a generic one.

There’s a whole language construct built in into C# that is devoted to this guideline, it’s called covariance and contravariance.

Be conservative in what you send, be liberal in what you accept.

☝ This statement is usually used in the context of backward compatibility between applications, but it’s a surprisingly accurate approximation of our guideline as well.

  • Be liberal in what you accept ⇒ accept as wide range of values as possible ⇒ accept the most generic type possible.

  • Be conservative in what you send ⇒ return as "fixed" set of values as possible ⇒ return the most specific type possible.

2. Leaky abstractions

Let me repeat the guideline again:

  • Use the most generic types possible for arguments,

  • Use the most specific types possible for return values.

What does the "possible" part of it mean, exactly?

For example, it is possible to accept the more generic Object instead of Pet in the Feed method:

public static void Feed(Object obj)
{
    /* ... */
}

Of course, the method itself wouldn’t apply to all objects, but we can do a cast like this:

public static void Feed(Object obj)
{
    Pet pet = obj as Pet;
    if (pet == null)
        return;

    /* ... */
}

Don’t we adhere to the guideline even better this way?

No, we don’t, because such a change makes the method a leaky abstraction.

Abstraction is the amplification of the essential and the elimination of the irrelevant.

 — Robert Martin

All code is abstraction. You don’t have to use interfaces or abstract classes to introduce an abstraction.

A regular method is an abstraction too:

  • Its signature is the public part that helps you to amplify the essential — what the code does.

  • That signature also allows you to hide to the irrelevant — how the code does what it does, the method’s implementation details.

A well-defined method forms a good abstraction, where in order to properly use it you only need its signature; you don’t have to dig into the method’s body. On the other hand, a leaky abstraction is an abstraction that requires you to know implementation details it ought to abstract.

This is the same concept as method signature honesty that goes hand in hand with functional programming: a method signature is honest when it doesn’t have any hidden inputs and outputs.

The Feed method that accepts an Object argument becomes a leaky abstraction because consumers now have to understand that method’s implementation details. They need to know that this method doesn’t really work with all objects, and you can’t for example feed a User with it.

So this is what the "possible" part of the guideline means: use the most generic and specific types so long as you keep the method’s signature honest and the method itself doesn’t become a leaky abstraction:

  • Use the most generic types for arguments, while keeping the method’s signature honest,

  • Use the most specific types for return values, while keeping the method’s signature honest.

Note

Not everything can be conveyed with a method’s signature, unless you are using a functional programming language with an explicit emphasis on purity, such as Haskell.

There’s an argument to be made that even when accepting a Pet and not Object, the Feed method is still a leaky abstraction because its signature doesn’t express the side effects (data modifications) this method incurs.

Still, when it comes to aspects that are expressed by the signature, you should convey them as precisely as possible, to avoid any misunderstanding.

Next week, we’ll talk about the application of this guideline to collection interfaces.

3. Summary

  • Follow the guideline below.

    • Use the most generic types possible for arguments. Argument types provide the most utility when they are as generic as possible, because the method starts to apply to a wider range of input values.

    • Use the most specific types possible for return values. Return types provide the most utility when they are as specific as possible, because a specific return value provides more functionality than a generic one.

  • The word "possible" in the guideline means "without making the method a leaky abstraction".

  • A leaky abstraction is an abstraction that requires you to know implementation details it ought to abstract.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus