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 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:
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:
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 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.
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.
4. Related
Subscribe
Comments
comments powered by Disqus