dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
18.97k stars 4.03k forks source link

"defer" statement #8115

Closed gafter closed 7 years ago

gafter commented 8 years ago

Swift recently added the defer statement. Would that make sense for C# and VB? in C# I imagine it would look something like

    {
        SomeType thing = Whatever...;
        defer {
            thing.Free();
        }
        // some code code using thing
    }

The idea is that the code in the defer block would be executed as the last action in the enclosing block. This would be the same as writing

    {
        SomeType thing = Whatever...;
        try
        {
            // some code code using thing
        }
        finally
        {
            thing.Free();
        }
    }
gafter commented 8 years ago

@HaloFour prints 2, just the same as if you wrote

void Foo() {
    int i = 1;
    try
    {
        i = 2;
    }
    finally
    {
        Console.WriteLine(i);
    }
}

(Unless you get an exception between the two assignments)

aL3891 commented 8 years ago

Oh, it would just defer to the end of the current block? i somehow thought it deferred to the end of the entire method... :) if its just the current block i think the debugging would be much more reasonable.

lachbaer commented 8 years ago

When flying over the code above, I think that it might be hard for 3rd party readers to always directly see the defer statement, esp. when the code block is more complex, has complicated algs, etc.

For a better visual experience I think that you should group the blocks where defer belongs to, e.g. using the use keyword:

void Foo() {
    use
    {
        // maybe some more statements
        var fileStream = File.OpenWrite(@"C:\users\svick\desktop\test.txt");
    }
    defer
    {
        fileStream.Close();
    }
}

or shorter:

void Foo() {
    use { 
        var fileStream = File.OpenWrite(@"C:\users\svick\desktop\test.txt"); 
    } defer { fileStream.Close(); }
}

So, the requirement would be that every defer must follow a directly preceding use block.

PS: you could use the more verbose do keyword instead of use, a lookahead behind the do-block would tell if it has a loop or a defer meaning.

PPS: adding the defer statement to the try statement additionally would then also be of use. With try exceptions can also caught by catch blocks, but in contrast to finally those blocks are always excecuted when leaving the function.

void Foo() {
    try { 
        var fileStream = File.OpenWrite(@"C:\users\svick\desktop\test.txt"); 
    }
    catch (IOException e) {
        Debugger.Log(1, "FileErrors", "Cannot open test.txt");
        return;
    }
    defer {   // outer block for defer is the Foo method
        if (fileStream != null) fileStream.Close();
    }
    // try block already ended, but fileStream is still open
    ReadFile(fileStream);
}

(I know that the sample doesn't make much sense and there are other, better ways, it's just a coding sample ;-)

migueldeicaza commented 8 years ago

Another thought on defer.

The real win here is that it can be used for things that are not IDisposable. And while there was a comment earlier from @HaloFour that he sees no validity in the argument, defer is not limited to resources that must be disposed, nor is every resource or operation that requires some finalization always surface a Dispose().

Defer instead introduces a new construct that can be used not only for releasing resources, but to ensure that certain operations take place before the code completes.

@gafter already provided a common idiom from the Roslyn compiler, but this is not limited to Roslyn, the idiom "var saved = GetState (); ChangeState (newstate); DoOperation (); RestoreState (saved)" is common.

Do not take my word for it, a search here, shows very interesting uses of defer and they are not all bound to releasing resources:

https://github.com/search?l=&o=desc&q=defer+language%3ASwift&ref=advsearch&s=indexed&type=Code&utf8=%E2%9C%93

HaloFour commented 8 years ago

@migueldeicaza

Quickly scanning those examples I can easily see that the vast majority of cases are Swift's reimplementations of using or lock. Surprisingly, much more the latter than the former. Of the remaining cases I mostly see bizarre ways of injecting mutation logic post return, e.g.:

  mutating func unsafePop() -> UTF8.CodeUnit {
    defer { pointer = pointer.advancedBy(1) }
    return pointer.memory
  }

I frankly don't see how that's more readable than the equivalent:

  mutating func unsafePop() -> UTF8.CodeUnit {
    let result = pointer.memory
    pointer = pointer.advanceBy(1)
    return result
  }

Sure, one less line of code, which buys you out-of-lexical-order execution of sequential statements.

I don't doubt that there are some really good novel uses for a statement like defer. I'm not seeing them in those examples. Nor do I think that the rare occasion where it might be useful warrants a language feature that is effectively an alias for try/finally. The idea of having to mentally keep track of implicit scope popping behavior when reading code does not appeal to me. Having to know when to read code backwards does not appeal to me. Encouraging people to write disposable resources that spurn the 15 year old established disposable pattern does not appeal to me.

bbarry commented 8 years ago

@migueldeicaza, @HaloFour I suspect by far the most common use case for a defer statement in C# would be lock management structures like ReaderWriterLockSlim changing code like this to:

public class Set<T>
{
  private readonly HashSet<T> set = new HashSet<T>();
  private readonly ReaderWriterLockSlim readerWriterLockSlim = new ReaderWriterLockSlim();

  public bool Add(T value)
  {
    readerWriterLockSlim.EnterReadLock();
    {
      defer { readerWriterLockSlim.ExitReadLock(); }
      if(set.Contains(value))
        return false;
    }

    readerWriterLockSlim.EnterWriteLock();
    defer { readerWriterLockSlim.ExitWriteLock(); }
    return set.Add(value);
  }

  public bool Remove(T value)
  {
    readerWriterLockSlim.EnterWriteLock();
    defer { readerWriterLockSlim.ExitWriteLock(); }
    return set.Remove(value);
  }

  public T[] GetValues()
  {
    readerWriterLockSlim.EnterReadLock();
    defer { readerWriterLockSlim.ExitReadLock(); }
    return set.ToArray();
  }
}

Here, the necessary naked block in the Add method makes me more uneasy about the thought of a defer statement than anything else I've seen so far. Other tasks I'd imagine are transaction commits.

Also, how should it play with a switch statement?

void Foo(int state)
{
  int i = 1;
  {
    switch (state)
    {
      case 0: defer {i++;} goto case 1;
      case 1: defer {i *= 2;} break;
      case 2: defer {i = 3;} goto case 1;
      default: defer {i = 5;} break;
    }
  }
  Console.WriteLine(i);
}

Or yield or await?

alrz commented 8 years ago

This prints a 0 b 1 because in Swift each case body has its own block.

let state = 0
switch (state) {
    case 0: defer {print("0")}; print("a"); fallthrough
    case 1: defer {print("1")}; print("b");
    case 2: defer {print("2")}; print("c");
    default: defer {print("3")}; print("d");
}

No idea how it should work in C# though.

gafter commented 7 years ago

Issue moved to dotnet/csharplang dotnet/roslyn#513 via ZenHub