I stumbled upon a piece of C# code the other day that made me do a double-take. It looked something like this.
At a glance, it seems to defy a fundamental rule of C# inheritance.
The “Problem” Code
Here’s a simplified version of what I saw:
1using System;
2using System.Linq;
3using System.Collections.Generic;
4
5public class Program
6{
7 public static void Main()
8 {
9 var a = new Generic<FieldValue> ();
10 var b = new Generic<TextFieldValue> ();
11
12 var fa = a.GetFieldValue ();
13 var fb = b.GetFieldValue ();
14
15 Console.WriteLine ($"{fa} of type {fa.GetType ().Name}");
16 Console.WriteLine ($"{fb} of type {fb.GetType ().Name}");
17 }
18}
19
20/*****************************************************************************/
21
22public interface IStaticCreate
23{
24 static abstract FieldValue Create();
25}
26
27/*****************************************************************************/
28
29public record FieldValue(int Id)
30 : IStaticCreate
31{
32 static FieldValue IStaticCreate.Create() => new FieldValue (1);
33}
34
35public record TextFieldValue(int Id, string Text)
36 : FieldValue(Id), IStaticCreate
37{
38 static FieldValue IStaticCreate.Create() => new TextFieldValue (2, "bla");
39}
40
41/*****************************************************************************/
42
43public abstract class AbstractBase
44{
45 public abstract FieldValue GetFieldValue();
46}
47
48public class Generic<T> : AbstractBase
49 where T : FieldValue, IStaticCreate
50{
51 public override T GetFieldValue() // <-- This is the weird part
52 {
53 var result = T.Create();
54 return (T)result;
55 }
56}
When you run this, the output is:
1FieldValue { Id = 1 } of type FieldValue
2TextFieldValue { Id = 2, Text = bla } of type TextFieldValue
So, What’s Going On?
Look at the Generic<T> class. It inherits from AbstractBase, which has an abstract method:
1public abstract FieldValue GetFieldValue();
But the override in Generic<T> is:
1public override T GetFieldValue()
How is this allowed? I always thought an overriding method had to have the exact same signature as the method it’s overriding, including the return type.
It turns out this is a feature called Covariant Return Types.
The Answer: C# 9 Covariant Return Types
This feature was introduced in C# 9.0 (which shipped with .NET 5 back in November 2020).
Covariant return types allow an overriding method to return a type that is more derived than the return type of the base class method.
The magic that makes it all work is the generic constraint on Generic<T>:
1where T : FieldValue, IStaticCreate
This constraint guarantees to the compiler that whatever T is, it will always be a FieldValue or a class that inherits from FieldValue (like TextFieldValue).
Because the compiler has this guarantee, it knows that any T returned by the override is assignable to the FieldValue required by the base method. The rule is satisfied.
Why Is This Useful?
This isn’t just a neat trick; it’s incredibly useful for type safety.
Look at the Main method again:
1var b = new Generic<TextFieldValue> ();
2var fb = b.GetFieldValue ();
The compiler knows that b.GetFieldValue() returns a TextFieldValue, not just a FieldValue. The variable fb is correctly inferred as TextFieldValue. This means we can immediately access its Text property (fb.Text) without having to do an ugly and potentially unsafe cast.
Before C# 9.0, this code wouldn’t have compiled. You would have been forced to write the override like this:
1// The "old" way
2public override FieldValue GetFieldValue()
3{
4 var result = T.Create();
5 return result;
6}
…and then the caller would have to cast the result:
1// The "old" way at the call site
2var b = new Generic<TextFieldValue> ();
3var fb_base = b.GetFieldValue (); // fb_base is FieldValue
4var fb = (TextFieldValue)fb_base; // Nasty cast needed
So, if you see an override returning a more specific type than its base, don’t panic. It’s not a bug, it’s C# 9.0 making our lives just a little bit better.