Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
C# Lambda Discards

I recently read a tweet that asked a seemingly straightforward question, but it prompted some C# language wonks to weigh in with more detail than was strictly necessary. (If you regularly read my blogs, it won't come as a huge surprise that I was one of these wonks. Speaking of which, why not buy my new book, Programming C# 10.0?)

The tweet asked whether using an underscore as a parameter discard in a lambda made sense:

The short answer (as several people had already replied before I turned up) is: yes, that makes sense, and it's a common idiom in C#.

But who would want a simple, direct answer like that when you could make it so much more complicated?

If you ever deal with legacy code, it is worth understanding what happens here in a bit more detail. A few years ago, discards were not a thing in C#, but recent versions have gradually been adding more support for them. This history means that not everything is as it seems, and it can be useful to know that, because it can explain some strange-looking practices in older code.

That's not a discard, THIS is a discard

Let's get the language pedantry out of the way. There are various places in C# where we use _ as what's called a discard. However, in this example, the _ is not technically a discard:

Func<int, int, int> f = (_, x) => 2 * x;

The lambda here (the part to the right of the first = symbol) has two parameters, the first called _ and the second called x. We can demonstrate this by using both parameters:

Func<int, int, int> f2 = (_, x) => _ * x;

That shows that the underscore is just an ordinary parameter—C# accepts _ as an identifier so _ is as good a name as x, according to the rules of the language. However, it is very common to see examples like the first one, where we use _ for an unused parameter. Informally, people often call this kind of parameter a "discard" but as far as the C# compiler is concerned, the _ is just an ordinary parameter in these first two examples.

I already discussed this not-really-a-discard behaviour in blog on discards and underscores in C# 8.0 a few years ago. However, things have changed since then. C# 9.0 made it possible to write code like this:

Func<int, int, int> f2 = (_, _) => 42;

In this third example, these underscores really are discards. (The rule is that if a lambda has multiple parameters called _ then they are all discards.) I'll get into why that's true only for the third example soon, but first, why would we even want to do this?

Why would I be discarding an argument?

Some APIs require you to provide a callback with a particular signature. This often happens because of the pattern that most .NET events use, in which handlers are passed two arguments: the object that raised the event, and information about the event. For example, the SerialPort class has a DataReceived event that uses the SerialDataReceivedEventHandler delegate type:

public delegate void SerialDataReceivedEventHandler(object sender, SerialDataReceivedEventArgs e);

That tells us that handlers for this event will be passed two arguments. The first argument (corresponding to the sender parameter) is just the instance of SerialPort that raised the event. The second argument (for the e parameter in that delegate signature) provides information about what just happened.

That first argument is there so that if we attach a single handler to multiple event sources, the handler can determine where each event came from. But if you attach a handler to exactly one object, that information is redundant. You won't need to look at the first argument because you already know which object raised the event.

The discard convention and its shortcomings

It's an unavoidable fact that we will sometimes have no choice but to write a method that is required to accept one or more arguments that it won't use. This creates a problem. Unused parameters look like they might be a mistake. Take this example:

public static void ShowNumber(int i)
{
    Console.WriteLine(42);
}

This declares a parameter called i, but never uses it. Now perhaps that's not a mistake—this code is just an example in a blog, so perhaps there are no conceivable circumstances under which anyone might invoke the ShowNumber method passing in any value other than 42, in which case the code is functionally correct. Or maybe there's some good reason that it needs to print out that number regardless of its input. But it certainly looks suspect.

The compiler thinks it might be wrong—it emits a Remove unused parameter (IDE0060) message. (If you're curious, you can look at the source for the analyser that detects unused parameters.) But it's not confident enough to issue a warning. We only get a "Message" level diagnostic, because it can't really be sure that this is a mistake.

That uncertainty is the problem. When software projects are successful, the code they contain is typically read much more often than it is written. (Ideally, we write it just once, but changing requirements or bug fixes mean that's not always true. Even so, it will typically be read many times.) This means it's a really good idea for your code's intent to be self-evident. You don't want people who look at your code to have to spend time wondering whether you really did mean to ignore that argument. And it's not just people either: we want automated code analysis to be able to understand what we meant.

Getting the most out of code analysers

Code analysers are a vital tool for improving the quality of our code, but their power is greatly diminished if you get into the habit of ignoring them. If you ensure you always get a completely clean build—no warnings and no analyser messages—then code changes that produce new diagnostic messages will instantly get your attention. But if your codebase is in a state where it's normal to have a bunch of messages (or worse, warnings) that you habitually ignore, it becomes very hard to notice when new ones have something important to tell you.

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

This means it's really useful to be able to make our intentions clear. If we can somehow indicate unambiguously that we are deliberately ignoring some particular parameter, the diagnostic tools can know that they should not produce a message, and we can keep our builds clean.

This is why .NET developers started adopting a convention of using _ as the name for a parameter that was deliberately being ignored. If you modify the preceding example by renaming the i parameter to _, you will see that the compiler stops producing an IDE0060 message, because it is aware of this convention, enabling it to understand that we are ignoring the parameter on purpose. (If you looked at the source for the analyser earlier, you might be wondering why changing the name to _ prevents the error. The logic for that is in a different file: this line ensures that although variables called _ are not technically discards, the analyser recognizes that the developer intends them to be understood as discards.)

The _ convention was inspired by various other languages. For example, Haskell uses _ in various ways; F# uses _ to denote a wildcard pattern. Technically in C#, there was nothing special about using _ (at least, not back when people first started doing this). We could have chosen any convention—perhaps we could all have agreed to name unused parameters unused, or junk, or something similar. But underscores were already widely used in some other languages, so they were a sensible choice.

There's one problem. What if you have two parameters you want to ignore? Going back to the .NET event handler example, sometimes it's not just the first object argument that will be useless. Sometimes the second argument has no meaningful information either. For example, various user interface frameworks in .NET offer a class representing a clickable button, which offers an event called Click, or Clicked. The thing about button clicks is that there's nothing much to say beyond the fact that the button just got clicked. The only interesting piece of information the event has to offer is that the thing it represents just occurred, so the second argument is usually just EventArgs.Empty.

So now we want to ignore two arguments. But since the underscore thing is just a convention we come unstuck:

public void MyClickHandler(object _, EventArgs _)  // Won't compile
{
...

The compiler rejects this, because it requires methods to use a different name for each parameter:

error CS0100: The parameter name '_' is a duplicate

This happens because the use of _ was just a convention. It is a perfectly valid name for a parameter, so we can't have two parameters called _ for the same reason we can't have two parameters called x.

An ugly convention

The standard workaround was to use more underscores. The compiler is happy with this, for example:

public void MyClickHandler(object _, EventArgs __)
{
...

This works up to a point, if you can live with how it looks. Where it tends to fall down is that not all tools recognize this extended form of the convention. So depending on which code analysers you use, and which editor you use, you might start seeing messages telling you about unused parameters even when you've signalled your intentions by using __. One widely used analyser was fixed to recognize double underscores, but didn't recognize the broader pattern, so triple, quadruple, or longer underscore sequences would continue to be misunderstood. In any case, it all looks rather messy when you start doing that.

Programming C# 10 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

If only these underscores were more than mere convention.

C# 9.0 introduced formal parameter discards

Starting with C# 9.0 (which was introduced with .NET 5.0) it became possible to write this sort of thing:

Func<int, int, int> f = (_, _) => 42;

With C# 8.0 and older, this was not allowed, for exactly the same reason that the first of the MyClickHandler examples above was forbidden, so with lambdas, multiple discards were typically indicated with ever-longer sequences of underscores. We used to have to write (_, __) => 42.

C# 9.0 made discarded lambda parameters a formal part of the language. Since the compiler understands that we intend not to use either of the two parameters in the example above, it no longer reports them as duplicate names.

Because this was a late addition to the language, things are not quite as neat as you might hope. There are a couple of issues with parameter discards:

  • Only lambdas support parameter discards
  • For backwards-compatibility, single discards are still treated as normal parameters named _

The first issue means that if you're using the normal method syntax, there's still no formal way to denote a discard—underscores remain a convention. That means that of the two MyClickHandler examples shown earlier, you need to use the second one even with C# 9.0 or 10.0, because the first still causes a CS0100 error.

The second issue mentioned above arises because _ has always been a valid identifier name in C#. You can use _ as the name of a parameter that you are in fact using, as in this example from earlier:

Func<int, int, int> f2 = (_, x) => _ * x;

The C# team didn't want to break existing code that did this, so this continues to mean what it always meant.

As a result of the discussion on Twitter, I discovered that I had misunderstood one detail of this. I had thought that the reason C# treated this as not being a discard was that the parameter is used. But I was wrong, because this would mean that in my very first example the unused _ would be treated as a discard. In fact, it isn't. The compiler doesn't look inside the lambda body to determine whether a parameter is a discard. It uses a simpler rule: if a lambda has exactly one parameter named _, then even if you don't use it, the compiler continues to treat it as a normal parameter.

This is a pretty pedantic point, because it makes no difference to the way the code behaves. The distinction is only visible to code using Roslyn, the compiler API. But it turns out Visual Studio does actually take this information from the compiler and can show it on screen. If you hover the mouse over a discard it will shows the text "(discard)" in the tooltip:

Visual Studio showing a tooltip with the text "(discard) int _" for the first parameter of a lambda where both parameters are called _

But if you hover over the _ in a lambda where only one parameter uses that name, you don't see that text, because it is not, strictly speaking, a discard:

Visual Studio showing a tooltip with the text "(parameter) int _" for the first parameter of a lambda where only the first parameters is called _

Why did the C# designers do this? It might seem unhelpful because it means that there are things in the code that the developer intended as discards, but which the Roslyn API will tell you are not discards. My guess is that the C# team were trying not to break existing code analysers that expect all lambda parameters to be actual parameters, and that don't even know what a discard is. (Those analysers could still break when they encounter code using the new feature, of course. But the important point is that they won't break on old code that doesn't use the new feature, something that could have happened if the C# compiler had retconned single underscores into discards. The C# team's basic goal here is that if you upgrade to a new version of the compiler, and you haven't yet modified any of your code to use any new language features, everything should continue to work as it did before.)

Symbol collisions

One other related topic came up in the thread: the potential collision between _ as a parameter name, and _ as the name of a local variable (or any other symbols that might have been in scope):

Bill is addressing two distinct points here, so to clarify, the sentence I'm interested in here is this one:

The _ is a discard unless there’s a variable in scope named _.

This doesn't seem to be true. C# is perfectly happy with this:

int _ = 99;
Func<int, int, int> f = (_, _) => 42;

And even if we wind back to C# 8.0, in which formal discards weren't a thing and you couldn't write the code above, you could still write this without problems:

int _ = 99;
int __ = 123;
Func<int, int, int> f = (_, __) => 42;

The lambda has two parameters named _ and __, and there are also two local variables in scope with the same names. It works fine.

I think the reason Bill raised this issue is that there was a time when you weren't allowed to write that. C# 8.0 is oldest language version in which that last example is legal. Before C# 8.0, lambdas were not allowed to define parameters with names that were the same as local variables in scope in the containing method. If we're talking about formal discards this is not strictly relevant: if you're using C# 7.2 or older, the _ is never a discard because parameter discards didn't exist until C# 9.0. So I take slight issue with his precise wording, but I think it's correct in spirit (for C# 7.2 or older) because the presence of a local variable named _ used to prevent you from using the convention. But this isn't true for C# 8.0 or later.

Summary

Technically, a parameter is only a discard if it's in a lambda where at least 2 params are called _ (and you need to be using C# >= 9.0 to be able to do this). A single _ is just a parameter called _. Double underscores (__) or longer sequences are never technically discards; these are just parameters called __, ___, etc. However, there is a long-standing convention that parameters with names consisting entirely of _ signal our intent to discard the parameter, even though the compiler processes them as ordinary parameters for backwards compatibility reasons. On C# 7 and older, lambda parameters couldn't share names with local variables, which could sometimes prevent you from using this convention, but that restriction no longer applies.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked in various aspects of computing, including computer networking, embedded real-time systems, broadcast television systems, medical imaging, and all forms of cloud computing. Ian is a Technical Fellow at endjin, and Microsoft MVP in Developer Technologies. He is the author of O'Reilly's Programming C# 10.0, and has written Pluralsight courses on WPF (and here) and the TPL. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects. Technology brings him joy.