Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
C# 8 Positional Patterns Custom Deconstructor Pitfall

C# 8 adds a wide array of new pattern types to the pattern matching features introduced in C# 7. One of these is the 'positional pattern'. This is a recursive pattern, meaning that it contains nested patterns. It can be used to determine whether tuples match certain criteria. For example:

switch (v)
{
    case (0, 0): return "Origin";
    case (0, _): return "On Y axis";
    case (_, 0): return "On X axis";
    default: return "Somewhere";
}

All the case labels here are using positional patterns with two positions. The first uses constant patterns in both positions. The next two use a constant in one position, and a discard pattern (which will match any value) in the other.

What you might not know is that this kind of pattern is not limited to tuples. If a type supplies a deconstructor, it gets to enjoy some of the same language features as tuples. Here's a type with a custom deconstructor:

public readonly struct Size
{
    public Size(double w, double h)
    {
        W = w;
        H = h;
    }

    public void Deconstruct(out double w, out double h)
    {
        w = W;
        h = H;
    }

    public double W { get; }
    public double H { get; }
}
The Introduction to Rx.NET (v6.1) 3rd Edition (2025) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

That Deconstruct method enables us to use the code in a deconstructing assignment:

public static double GetDiagonalLength(Size s)
{
    (double w, double h) = s;
    return Math.Sqrt(w * w + h * h);
}

But of more interest to the topic at hand, it also lets us apply positional patterns to our type:

static string DescribeSize(Size s) => s switch
{
    (0, 0) => "Empty",
    (0, _) => "Extremely narrow",
    (_, 0) => "Extremely wide",
    _ => "Normal"
};

Here I'm using C# 8's new switch expression, because it was a slightly more succinct option than the ordinary switch statement I wrote in the first example, in which each case is a return statement. As far as its use of patterns is concerned, this looks very similar to my first example, but the interesting point with this example is that these patterns are all using my type's custom deconstructor.

Deconstruction and positional patterns work with tuples thanks to baked-in support in C#. The types that underpin tuples (a family of generic types including ValueTuple<T>, ValueTuple<T1, T2>, ValueTuple<T1, T2, T3> etc.) don't provide a Deconstruct method. The compiler just recognizes these types, and if you deconstruct them, it generates code that extracts the tuple members directly. And as we'll see, it generates special tuple-specific code for pattern matching too. But there's another respect in which tuples are special: in cases where C# cannot infer a pattern's input type statically, the compiler will presume that a positional pattern is looking for a tuple unless you state some other type explicitly. Look at this example:

static string DescribeSize2(object s) => s switch
{
    (0, 0) => "Empty",
    (0, _) => "Extremely narrow",
    (_, 0) => "Extremely wide",
    _ => "Normal"
};

This is almost identical to the preceding example. The only differences are the method name, and a change to the parameter type. But that second difference turns out to be critical, as becomes apparent when we try to use these two methods:

Size[] sizes = { new Size(0, 0), new Size(0, 10), new Size(10, 0) };
foreach (Size size in sizes)
{
    Console.WriteLine(DescribeSize(size));
    Console.WriteLine(DescribeSize2(size));
    Console.WriteLine();
}

Since DescribeSize and DescribeSize2 have identical bodies, you might expect each iteration of the loop to print out two copies of the same message. But in fact, we see this:

EmptyNormalExtremely narrowNormalExtremely wideNormal
Programming C# 12 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

DescribeSize matches in the way we want. But DescribeSize2 has hit the fallback discard pattern every time! What's happening? Here's how the compiler has interpretted these patterns:

static string DescribeSize(Size s) => s switch
{
    Size s when s.W == 0.0 && s.H == 0.0 => "Empty",
    Size s when s.W == 0.0 => "Extremely narrow",
    Size s when s.H == 0.0 => "Extremely wide",
    _ => "Normal"
};

static string DescribeSize2(object s) => s switch
{
    ITuple t when t.Length == 2 && t[0] is int t0 && t0 == 0 && t[1] is int t1 && t1 == 0 => "Empty",
    ITuple t when t.Length == 2 && t[0] is int t0 && t0 == 0 => "Extremely narrow",
    ITuple t when t.Length == 2 && t[1] is int t1 && t1 == 0 => "Extremely wide",
    _ => "Normal"
};

In the expanded DescribeSize you can see that it's looking specifically for values of type Size, and that it's then checking either or both of the properties. But DescribeSize2 is altogether more weird.

Why is that what we get if we omit the type? Well when you use a positional pattern, C# will attempt to infer the type it should match. In the original DescribeSize, it could see that the input to the switch expression (and thus the input to each of the patterns) was the argument Size s. But in the original DescribeSize2, that input is of type object, so it has nothing to go on. (The built-in object type does not define a Deconstruct method.) In this case, the compiler just presumes that we're looking for a tuple, so for each pattern it generates code that first checks to see whether the input implements the ITuple interface. (This is implemented by all the various ValueTuple types that underpin tuples.) And if that interface is present, it next checks that the tuple has the expected number of elements, before going on to evaluate each child pattern (except for the discards, which don't need any code) against its corresponding tuple element.

We could make our Size type implement ITuple, but that wouldn't actually help: the nested patterns are all looking for int values but since Size contains double values, these would fail to match. This problem doesn't arise in DescribeSize because the compiler knows exactly which type we're using, enabling it to know that it has to implicitly promote those integer 0 constants to floating point values.

Note: If you want a positional pattern to match through a custom deconstructor even in cases where the pattern input's static type is object, you can just include the type name specification, i.e., put Size before the opening parenthesis of the pattern.

Until I experimented with this, I hadn't understood that C# has two completely different code generation strategies that it uses with positional patterns, depending on whether it can see a custom deconstructor on the static type of the input. It makes sense that the pattern somehow needs to know the type—otherwise it would need to support matching against any type with a deconstructor, and since anyone can write new deconstructable types, that's an open ended list. That could only really work through reflection, which would be inefficient. Even so, I hadn't previously realised that the mechanisms used with tuples are so different from the ones used with custom deconstructors.

I describe this as a pitfall in the title because with a very subtle change (the static type of the input to the pattern) you can significantly alter a pattern's meaning, possibly to something quite different from what you had in mind. (This can be a problem even in an all-tuple world: when the input is a known tuple type, the compiler may know that it has to perform implicit type conversions, which would not happen for a tuple hiding behind a variable with a static type of object) In fact, my very first example is ambiguous: you can't know exactly what it does without knowing the static type of its input, v.

FAQs

What is a positional pattern in C# 8? A positional pattern is a recursive pattern that contains nested patterns and can be used to determine whether tuples match certain criteria. It can also be used with any type that supplies a Deconstruct method, not just tuples.
Why do positional patterns behave differently when the input type is object? When C# cannot infer a pattern's input type statically, it presumes a positional pattern is looking for a tuple. The compiler generates code that checks for ITuple interface implementation and uses tuple-specific matching. With a known type, it uses the type's custom Deconstruct method instead.
What are the two different code generation strategies C# uses for positional patterns? For known types with custom deconstructors, C# generates code that checks the type and directly accesses properties via the Deconstruct method. For unknown types (like object), it generates code using the ITuple interface, checking length and accessing elements by index with type checks.
How can you make a positional pattern use a custom deconstructor when the input type is object? You can include the type name specification before the opening parenthesis of the pattern. For example, use 'Size (0, 0)' instead of just '(0, 0)' to explicitly tell the compiler to match against the Size type and use its deconstructor.
Why might the same positional pattern produce different results depending on the input's static type? The static type affects whether implicit type conversions occur. When the input is a known type, the compiler knows to promote integer constants to the appropriate type (like double). When matching against ITuple with an object input, the compiler expects exact types, so integer 0 won't match a double 0.0.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked across an extraordinary breadth of computing - from embedded real-time systems and broadcast television to medical imaging and cloud-scale architectures. As Technical Fellow at endjin, he brings this deep cross-domain experience to bear on the hardest technical problems.

A 17-time Microsoft MVP in Developer Technologies, Ian is the author of O'Reilly's Programming C# 12.0 and one of the foremost authorities on the C# language and high-performance .NET development. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects.

Ian has created Pluralsight courses on WPF fundamentals, WPF advanced topics, WPF v4, and the TPL, and has given over 20 talks at conferences worldwide. Technology brings him joy.