By Jon Skeet

This article, taken from chapter 14 of C# in Depth, Fourth Edition, delves into using local methods in C#.

Save 37% off C# in Depth, Fourth Edition with code fccskeet at manning.com.


Local methods

If this weren’t C# in Depth, this article could be written on a napkin: you can write methods within methods. End of story. But, as this article is from C# in Depth, and we’re going to go deeper! Let’s start with a simple example. Listing 1 demonstrates a simple local method that prints and then increments a local variable declared within the Main method.

Listing 1. A simple local method which accesses a local variable

  
 static void Main()
 {
     int x = 10;                            ❶ 
     PrintAndIncrementX();                  ❷ 
     PrintAndIncrementX();                  ❷ 
     Console.WriteLine($"After calls, x = {x}");
  
     void PrintAndIncrementX()              ❸ 
     {                                      ❸ 
         Console.WriteLine($"x = {x}");     ❸ 
         x++;                               ❸ 
     }                                      ❸ 
 }
  

❶   Declare local variable used within method

❷   Call local method twice

❸   Local method

It’s a bit odd when you see this for the first time, but you soon get used to it. Local methods can appear anywhere you have a block of statements – methods, constructors, properties, indexers, event accessors, finalizers… even within anonymous functions or nested within another local method.

A local method declaration is like a normal method declaration, with the following restrictions:

  • It can’t have any access modifiers (like public)
  • It can’t have the extern, virtual, new, override, static or abstract modifiers
  • It can’t have any attributes (such as [MethodImpl]) applied to it
  • It can’t have the same name as another local method within the same “parent;” there’s no way to overload local methods

On the other hand, in other ways a local method acts like normal methods. For example:

  • It can be void or return a value
  • It can have the async modifier
  • It can have the unsafe modifier
  • It can be implemented via an iterator block
  • It can have parameters, including optional ones
  • It can be generic, it can have optional parameters
  • It can refer to any enclosing type parameters
  • It can be the target of a method group conversion to a delegate type

As shown in the example, it’s fine to declare the method after it’s used. Local methods can call themselves or other local methods which are in scope. Positioning can still be important though, largely in terms of how they refer to captured variables: local variables declared in the enclosing code but used in the local method.

Indeed, much of the complexity around local methods – both in language rules and implementation – revolves around the ability for them to both read and write captured variables. Let’s start off talking about the rules the language imposes.

Variable access within local methods

We’ve already seen that local variables in the enclosing block can be both read and written, but there’s more nuance to it than that.

A local method can only capture variables which are in scope

You can’t refer to a local variable outside its scope – which is, broadly speaking, “the block in which it’s declared.” For example, suppose you want your local method to use an iteration variable declared in a loop: the local method itself must be declared in the loop too. As a trivial example, this isn’t valid:

  
 static void Invalid()
 {
     for (int i = 0; i < 10; i++)
     {
         PrintI();
     }
  
     void PrintI() => Console.WriteLine(i);  ❶ 
 }
  

❶  : Unable to access i – it’s not in scope

But with the local method inside the loop, it’s valid[1]:

  
 static void Valid()
 {
     for (int i = 0; i < 10; i++)
     {
         PrintI();
  
         void PrintI() => Console.WriteLine(i);   ❶ 
     }
 }
  

❶  : Local method declared within loop; i is in scope

A local method must be declared after the declaration of any variables it captures

As you can’t use a variable earlier than its declaration in regular code, you can’t use a captured variable in a local method until after its declaration either. This rule is more for consistency than out of necessity; it’s feasible to specify the language to only require any calls to the method to occur after the variable’s declaration, for example – but it’s simpler to require all access to occur after declaration. Another trivial example of invalid code:

  
 static void Invalid()
 {
     void PrintI() => Console.WriteLine(i);   ❶ 
     int i = 10;
     PrintI();
 }
  

❶  : CS0841: Can’t use local variable i before it’s declared

Moving the local method declaration to after the variable declaration (whether before or after the PrintI() call) fixes the error.

A Local method can’t capture ref parameters of the enclosing method

As with anonymous functions, local methods aren’t permitted to refer to reference parameters of their enclosing method. For example, this is invalid:

  
 static void Invalid(ref int p)
 {
     PrintAndIncrementP();
     void PrintAndIncrementP() => Console.WriteLine(p++);   ❶ 
 }
  

❶  : Invalid access to reference parameter

The reason for this prohibition for anonymous functions is that the delegate created might outlive the variable being captured. In most cases this reason doesn’t apply to local methods, but as we’ll see later it’s possible for local methods to have the same kind of issue. In most cases, you can work around this by declaring an extra parameter in the local method, and passing the reference parameter by reference again:

  
 static void Valid(ref int p)
 {
     PrintAndIncrement(ref p);
     void PrintAndIncrement(ref int x) => Console.WriteLine(x++);
 }
  

If you don’t need to modify the parameter within the local method, you can make it a value parameter instead.

As a corollary of this restriction (again, mirroring a restriction for anonymous functions), local methods declared within structs can’t access this. Imagine that this is an implicit extra parameter at the start of every instance method’s parameter list. For class methods, it’s a value parameter; for struct methods it’s a reference parameter. Therefore, you can capture this in local methods in classes, but not in structs. The same workaround applies as for other reference parameters1.

Local methods interact with definite assignment

The rules of definite assignment in C# are complicated, and local methods complicate them further. The simplest way to think about it’s as if the method were inlined at any point where it’s called. That impacts assignment in two ways.

Firstly, if a method that reads a captured variable is called before it’s definitely assigned, that causes a compile-time error. Here’s an example which tries to print the value of a captured variable in two places: once before it’s been assigned a value, and once afterwards.

  
 static void AttemptToReadNotDefinitelyAssignedVariable()
 {
     int i;
     void PrintI() => Console.WriteLine(i);
     PrintI();  ❶ 
     i = 10;
     PrintI();  ❷ 
 }
  

❶  : CS0165: Use of unassigned local variable ‘i’

❷  : No error: i is definitely assigned here

Notice how it’s the location of the call to PrintI that causes the error here; the location of the method declaration is fine. If you move the assignment to i before any calls to PrintI(), this is fine – even if it’s still after the declaration of PrintI().

Secondly, if a local method writes to a captured variable in all possible execution flows, then the variable is assigned at the end of any call to that method. Here’s an example which assigns a value within a local method, but then reads it within the containing method:

  
 static void DefinitelyAssignInMethod()
 {
     int i;
     AssignI();                 ❶ 
     Console.WriteLine(i);      ❷ 
     void AssignI() => i = 10;  ❸ 
 }
  

❶  : Call to the method makes i definitely assigned…

❷  : … it’s fine to print it out

❸  : Method performs the assignment

A couple of final points need to be made about local methods and variables – this time not captured variables, but fields.

Local methods can’t assign read-only fields

Read-only fields can only be assigned values in field initializers or constructors. That rule doesn’t change with local methods, but it’s made a little stricter: even if a local method is declared within a constructor, it doesn’t count as being “inside” the constructor in terms of field initialization. This code is invalid:

  
 class Demo
 {
     private readonly int value;
  
     public Demo()
     {
         AssignValue();
         void AssignValue()
         {
             value = 10;  ❶ 
         }
     }
 }
  

❶  : Invalid assignment to read-only field

This restriction isn’t likely to be a significant problem, but it’s worth being aware of. It stems from the fact that the CLR hasn’t had to change in order to support local methods. They’re a compiler transformation… which leads us on to considering exactly how the compiler does implement local methods, particularly with respect to captured variables. That, however, is a topic for a different article.

That’s all for this article. For more, read chapter 6 of C# in Depth, Fourth Edition for free, and see this Slideshare presentation for more info.

 

[1] It may be a little strange to read, but it’s valid.