Search code examples
c#refc#-7.0nested-functionlocal-functions

With c# why are 'in' parameters not usable in local functions?


For example,

public int DoSomething(in SomeType something){
  int local(){
     return something.anInt;
  }
  return local();
}

Why does the compiler issue an error that the something variable cannot be used in the local function?


Solution

  • The documentation on local functions states the following

    Variable capture

    Note that when a local function captures variables in the enclosing scope, the local function is implemented as a delegate type.

    And looking at lambdas:

    Capture of outer variables and variable scope in lambda expressions

    A lambda expression can't directly capture an in, ref, or out parameter from the enclosing method.

    The reason is simple: it's not possible to lift these parameters into a class, due to ref escaping problems. And that is what would be necessary to do in order to capture it.

    Example

    public Func<int> DoSomething(in SomeType something){
      int local(){
         return something.anInt;
      }
      return local;
    }
    

    Suppose this function is called like this:

    public Func<int> Mystery()
    {
        SomeType ghost = new SomeType();
        return DoSomething(ghost);
    }
    
    public void Scary()
    {
        var later = Mystery();
        Thread.Sleep(5000);
        later(); // oops
    }
    

    The Mystery function creates a ghost and passes it as an in parameter to DoSomething, which means that it is passed as a read-only reference to the ghost variable.

    The DoSomething function captures this reference into the local function local, and then returns that function as a Func<int> delegate.

    When the Mystery function returns, the ghost variable no longer exists. The Scary function then uses the delegate to call the local function, and local will try to read the anInt property from a nonexistent variable. Oops.

    The "You may not capture reference parameters (in, out, ref) in delegates" rule prevents this problem.

    You can work around this problem by making a copy of the in parameter and capturing the copy:

    public Func<int> DoSomething2(in SomeType something){
      var copy = something;
      int local(){
         return copy.anInt;
      }
      return local;
    }
    

    Note that the returned delegate operates on the copy, not on the original ghost. It means that the delegate will always have a valid copy to get anInt from. However, it means that any future changes to ghost will have no effect on the copy.

    public int Mystery()
    {
        SomeType ghost = new SomeType() { anInt = 42 };
        var later = DoSomething2(ghost);
        ghost = new SomeType() { anInt = -1 };
        return later(); // returns 42, not -1
    }