The discard operator in C#, a simple underscore, can be used in multiple situations. It is used to signal that something is intentionally unused, like an out parameter or the return value of a method. In this article, I will show how to use this operator and analyze the benefits of using it.
Discard Out Parameters
The discard operator can be used when a method has an out
parameter, but the value
of the out
parameter is not used. In these cases, we can explicitly show that we don’t
need the value by using the discard operator:
public static bool IsInt32WithDiscard(string text)
{
return int.TryParse(text, out _);
}
public static bool IsInt32WithVariable(string text)
{
return int.TryParse(text, out int x);
}
In the second method we declared a variable but didn’t use it which can lead to confusion. In the first method we used the discard operator. The code is not only shorter, but it is also clear that the result is intentionally unused.
The discard operator is only syntactic sugar in this case. We can decompile the method based on the IL with ILSpy, and see that a variable was declared anyways:
Discard for Tuple Deconstruction
Another use case for the discard operator is
Tuple deconstruction.
Considering this C# code, once with discard for age
and once without:
public static void TupleDeconstructionWithDiscard()
{
(string firstName, string lastName, _) = GetPersonInformation();
Console.WriteLine(firstName + " " + lastName);
}
public static void TupleDeconstructionWithoutDiscard()
{
(string firstName, string lastName, int age) = GetPersonInformation();
Console.WriteLine(firstName + " " + lastName);
}
public static (string firstName, string lastName, int age) GetPersonInformation()
{
string firstName = "John";
string lastName = "Doe";
int age = 34;
return (firstName, lastName, age);
}
We get the following IL code for the method with discard (TupleDeconstructionWithDiscard()
)
with the unoptimized Debug
build:
.method public hidebysig static
void TupleDeconstructionWithDiscard () cil managed
{
.maxstack 3
.locals init (
[0] string firstName,
[1] string lastName
)
IL_0000: nop
IL_0001: call valuetype [System.Runtime]System.ValueTuple`3 DiscardOperator.Program::GetPersonInformation()
IL_0006: dup
IL_0007: ldfld !0 valuetype [System.Runtime]System.ValueTuple`3::Item1
IL_000c: stloc.0
IL_000d: ldfld !1 valuetype [System.Runtime]System.ValueTuple`3::Item2
IL_0012: stloc.1
IL_0013: ldloc.0
IL_0014: ldstr " "
IL_0019: ldloc.1
IL_001a: call string [System.Runtime]System.String::Concat(string, string, string)
IL_001f: call void [System.Console]System.Console::WriteLine(string)
IL_0024: nop
IL_0025: ret
}
We receive a Tuple with three values from GetPersonInformation()
but only two values are stored.
The IL for the TupleDeconstructionWithoutDiscard()
method on the other hand also assigns the
age
value, even though it is not used later in the code.
In the optimized Release
build, the methods have almost no difference anymore. In the method
without the discard, it is detected that the age
variable is unused, and therefore no IL code is
generated for that. There is still an additional dup
and a pop
instruction (I am not
sure why), but the value is not read from the Tuple.
Switch Expression Requires Discard
The switch expression
(not switch statement)
uses the discard operator to handle the remaining cases (default
case in switch
statements).
A switch
expression where not all cases are handled leads to a
CS8509
warning at build time or a SwitchExpressionException
at runtime.
public static FileAccess GetFileAccess(string input)
{
return input switch
{
"R" => FileAccess.Read,
"W" => FileAccess.Write,
_ => FileAccess.ReadWrite,
};
}
public static FileAccess GetFileAccessOrThrow(string input)
{
return input switch // <- Warning CS8509:
// The switch expression does not handle all possible
// values of its input type (it is not exhaustive). For example,
// the pattern '""' is not covered.
{
"R" => FileAccess.Read,
"W" => FileAccess.Write,
};
}
Standalone Discards for Unused Method Return Values
Sometimes it is useful to explicitly ignore return values from methods. This can be done by assigning the return value to the discard operator:
public static void StandaloneDiscard(string input)
{
_ = int.TryParse(input, out int value);
// use value...
}
This can be used to signal that the return value is intentionally unused, what is useful with methods that use return codes instead of exceptions.
Using explicit discards for methods can be enforced with the
IDE0058
and IDE0059
rules in the .editorconfig
. I tried that in my projects but didn’t gain any quality improvements.
In those cases where I actually should have used the return value (see
ImmutableList
for example), I just automatically added the discards with the code fix shortcut. But if you follow the
“The Power of 10: Rules for Developing Safety-Critical Code”
from NASA, this is the way to go for rule 7.
Argument validation is another use case. Before we had ArgumentNullException.ThrowIfNull()
it
was possible combine the discard operator with the
null coalescing operator
to check the argument for null
on a single line:
public static void ArgumentValidation(string input)
{
_ = input ?? throw new ArgumentNullException(nameof(input));
// ...
}
Conclusion
The discard operator is a useful addition to signal intentionally unused values. Discarding unused return values of methods can be helpful in some rare cases but has no real benefit when enforced on the whole project.