Local functions were introduced in C# 7.0 but could not have attributes applied to them. This meant you could not use compiler-recognized attributes like [NotNullWhen], [DoesNotReturn] or [Conditional] on local functions, so a helper that the compiler needed to understand had to be a regular method.
C# 9.0 allows attributes on local functions and their parameters, bringing them to parity with regular methods.
Code
csharp
static void Validate(string name)
{
[DoesNotReturn]
static void ThrowHelperError(string paramName)
{
throw new ArgumentNullException(paramName);
}
if (name is null)
ThrowHelperError(nameof(name));
}
static bool TryParse(string input, [NotNullWhen(true)] out MyType? result)
{
[return: NotNullIfNotNull("s")]
static MyType? ParseCore(string? s) => s is null ? null : new MyType(s);
result = input is not null ? ParseCore(input) : null;
return result is not null;
}csharp
static void Validate(string name)
{
// Could not mark local functions with [DoesNotReturn]
// so they had to be promoted to regular private methods
if (name is null)
ThrowHelperError(nameof(name));
}
[DoesNotReturn]
private static void ThrowHelperError(string paramName)
{
throw new ArgumentNullException(paramName);
}Notes
NotNullIfNotNulltakes the name of the local function's own parameter. Use a string here becausenameofcannot see that parameter until extended nameof scope C# 11.0- The attribute is emitted onto the generated method even though a local function has no metadata-visible name, so the effect is compile-time only and cannot be read back with reflection
[Conditional]works here too: a call to a local function marked[Conditional("DEBUG")]is omitted entirely when the symbol is not defined, which is a useful way to keep a diagnostic helper out of release builds- The flow analysis attributes are the other main reason to want this -
[DoesNotReturn],[NotNull],[NotNullWhen],[MaybeNullWhen]and[NotNullIfNotNull]change what the compiler knows after the call - The attributes can be applied to the local function itself or to its parameters, as with a normal method
- Before C# 9 the workaround was to promote the helper to a private static method, which cost you the enclosing method's scope and made the helper reachable from the whole class