|
From C# in Depth, Fourth Edition by Jon Skeet This article, taken from chapter 14 of C# in Depth, Fourth Edition, delves into using local methods in C# to write more concise code. |
Save 37% on C# in Depth, Fourth Edition. Just enter code fccskeet into the discount code box at checkout 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
orabstract
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.
If you want to learn more about the book, check it out on liveBook here and see this Slideshare presentation.
[1] It may be a little strange to read, but it’s valid.