Using the Discard Operator in C#

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.Add() 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.