Using the C# Discard Operator Effectively
c# discard: Learn how to use the C# discard operator (`_`) to ignore unwanted values in assignments, out parameters, tuple deconstruction, and pattern matching.
The C# discard operator, written as an underscore (_), tells the compiler that you intentionally want to ignore a value. It is not a variable, so you cannot read from it or assign to it again. Discards are useful when a method returns a value you do not need, when an out parameter is required but irrelevant, or when a tuple contains fields that are not part of your current logic.
// Return value ignored _ = int.TryParse(input, out _);
This article explains where discards fit into everyday C# code, how they behave at compile time, and where they can mislead you if used carelessly.
What the Compiler Does with a Discard
A discard is not a storage location. When you write _ = SomeMethod(), the compiler still evaluates the method, but it does not allocate a variable to hold the result. That means the expression on the right side executes in full, including any side effects. The discard simply discards the resulting value.
This is different from assigning to a variable and then never reading it, because a variable occupies memory and may trigger a compiler warning. A discard signals intent: the value is deliberately unneeded.
A discard can appear in several contexts:
- assignment:
_ = ComputeResult(); - deconstruction:
(_, y) = GetPoint(); outparameters:TryGetValue(key, out _)- pattern matching:
case int _: - lambda parameters:
(_, _) => ...(C# 9+) - switch expressions:
_ => defaultValue
Each context follows the same rule: the underscore is not a real variable, so it cannot be read or reassigned.
Using Discards with out Parameters
Methods like int.TryParse and Dictionary.TryGetValue use out parameters to return additional data. Sometimes you only care about the boolean return value, and the out value is meaningless in that call.
if (int.TryParse(input, out _)) { Console.WriteLine("Valid integer"); }
Here the parsed integer is discarded because only the success/failure flag matters. Using a discard is clearer than declaring a dummy variable, because the dummy variable signals that you might use it later, which invites future misuse.
This pattern also works with custom methods that expose out parameters only for completeness. If the caller does not need the value, a discard keeps the call site clean.
Deconstructing Tuples with Discards
Tuples are a common way to return several values from a method. When you only need some of them, discards let you ignore the rest.
(var name, _, var score) = GetUserInfo(); Console.WriteLine($"{name}: {score}");
The GetUserInfo method returns a three-element tuple, but the middle field (perhaps an ID or a timestamp) is irrelevant. The discard occupies that position, so the deconstruction still compiles.
If you discard every element, you can deconstruct without binding any local variables:
(_, _, _) = GetUserInfo();
That may look odd, but there are cases where you need to force evaluation of the method and do not want the returned data. More commonly, you discard a subset of fields.
Discards in Pattern Matching
Pattern matching uses discards as a catch-all for cases where you do not need the matched value.
object value = ...; string description = value switch { int i when i > 0 => $"Positive {i}", int _ => "Non-positive integer", string s => s, _ => "Unknown type" };
The int _ pattern matches any integer but ignores the actual value. The final _ is the default case, matching anything that was not handled above. Both are discards, though they occupy different positions: one is a type pattern with a discard, the other is a standalone discard pattern.
A discard in a pattern is different from a wildcard that matches everything. In a switch expression, the _ pattern at the end acts as the default. You cannot use _ in a simple switch statement as a case label; instead, you still use default:. But in a switch expression, _ is the fallback.
Discards as Lambda Parameters
C# 9 introduced support for discards as lambda parameters, which is useful when you need to supply a delegate with a specific signature but do not need all the arguments.
Action<int, int> handler = (_, _) => Console.WriteLine("Button clicked");
Here both parameters are discarded. The lambda can still be assigned to an Action<int, int> delegate, and it will be invoked whenever the event fires. The discard names signal to readers that the arguments are intentionally ignored.
This is cleaner than using a single underscore for both parameters, which would not compile before C# 9. With two underscores, each one is a separate discard, and there is no conflict.
Performance, Maintainability, and Tradeoffs
From a performance perspective, discards have essentially zero cost. The compiler does not generate code to store the value, which may avoid an assignment or a temporary allocation. This is rarely the reason to use a discard, but it means there is no downside.
The more important effect is maintainability. A discard says: "I know this value exists and I do not need it." A dummy variable (such as var unused = ...) leaves ambiguity. Future readers may wonder if the variable was meant to be used later, and an unused local variable can trigger analyzer warnings.
That said, overusing discards can hurt clarity. If you discard many values, you lose the ability to reference them later without changing the deconstruction or call. Discards are also positional: in a deconstruction, you must know the order of the tuple elements. If that order changes in the method signature, the discard may silently ignore a different field than intended. This is a real maintainability risk, especially in large codebases.
A discard can also be confused with the placeholder underscore used in numeric literals, such as 1_000_000. Those underscores are part of the literal and have nothing to do with discards. The context makes the meaning clear, but it is worth noting when reading code.
Where Discards Can Cause Problems
Discards are not always harmless. One common mistake is to discard the result of a method that has side effects or errors that should be handled. For example, ignoring the return value of a Task without awaiting it can cause unobserved exceptions. A discard does not suppress exceptions; it only discards the returned value.
_ = FireAndForgetAsync(); // dangerous if you ignore the Task
This code discards the Task, which means you will never observe an exception that the task throws. In most cases, you should await such a task instead. A discard does not change the runtime behavior of the awaited operation; it just drops the reference.
Another issue appears with out parameters that also serve as input, such as TryGetValue with a reference type. Discarding the out value may be fine, but if the method populates a field you later need, you obviously cannot use a discard.
Practical Decision Criteria
Use a discard when:
- The return value is genuinely irrelevant to the current logic.
- An
outparameter exists only to satisfy an API contract. - A tuple element is not part of the current computation.
- A pattern needs to match any value of a type without using it.
- A lambda must match a delegate signature but does not need its parameters.
Do not use a discard when:
- The value might be needed later in the method.
- The value carries important error information, such as an error code in an
outparameter. - The
TaskorIAsyncEnumerableis the only way to observe completion or exceptions.
If you later find yourself changing a discard into a real variable, that is normal. The discard is explicit about what you originally ignored, which makes that change safe and obvious.
Final Example: A Complete Method Using Discards
Putting the pieces together, here is a small method that validates and parses user input using several discard patterns.
public bool TryGetPositiveInt(string input, out int value) { if (!int.TryParse(input, out _)) { value = 0; return false; } value = int.Parse(input); // Could reuse the result, but we want to show a discard. return value > 0; }
In that example, the first out is discarded because the parsed value will be parsed again later. In real code you would combine the steps, but the principle stands: the discard makes it obvious that you intentionally ignore that intermediate result. The second call does not use a discard because you need the value.
This example also shows that discards are a tool for clarity, not a performance optimization. The double parse is wasteful, but a skilled developer would either use the first result or check success differently. The discard is not the reason for the inefficiency; the duplicate parse is.