Back to Blog
C#

Using the C# Constant Pattern in Switch Expressions

c# constant pattern: Learn how the C# constant pattern works in switch expressions and statements, including syntax, matching rules, and practical use cases.

C# pattern matchingswitch expressionsconstant patterntype patternsC# 9
Illustration of a switch expression branching on constant values in C#

The C# constant pattern is one of the simplest forms of pattern matching, but it has several behaviors that can trip up developers who are new to it. It matches an input value against a constant such as a numeric literal, a string literal, or a value declared with const. Because it uses the underlying equality semantics, its behavior can vary based on the type of the comparison.

The constant pattern is most commonly seen in switch expressions and switch statements, where it serves as a concise way to branch on specific values. This article walks through the syntax, the matching rules, and the practical boundaries you should keep in mind.

Constant Pattern Syntax in Switch Expressions

In a switch expression, the constant pattern appears as the case label before the => arrow. Here is a minimal example:

string GetPriority(int level) => level switch { 1 => "Low", 2 => "Medium", 3 => "High", _ => "Unknown" };

The numeric literals 1, 2, and 3 are constant patterns. The _ discard pattern matches any value that did not match earlier arms, which is the equivalent of a default case in a classic switch statement.

When the input is an integer value, the pattern is matched using ==, which for value types means a direct comparison of the underlying value. For reference types, the constant pattern uses object.Equals, which can lead to differences from == in certain cases. The rule is that the constant pattern is essentially equivalent to writing input.Equals(constantValue), with the caveat that if the constant is null, the pattern matches when the input is also null.

Matching Rules for null and Reference Types

The constant pattern can match null, which is useful for handling optional values in a switch expression. Consider this example:

string Describe(object? value) => value switch { null => "No value", "test" => "Exactly the string test", 42 => "The integer 42", _ => "Something else" };

Here, null is a constant pattern that matches only when value is null. The string literal "test" is also a constant pattern, but it uses string equality. This is where a subtlety arises: the constant pattern for a string uses object.Equals, which performs ordinal comparison, not culture-sensitive comparison. That is usually irrelevant for exact-match scenarios, but it is a departure from the default == operator on strings, which also performs ordinal comparison but can be overridden to use different semantics.

For reference types other than string, the constant pattern can match a const field or a static readonly field, but the behavior is exactly the equality check provided by the type's Equals method. If a type overrides Equals to be case-insensitive, the constant pattern will use that override. This can be surprising, so it is important to be aware of the type involved.

Constant Pattern in Switch Statements

The constant pattern predates the switch expression and appears in classic switch statements as well. The same matching semantics apply, but the syntax differs slightly because case labels use a colon and a block body, and you often use goto case for fallthrough. For example:

void PrintColor(int code) { switch (code) { case 1: Console.WriteLine("Red"); break; case 2: case 3: Console.WriteLine("Yellow"); break; default: Console.WriteLine("Unknown"); break; } }

Both case 1 and case 2 are constant patterns. case 2 and case 3 are stacked to allow multiple constants to map to the same behavior. In a switch statement, the constant pattern is evaluated in order, and the first matching case is executed, similar to the switch expression.

One difference between the statement and the expression is that a switch statement can have side effects, while a switch expression is expected to return a value and is more restrictive in what can appear in each arm. The constant pattern itself works the same in both contexts.

Practical Use Cases for the Constant Pattern

The constant pattern is most effective when you have a known set of discrete values that you want to map to distinct outcomes. Common scenarios include:

  • Mapping enum values to user-friendly descriptions
  • Parsing command-line arguments where each option is a literal string
  • Handling HTTP status codes with numeric constants
  • Validating configuration values that have a limited number of allowed inputs

An example of mapping an enum is straightforward:

public enum OrderState { Created, Paid, Shipped, Delivered } string GetStateLabel(OrderState state) => state switch { OrderState.Created => "Order created", OrderState.Paid => "Payment received", OrderState.Shipped => "Order shipped", OrderState.Delivered => "Order delivered", _ => "Unknown state" };

The constants OrderState.Created and so on are constant patterns. They match on the enum value exactly. Enum values that are not named are handled by the discard pattern.

Constant Patterns with Type Patterns and Relational Patterns

The constant pattern is often used alongside other pattern forms, such as type patterns and relational patterns. For instance, you can combine a type pattern with a constant pattern to match a specific value of a given type:

string Inspect(object value) => value switch { int 0 => "Zero", int > 0 and <= 10 => "Small positive", int > 10 => "Large positive", string s when s.Length > 0 => $"Non-empty string of length {s.Length}", _ => "Other" };

Here, int 0 is a type pattern combined with a constant pattern, and int > 0 uses the relational pattern. The constant pattern is the specific case inside a type pattern, and it is evaluated after the type check succeeds. This ordering can matter when you have overlapping patterns, because the first matching arm wins.

Common Pitfalls with Constant Patterns

A frequent mistake is to assume the constant pattern uses == semantics when the type defines its own equality operator. Because the constant pattern calls Equals (via the pattern's implementation), a custom Equals can change which values match. This can lead to the pattern matching values that you did not intend.

Another pitfall is using floating-point constants. For example, matching 0.1 exactly can be unreliable because the binary representation of 0.1 is not exact in IEEE 754. The constant pattern will match only if the input is exactly the same binary value. If you compute a value that should mathematically be 0.1, it may not compare equal. Avoid constant patterns for floating-point values unless you are sure the value is an exact representation, such as an integer-valued double.

Also, constant patterns cannot match values that are not compile-time constants. You cannot use a static readonly field or a local variable that is not const as a pattern. Attempting to do so results in a compile-time error. Instead, use a when clause to compare against a non-constant value.

Performance and Compatibility Considerations

For simple numeric or string constants, the constant pattern compiles to efficient comparison code that is often as fast as an explicit equality check. The compiler may optimize a switch over consecutive integers into a jump table, similar to a classic switch statement. However, if the constant pattern involves a custom type with a complex Equals implementation, the cost is the same as calling that method.

In terms of compatibility, the constant pattern has been available since C# 7, and switch expressions were introduced in C# 8.0. If you are using an older compiler, you may need to upgrade to take advantage of switch expressions; the constant pattern within switch statements has been available since C# 7 as part of the pattern-matching feature. The exact version support depends on your SDK and target framework, so check the language version configured for your project if you encounter syntax errors.

When the constant pattern is used in a hot path, the performance is generally acceptable, but you should avoid using it with expensive Equals overrides. For most applications, the constant pattern is a readability win rather than a performance concern.

Alternatives to the Constant Pattern

For simple value checks, the constant pattern is concise, but there are situations where another form is clearer. If you need to compare against a range of values, the relational pattern is more expressive than listing multiple constants. If you need to compare against a non-constant value, a when clause is the appropriate choice.

For example, a when clause can compare against a runtime variable:

string CompareToThreshold(int input, int threshold) => input switch { _ when input == threshold => "Equal", _ when input > threshold => "Above", _ => "Below" };

The discard pattern _ is used as a placeholder, and the when clause evaluates the condition. This is different from a constant pattern because the comparison value is dynamic.

The choice between a constant pattern and a when clause comes down to whether the comparison value is known at compile time. When it is, the constant pattern is more idiomatic and often more readable. When it is not, the when clause is necessary.

Final Code Example: Order Status Parser

To bring the concepts together, here is a more complete example that uses constant patterns to parse a command-line argument and return a status label:

using System; public static class OrderStatus { public static string Parse(string? input) => input switch { "new" => "New order", "paid" => "Paid", "shipped" => "Shipped", "delivered" => "Delivered", null => "No status provided", _ => $"Unknown status: {input}" }; public static void Main() { Console.WriteLine(Parse("shipped")); Console.WriteLine(Parse(null)); Console.WriteLine(Parse("cancelled")); } }

The constant patterns "new" and "paid" match exact string values. The null pattern is also a constant pattern, and the discard pattern handles any other input. This covers the common cases without introducing branching logic or multiple if statements.

c# constant pattern: Practical Usage and Code Examples | RYUSLOG DEV