Interface versus generic constraint with regard to structs

The other day I asked the following on Twitter:

Of course, with my minuscule follow count, I couldn't really expect an answer (no, I'm not bitter, it's not that I put any energy into growing that count), so I had to answer the question myself, or rather, confirm what I thought was correct. I wrote the following 2 methods:

public interface IPrimitive<T> { T Value { get; } }
readonly record struct UserId(string Value) : IPrimitive<string>;
readonly record struct ProductId(string Value) : IPrimitive<string>;

static string SwitchOnValue1<T>(T value) where T : IPrimitive<string> =>
  value switch
  {
    UserId { Value: "user-123" } => "It's you, Ralph",
    ProductId { Value: "product-123" } => "It's Ralph's product",
    _ => throw new NotSupportedException()
  };

static string SwitchOnValue2(IPrimitive<string> value) =>
  // identical to the first version

Notice the different argument type and the first version being generic while the second isn't. With the wonderful BenchmarkDotNet set up two benchmarks that would call each version with the same structs...

[Benchmark]
public void CallingTheSwitch1()
{
  var result = SwitchOnValue1(new UserId("user-123"));
  result = SwitchOnValue1(new ProductId("product-123"));
}

[Benchmark]
public void CallingTheSwitch2()
{
  var result = SwitchOnValue2(new UserId("user-123"));
  result = SwitchOnValue2(new ProductId("product-123"));
}

Once the benachmark program did its job, I was rewarded with a great confirmation:

|            Method |      Mean |     Error |    StdDev |  Gen 0 | Allocated |
|------------------ |----------:|----------:|----------:|-------:|----------:|
| CallingTheSwitch1 |  4.977 ns | 0.1570 ns | 0.3172 ns |      - |         - |
| CallingTheSwitch2 | 20.872 ns | 0.4560 ns | 0.5068 ns | 0.0102 |      48 B |

The generic method will not do any boxing, T really is the struct and the fact that you used an interface for constraining the value is irrelevant. In the second case however, using the Interface requires a boxing operation of the struct with allocation and a significant performance penalty.

Creative Commons License

Frank Quednau 2022