Proposal: Destructible Types #161

Open
stephentoub opened this Issue Jan 29, 2015 · 82 comments

Projects

None yet
@stephentoub
Member

Background

C# is a managed language. One of the primary things that's “managed” is memory, a key resource that programs require. Programs are able to instantiate objects, requesting memory from the system, and at some point later when they're done with the memory, that memory can be reclaimed automatically by the system's garbage collector (GC). This reclaiming of memory happens non-deterministically, meaning that even though some memory is now unused and can be reclaimed, exactly when it will be is up to the system rather than being left to the programmer to determine.
Other languages, in particular those that don't use garbage collection, are more deterministic in when memory will be reclaimed. C++, for example, requires that developers explicitly free their memory; there is typically no GC to manage this for the developer, but that also means the developer gets complete control over when resources are reclaimed, as they're handling it themselves.

Memory is just one example of a resource. Another might be a handle to a file or to a network connection. As with any resource, a developer using C++ needs to be explicit about when such resources are freed; often this is done using a “smart pointer,” a type that looks like a pointer but that provides additional functionality on top of it, such as keeping track of any outstanding references to the pointer and freeing the underlying resource when the last reference is released.

C# provides multiple ways of working with such “unmanaged” resources, resources that, unlike memory, are not implicitly managed by the system. One way is by linking such a resource to a piece of memory; since the system does know how to track objects and to release the associated memory after that object is no longer being referenced, the system allows developers to piggyback on this and to associate an additional piece of logic that should be run when the object is collected. This logic, known as a “finalizer,” allows a developer to create an object that wraps an unmanaged resource, and then to release that resource when the associated object is collected. This can be a significant simplification from a usability perspective, as it allows the developer to treat any resource just as it does memory, allowing the system to automatically clean up after the developer.

However, there are multiple downsides to this approach, and some of the biggest reliability problems in production systems have resulted from an over-reliance on finalization. One issue is that the system is managing memory, not unmanaged resources. It has heuristics that help it to determine the appropriate time to clean up memory based on the system's understanding of the memory being used throughout the system, but such a view of memory doesn't provide an accurate picture about any pressures that might exist on the associated unmanaged resources. For example, if the developer has allocated but then stopped using a lot of file-related objects, unless the developer has allocated enough memory to trigger the garbage collector to run, the system will not know that it should run the garbage collector because it doesn't know how to monitor the “pressure” on the file system. Over the years, a variety of techniques have been developed to help the system with this, but none of them have addressed the problem completely. There is also a performance impact to abusing the GC in this manner, in that allocating lots of finalizable objects can add a significant amount of overhead to the system.

The biggest issue with relying on finalizers is the non-determinism that results. As mentioned, the developer doesn't have control over when exactly the resources will be reclaimed, and this can lead to a wide variety of problems. Consider an object that's used to represent a file: the object is created when the file is opened, and when the object is finalized, the file is closed. A developer opens the file, manipulates it, and then releases the object associated with it; at this point, the file is still open, and it won't be closed until some non-deterministic point in the future when the system decides to run the garbage collector and finalize any unreachable objects. In the meantime, other code in the system might try to access the file, and be denied, even though no one is actively still using it.

To address this, the .NET Framework has provided a means for doing more deterministic resource management: IDisposable. IDisposable is a deceptively simple interface that exposes a single Dispose method. This method is meant to be implemented by an object that wraps an unmanaged resource, either directly (a field of the object points to the resource) or indirectly (a field of the object points to another disposable object), which the Dispose method frees. C# then provides the 'using' construct to make it easier to create resources used for a particular scope and then freed at the end of that scope:

using (var writer = new StreamWriter("file.txt")) { // writer created
    writer.WriteLine("hello, file");
}                                                   // writer disposed

Problem

While helpful in doing more deterministic resource management, the IDisposable mechanism does suffer from problems. For one, there's no guarantee made that it will be used to deterministically free resources. You're able to, but not required to, use a 'using' to manage an IDisposable instance.

This is complicated further by cases where an IDisposable instance is embedded in another object. Over the years, FxCop rules have been developed to help developers track cases where an IDisposable goes undisposed, but the rules have often yielded non-trivial numbers of both false positives and false negatives, resulting in the rules often being disabled.

Additionally, the IDisposable pattern is notoriously difficult to implement correctly, compounded by the fact that because objects may not be deterministically disposed of via IDisposable, IDisposable objects also frequently implement finalizers, making the pattern that much more challenging to get right. Helper classes (like SafeHandle) have been introduced over the years to assist with this, but the problem still remains for a large number of developers.

Solution: Destructible Types

To address this, we could add the notion of "destructible types" to C#, which would enable the compiler to ensure that resources are deterministically freed. The syntax for creating a destructible type, which could be either a struct or a class, would be straightforward: annotate the type as 'destructible' and then use the '~' (the same character used to name finalizers) to name the destructor.

public destructible struct OutputMessageOnDestruction(string message)
{
    string m_message = message;

    ~OutputMessageOnDestruction() // destructor
    {
        if (message != null)
            Console.WriteLine(message);
    }
}

An instance of this type may then be constructed, and the compiler guarantees that the resource will be destructed when the instance goes out of scope:

public void Example()
{
    var omod = new OutputMessageOnDestruction("Destructed!");
    SomeMethod();
} // 'omod' destructed here

No matter what happens in SomeMethod, regardless of whether it returns successfully or throws an exception, the destructor of 'omod' will be invoked as soon as the 'omod' variable goes out of scope at the end of the method, guaranteeing that “Destructed!” will be written to the console.

Note that it's possible for a destructible value type to be initialized to a default value, and as such the destruction could be run when none of the fields have been initialized. Destructible value type destructors need to be coded to handle this, as was done in the 'OutputMessageOnDestruction' type previously by checking whether the message was non-null before attempting to output it.

public void Example()
{
    OutputMessageOnDestruction omod = default(OutputMessageOnDestruction);
    SomeMethod();
} // default 'omod' destructed here

Now, back to the original example, consider what would happen if 'omod' were stored into another variable. We'd then end up with two variables effectively wrapping the same resource, and if both variables were then destructed, our resource would effectively be destructed twice (in our example resulting in “Destructed!” being written twice), which is definitely not what we want. Fortunately, the compiler would ensure this can't happen. The following code would fail to compile:

OutputMessageOnDestruction omod1 = new OutputMessageOnDestruction("Destructed!");
OutputMessageOnDestruction omod2 = omod1; // Error: can't copy destructible type

The compiler would prevent such situations from occurring by guaranteeing that there will only ever be one variable that effectively owns the underlying resource. If you want to assign to another variable, you can do that, but you need to use the 'move' keyword (#160) to transfer the ownership from one to the other; this effectively performs the copy and then zeroes out the previous value so that it's no longer usable. In compiler speak, a destructible type would be a "linear type," guaranteeing that destructible values are never inappropriately “aliased”.

OutputMessageOnDestruction omod1 = new OutputMessageOnDestruction("Destructed!");
OutputMessageOnDestruction omod2 = move omod1; // Ok, 'omod1' now uninitialized; won't be destructed

This applies to passing destructible values into method calls as well. In order to pass a destructible value into a method, it must be 'move'd, and when the method's parameter goes out of scope when the method returns, the value will be destructed:

void SomeMethod(OutputMessageOnDestruction omod2)
{
    ...
} // 'omod2' destructed here
...
OutputMessageOnDestruction omod1 = new OutputMessageOnDestruction("Destructed!");
SomeMethod(move omod1); // Ok, 'omod1' now uninitializedl; won't be destructed

In this case, the value needs to be moved into SomeMethod so that SomeMethod can take ownership of the destruction. If you want to be able to write a helper method that works with a destructible value but that doesn't assume ownership for the destruction, the value can be passed by reference:

void SomeMethod(ref OutputMessageOnDestruction omod2)
{
   ...
} // 'omod2' not destructed here
…
OutputMessageOnDestruction omod1 = new OutputMessageOnDestruction("Destructed!");
SomeMethod(ref omod1); // Ok, 'omod1' still valid

In addition to being able to destructively read a destructible instance using 'move' and being able to pass a destructible instance by reference to a method, you can also access fields of or call instance methods on destructible instances. You can also store destructible instances in fields of other types, but those other types must also be destructible types, and the compiler guarantees that these fields will get destructed when the containing type is destructed.

destructible struct WrapperData(SensitiveData data)
{
    SensitiveData m_data = move data; // 'm_data' will be destructed when 'this' is destructed
    …
}
destructible struct SensitiveData { … }

There would be a well-defined order in which destruction happens when destructible types contain other destructible types. Destructible fields would be destructed in the reverse order from which the fields are declared on the containing type. The fields of a derived type are destructed before the fields of a base type. And user-defined code runs in a destructor before the type's fields are destructed.

Similarly, there'd be a well-defined order for how destruction happens with locals. Destructible locals are destructed at the end of the scope in which they are created, in reverse declaration order. Further, destructible temporaries (destructible values produced as the result of an expression and not immediately stored into a storage location) would behave exactly as a destructible locals declared at the same position, but the scope of a destructible temporary is the full expression in which it is created.

Destructible locals may also be captured into lambdas. Doing so results in the closure instance itself being destructible (since it contains destructible fields resulting from capturing destructible locals), which in turn means that the delegate to which the lambda is bound must also be destructible. Just capturing a local by reference into a closure would be problematic, as it would result in a destructible value being accessible both to the containing method and to the lambda. To deal with this, closures may capture destructible values, but only if an explicit capture list (#117) is used to 'move' the destructible value into the lambda (such support would also require destructible delegate types):

OutputMessageOnDestruction omod = new OutputMessageOnDestruction("Destructed!");
DestructibleAction action = [var localOmod = move omod]() => {
    Console.WriteLine("Action!");
}

The destructible types feature would enable a developer to express some intention around how something should behave, enabling the compiler to then do a lot of heavy lifting for the developer in making sure that the program is as correct-by-construction as possible. Developers familiar with C++ should feel right at home using destructible types, as it provides a solid Resource Acquisition Is Initialization (RAII) approach to ensuring that resources are properly destructed and that resource leaks are avoided.

@scalablecory

I would prefer a way that allows me to apply this not just to new types but to existing code.

One issue I see is this essentially mimics std::unique_ptr, but without the ability to get() a reference that can easily exist in multiple places.

These two concerns are showstoppers for me and so I would not vote to include this in its current form.

Another issue as-is with this proposal is that when reading code, there's no obvious way to determine that a variable will have side effects when it goes out of scope. If there's not a move right next to it, there'd be no way to know.

I believe something closer to this would get about 90% of the way there and be a lot more usable: say a way to mark an IDisposable as unique, and keywords to move() and get() it:

var !con = new SqlConnection(...);
var !cmd = con.CreateCommand();
var !reader = cmd.ExecuteReader();

DbDataReader errorReader = reader; // error: did not move or get.
DbDataReader !movedReader = move reader; // nulls out reader
DbDataReader normalReader = get reader; // preserves movedReader

This would give scope-bound deterministic disposal, single ownership safety, obvious code readability, be immediately useful when using existing code, and I believe could be implemented without changing the VM similar to Nullable.

Additional safety regarding single ownership could be had by adding a weak reference designator that forces explicit ownership to exist elsewhere:

DbDataReader ^weakReader = reader; // implicit.

But, this may be of limited usefulness if "get reader" allows getting a raw instance. (Which, I think is very important to allow)

@ryancerium

What if you used C++ stack allocation construction syntax to visually separate garbage collected objects from destructible objects? Would that allow any object to be destructible then? I think you'd need CLR support at that point though.

OutputMessageOnDestruction omod("Hello world!");
@RichiCoder1

👍
@scalablecory Correct me if I'm wrong, but how much call is there for something equivalent to get()? What scenarios would it allow outside just ref passing into methods? Having it be very strict seems a plus in C# specifically.

@svick
Contributor
svick commented Jan 29, 2015

If you want to be able to write a helper method that works with a destructible value but that doesn't assume ownership for the destruction, the value can be passed by reference

Would this work together with readonly parameters (#115), so that I can create a method that works with the destructible value, but can't "steal" it, by making the parameter readonly ref?

You can also store destructible instances in fields of other types, but those other types must also be destructible types

Does that mean that array or List<T> of destructible type wouldn't be allowed? And that you would need something like DestructibleSinglyLinkedList<T> to have a collection of them?

@MrJul
MrJul commented Jan 29, 2015

That's definitely something I want to see, and reminiscent of Rust with its compile-time checking. Generalized enough, one can imagine a core runtime that almost doesn't need the GC at all.

What about returning a destructible type? Would it need to be moved explicitly, clearly expressing giving the ownership to the caller:

destructible class Destructible { }

Destructible CreateDestructible()
{
    var value = new Destructible();
    return move value;
}

Or would it be implicit?

Destructible CreateDestructible() {
    var value = new Destructible();
    return value; // ownership automatically transferred to caller
}

If the caller doesn't use the returned value, is it destructed immediately, or only at the end of the current scope?

void SomeLongMethod() {
    CreateDestructible();
    // is the return value destructed already?
    Thread.Sleep(60000);
} // or is it only now, a minute later?
@scalablecory

@RichiCoder1 from C++ experience, it is not uncommon to have one "owner" object and several other objects that still need to use the instance somehow. I can see the same need here.

Here's an exercise. Say FileStream was made destructible. I want to use it like such:

FileStream fs = new FileStream(...);

await WriteA(fs);
await WriteB(fs);

I still want to allow these other methods (and any objects they allocate in their use of it) to use it while maintaining strict lifetime control at a top level. A ref param won't work here if Write() is an async method that needs to put the FileStream into its state machine object.

@jaredpar
Member

@ryancerium

Would that allow any object to be destructible then?

No. One of the goals of this proposal is to have destruction be deterministic and getting that requires the implementation of the type obey certain restrictions. Hence it wouldn't be possible to make any type destructible simply by changing its construction syntax.

@Mr-Byte
Mr-Byte commented Jan 29, 2015

👍

This is one I've thought about several times, as having deterministic management of resources would be extremely useful in a lot of areas, such as scientific programming and game development.

Could this potentially be used to allow for deterministic management of heap allocated memory? Effectively implementing the C++ std::unique_ptr in C# for more deterministic memory management.

Can these types be stored in collections? I noticed the proposal requires that an object containing fields that are destructible must itself be destructible? How would that work with arrays and collections?

@ryancerium

@jaredpar The new construction syntax idea was simply so that you could tell how objects are allocated at the point of allocation.

A a = new A(); // Destructible?
B b(); // Destructible!

C# has done a very good job of making things explicit at the call site and not just at the declaration site; ref and out parameters in particular come to mind. (That has long been a problem in C++ to the point that the recommendation is that out parameters be raw pointers and in parameters be references. Raw pointers! What is this, 1986?)

What are the other "certain restrictions"? Why couldn't the CLR initialize any given object type on stack memory instead of heap allocated memory and have the finalizer run when it goes out of scope if it hasn't been moved? There's lambda capture to worry about I suppose, and concurrent modification, neither of which is anything remotely resembling easy.

@jaredpar
Member

@ryancerium

I agree that being able to distinguish between the two types here is important. In the past we did consider taking a page out of the F# book here and doing the following:

use var A a = new A();  // destructible

The overall feeling when we did this was mixed. Sure it made the construction more obvious but there was also concern it was adding too much verbosity to the code. Eventually we ripped it out in favor of finding a better solution later on.

Why couldn't the CLR initialize any given object type on stack memory instead of heap allocated memory and have the finalizer run when it goes out of scope if it hasn't been moved?

It's not just moving that is a problem but even simple aliasing. If the implementation of a method should put this into the heap somewhere then it would be possible to have a destructed object floating around in what appeared to be a non-destructed state.

@ryancerium

@jaredpar

it would be possible to have a destructed object floating around in what appeared to be a non-destructed state.

Good point, I take it for granted sometimes that C++ let's people do stupid things if they really feel like it. I assume you meant the implementation of a destructible object's method, so if you put this on the heap, wouldn't that invoke move semantics? @scalablecory has an excellent use case for using references to a destructible object, so it's going to be tricky not matter what.

I really like your sample syntax also, with a minor tweak. If that caused mixed feelings, I don't know what to tell you :-)

use a = new A(); // destructible shorthand, implicit var
use A a = new A(); // destructible longhand, explicit type
@RichiCoder1

Maybe somehow allow destructible objects to be boxed and passed like normal somehow? Though that seems like it would have it's own flaws. And lists and arrays bring up their own problem. Especially if you wanted something like a destructible array.

@jaredpar
Member

@RichiCoder1

Boxing is definitely an option we explored and feel is necessary for completeness. We even called it simply Box<T>. The basic summary of it was:

  • A box can be empty or have a value
  • A box has a finalizer and it will destruct the contents of the box if it's not empty
  • The contents of the box can be moved out of it
@RichiCoder1

@jaredpar Sounds great an exactly what I was thinking about. I think a read a previous article about discussion around a destructible types internal. Maybe piggy back on other ideas in this thread and follow Nullable<T> and have OutputMessageOnDestruction! be shorthand for Box<T>. Something like.

File! file = File.GetFile("....."); // <--- Implicitly is boxed up.
files.Add(file);

Random thought; Possibly include generic constraint T : destructible so that you could do something like:

public destructible class DestructibleList<T> : IList<T> where T : destructible 
{
    // .. implementation
}
public void ProcesFiles(string folderName)
{
    DirectoryInfo di = new DirectoryInfo(folderName);
    use DestructibleList<File> files = di.GetFiles("*");
    foreach(File! file in files)
    {
         // ... do stuff
    }
    // ...files go away here.
}

Though that raises the next question is how would a list of destructible work with something like IEnumerable?

Addendum: How would destructible types play with async?

@tomasr
tomasr commented Jan 30, 2015

Overall, I like the proposal, but a couple of things are not entirely clear to me.... would someone mind expanding on this:

  • What would the code generated for a destructible type look like? Would such a type implement IDisposable implicitly, or something else? would it also generate a finalizer (it sounds like it wouldn't, but curious nonetheless)
  • How would such types interact with other classes not part of the actual code?
@ufcpp
ufcpp commented Jan 30, 2015

A scope-based single-owned destruction is good idea in a certain situation, I like it, but there might be still many situations that the proposal can not solve.
I need the IDisposable for unsubscribing events more often than for managing sensitive resources. This kind of IDisposable could not be destructible, because typically it is nether in a method scope nor owned by a destructible object.

@sharwell
Member

I was originally concerned that destructable types could require VM changes which would have a negative impact on the performance of the garbage collector. I no longer believe that is the case. My current understanding of the proposal is the following (with the exception of lambda considerations):

  • A destructable type is a struct which implicitly implements IDisposable. For the points that follow say this is struct D.
  • An instance of D cannot be copied by value.
    • Assigning an instance of D to another location requires setting the original instance to default(D).
    • A method can have a parameter ref D obj or out D obj, which is used like any other ref or out parameter. However, if a method has a parameter D obj, it can only be used in conjuction with a transfer of ownership (e.g. by requiring the use of the move operator at the call site).
    • D cannot be used as a generic type argument (like a raw pointer in this respect).
    • D cannot be boxed (compiler enforced).
  • default(D).Dispose() is a NOP.
  • In D.Dispose(), fields in D which are destructable are disposed in the opposite order in which they are declared.
  • Any declaration of a variable D which is not a field is implicitly wrapped in a using block which is closed at the end of the containing block.
  • A field in type D2 may only have type D if D2 is also destructable.

The above rules could be expanded to support a destructable class:

  • A destructable class is syntactic sugar for creating a destructable struct which wraps an instance of a reference type which implements IDisposable.
  • Suppose you have destructable class T
    • The generated wrapper would look like:

        destructable struct TWrapper { public T _instance; }
    • The compiler treats a field or parameter of type T as though it were actually a field or parameter of type TWrapper.

    • If a class TDerived extends T, then TDerived must also be declared destructable.

    • A destructable class cannot have a user-defined finalizer, because a destructable class cannot be used in a context where the call to Dispose() is omitted.

The above is surely incomplete but may serve as a starting point for defining the semantics of destructable types.

@sharwell
Member

Regarding Box<T> - I could see a reference type Box<T> existing for the purpose of using a destructable type in a context where destructable types are generally not allowed, e.g. as a field in a non-destructable type.

destructable struct D { }

class T : IDisposable
{
  D _value1; // error
  Box<D> _value2; // allowed
  Box<T> _value3; // error - T is not destructable
}

When a destructable value is placed into a "box", ownership is assigned to that box. Box<T> implements IDisposable, but more importantly it has a user-defined finalizer. Use of Box<T> would be generally discouraged, but use of destructable types in general would be hindered if Box<T> was not provided, because all currently existing code uses non-destructable types and therefore could not hold instances of these new types.

@MrJul
MrJul commented Jan 30, 2015

@sharwell I assumed by reading the proposal that a destructible type doesn't implement IDisposable. The nice thing about destructible types is that only the runtime can destruct an instance (when nothing is using it anymore), and it guaranties that it happens only once. IDisposable doesn't have those guarantees.

@svick
Contributor
svick commented Jan 30, 2015

@sharwell

A method can have a parameter ref D obj, but it cannot have D obj.

Why not? @stephentoub's proposal contains this:

var omod1 = new OutputMessageOnDestruction("Destructed!");
SomeMethod(move omod1); // Ok, 'omod1' now uninitializedl; won't be destructed

I think this is reasonable, i.e. if the parameter is not ref, it requires explicit transfer of ownership.

D cannot be used as a generic type argument (like a raw pointer in this respect).

So, collections of destructible types wouldn't be allowed (at least not without Box)? I think having those would be very useful.

@RichiCoder1

@sharwell what @MrJul said. The implementation looks like something completely seperated from IDisposable. This would be more than than just syntactic sugar, but a completely new behavior.

I also agree with @svick in his assessment of your points.

@sharwell
Member

Use of destructable types in an array poses and interesting, but not insurmountable, challenge. Consider the following:

destructable struct D { }

...

D[] values = new D[10];

In this scenario, values is (semantically) treated as a destructable class (see previous post) containing 10 sequential instances of D. The implementation could actually emit this in metadata using the type D[], provided instances of this type are only used in contexts where destructable types are allowed. Perhaps a value type destructable struct DestructableArray<T> where T : destructable could exist in System.Runtime.CompilerServices to assist in the use of these types with the using construct. 💭

@sharwell
Member

@MrJul and @RichiCoder1: The implementation of IDisposable does not need to be exposed to the user. It is an implementation detail allowing destructable types in C# to be "lowered" to set of features provided by the underlying VM without requiring new runtime support. While IDisposable could be replaced by another equivalent type, simply using IDisposable allows the feature to be easily defined in terms of the existing using statement. Remember that if you can't box an instance of a destructable type, then you also cannot cast an instance of a destructable type to IDisposable.

@sharwell
Member

I think this is reasonable, i.e. if the parameter is not ref, it requires explicit transfer of ownership.

I agree, I updated my post above.

@sharwell
Member

So, collections of destructible types wouldn't be allowed (at least not without Box<T>)? I think having those would be very useful.

Right now I don't see a way to provide destructable guarantees for existing generic data types. Perhaps if we introduce a destructable generic type constraint, then a destructable type could be used for this parameter (or rather, must be used). Then you could require that use of the destructable type follows the semantic requirements for destructable types elsewhere.

@jaredpar
Member

@tomasr

The generated code for a destructible type would not have an IDisposable implementation or a Finalizer. These features exist to support non-deterministic destruction and add extra overhead to the runtime (in particular the finalizer). A destructible type would be deterministically destructed and hence these are not needed.

@RichiCoder1

@sharwell The point would to match C++ behavior here, if I'm understanding correctly. Where lifetime is deterministic, rather than using using. Essentially unique_ptr<T> but built into the language, with Box<T> essential being shared_ptr<T>.

And in regards to collections, I mentioned above adding a destructable generic type constraint, and destructible collections.

Edit: what @jaredpar said :)

@sharwell
Member

The generated code for a destructible type would not have an IDisposable implementation...

Why would it not? Implementing IDisposable would make it easier to implement the Box<T> type (which would appear in metadata with the where T : IDisposable constraint). It would also make lowering generic types with the destructable generic type constraint, because it could be easily represented as IDisposable in metadata.

@jaredpar
Member

@sharwell

A destructable type is a struct which implicitly implements IDisposable. For the points that follow say this is struct D.

No. IDisposable exists to support non-determistic destruction. A destructible type is always deterministically destructed and hence has no need of this interface.

D cannot be used as a generic type argument (like a raw pointer in this respect).

Correct. But Box<D> can always be used as a generic type argument.

default(D).Dispose() is a NOP.

No, this must run the destructor.

When we first implemented this feature we tried to add this exact behavior. Unfortunately it's just not really possible without some prohibitive changes to the runtime. The core problem is that it's impossible to tell the difference between:

(new D())
default(D)

If D has a side effect in the destructor the developer would sure expect it to run in the first case. Because we can't distinguish between the two, especially when you get to structs in other places besides locals, we ended up default to always running the dtor.

A destructable class is syntactic sugar for creating a destructable struct which wraps an instance of a reference type which implements IDisposable.

No IDisposable on classes either.

@sharwell
Member

The point would to match C++ behavior here, if I'm understanding correctly. Where block scope determines the lifetime, rather than using.

ECMA-335 does not provide for block-scoped destruction of data types. Lowering the language concept to the existing runtime requires defining the feature in terms of functionality it does provide. This means it could either be defined using try { ... } finally { ... }, or you could simply reuse the existing using construct as an intermediate step because it's already defined this way.

The using statement would not actually appear in code, but it is a handy way of explaining the semantics of destructable types in terms that C# developers already understand.

@sharwell
Member

No. IDisposable exists to support non-determistic destruction.

IDisposable supports non-deterministic destruction, but that is not its sole purpose. Otherwise the using statement would not have been written in terms of IDisposable. Also, IDisposable is used by the C++ compiler in support of deterministic destruction.

object.Finalize(), on the other hand, exists solely for the purpose of non-deterministic cleanup.

@tomasr
tomasr commented Jan 30, 2015

@jaredpar Sounds reasonable.

I'm not a big fan of reusing the destructor syntax to define the cleanup code, however, given it already has other, pretty well misunderstood behavior (and by that I mean developers implementing finalizers when they are not needed).

It ends up making the process more complex, and as someone who spends a lot of time reading other people's code, I hate having to look at one piece of a class, and then possibly having to scroll tens or hundreds of line to look for a single word somewhere else that modifies the behavior of the code I'm seeing.

Just my $0.0000002 :)

@sharwell
Member

default(D).Dispose() is a NOP.

No, this must run the destructor.

Interesting use case. How would you define the move operator for destructable value types, if even default instances of the value type must be destructed? Perhaps by always using them as Nullable<D> instead of just D?

@jaredpar
Member

@sharwell

We ended up writing the language rules such that it could optimize away calling the destructor in cases like return move. Essentially the rule said:

The compiler can elide destructor calls for locations which were the target of a move and not assigned to afterwards.

@sharwell
Member

@tomasr I agree with you here. One of the first things I look for in a new project is finalizers, because they are easy to search for and almost always incorrectly used. I suppose I could create a new analyzer to find them (and distinguish them from the ones in destructable types), but still...

@jaredpar
Member

@sharwell sure IDisposable can work both ways. Destructible types though do not, they are only ever deterministically destructed. Why implement an interface which suggests it can be used two ways when it can only ever be correctly used one way?

@RichiCoder1

It ends up making the process more complex, and as someone who spends a lot of time reading other people's code, I hate having to look at one piece of a class, and then possibly having to scroll tens or hundreds of line to look for a single word somewhere else that modifies the behavior of the code I'm seeing.

This would be a situation where you'd want to have some sort of stylecop ensuring that the destructor is only being used as a destructor, or that it's being using paired with IDisposable. Maybe add a compiler warning against using a finalizer without the destructible keyword or IDisposable.

@sharwell
Member

@jaredpar If we're talking deterministic cleanup of uniquely owned resources, then to the maximum extent supported by the runtime environment I would expect to see exactly one call to the finalizer for each created instance of the type. It seems the language quoted there would not meet my expectations for the following code:

destructable struct D { }
destructable struct D2
{
  D _value;
  static void Method(D d) { }
  void Things()
  {
    Method(move _value);
  }
}

...

{
  // this block creates one instance of D which is destructed twice
  D2 instance;
  instance.Things();
}
@jaredpar
Member

@sharwell

// this block creates one instance of D which is destructed twice

The language doesn't agree with this comment. The move command definitely assigns a new value to the location after reading the original one. This value is a valid instance of D and follows all other language rules.

I agree the nature of default instances of struct values being destructed is unnerving at first. This is an issue we discussed at length and what we came to is the following:

  • It is mainly a concern for destructors that have side effects that are not related to the contents of the type. For example unconditionally writing messages to the console. If that is a function of the type then it is better served as a class. This would be true even if destructible types weren't an issue.
  • In practice this comes up most often with locals and the language can be easily designed to remove the majority of these instances.
@tomasr
tomasr commented Jan 30, 2015

@RichiCoder1 Having analyzers that do that is great, but probably doesn't fit my needs.

I am often in the position that I am reviewing customer code where I often won't even have a source code that builds (or may be incomplete, or worse things). In many cases like that, tools don't work. And even if the tools work, I am still going to spend hours looking at the code, and often that code is going to be very, very ugly, long, and having to scroll up and down sucks :)

@RichiCoder1

@tomasr fair enough. The biggest issue might simply be that much of the mistakes in using the finalizer come from trying to use it like destructors in normal languages. It'd break back compat, and therefore probably never happen, but I'd be much more a fan of changing the finalizer to a different syntax than forcing destructor to use something besides ~MyClass().

@sharwell
Member

The language doesn't agree with this comment. The move command definitely assigns a new value to the location after reading the original one. This value is a valid instance of D and follows all other language rules.

I could buy this. However, it suddenly becomes painfully obvious that C# doesn't have constructor initializer lists. How would you initialize the value of a destructible field (of a destructible type) without invoking the destructor of a default instance?

@AlgorithmsAreCool

@sharwell That is an interesting case. Looking at your previous code example, after move is called on _value, it is definitely assigned a new default value. This new default value would then be destructed with the rest of D2 at some point later. By this logic if a object has a struct field that is destructible, it must be destructed whether it was used or not. This introduces some small but unavoidable overhead doesn't it?

We can avoid this overhead for locals through liveness analysis but I don't think it's avoidable for fields.

As a small side question point, calling move on a struct's field would mutate the struct, right?

@MgSam
MgSam commented Feb 4, 2015

I think deterministically managing memory (and other resources) would be a very useful addition to C#/.NET as it is by far the biggest knock against the language and runtime. However, I feel like this solution isn't widely applicable enough (as it would require extensive re-writing of existing code) and the move syntax also doesn't feel like C#.

Apple managed to shoe-horn ARC into Objective C without requiring changes in the fundamental syntax of assignment. I'd like to see the C# language team work with the CLR team to come up with a similarly broad proposal that doesn't require such fundamental shifts in the way C# is written.

It's definitely time for an alternative to garbage collection but I'm not sure this is it.

@jaredpar
Member
jaredpar commented Feb 4, 2015

@MgSam I don't think ARC is applicable here because it is solving a very different problem:

  • ARC: free this memory, resource when there are no more references to the value.
  • destructible: free this resource at this point in the program.

True there are intersections between the features but they also have different uses and different trade offs.

@Mr-Byte
Mr-Byte commented Feb 4, 2015

@jaredpar Would it ever be possible to use destructibles to implement ARC? Or would destructibles be limited to managing other resources?

@MgSam
MgSam commented Feb 4, 2015

@jaredpar The Background section frames the problem broadly as the non-deterministic nature of memory reclaimation in C#. I'm not saying ARC is the right answer just that whatever the right answer is, it should have a few characteristics that I think are lacking in the current proposal:

  • Work with existing code with minimal updates.
  • Not require radical shifts in the way new code is written.
  • Be intuitive to understand, and still feel like C#.

If the idea is that this proposal is only intended as a nicer alternative to the using statement, I think its missing the forest for the trees. The garbage collector is the biggest problem with .NET, period. If you look at many of the current feature proposals for C# 7.0, a lot of them are related to improving performance by directly or indirectly reducing allocations and thus collection pressure. I'd much rather see garbage collection become optional or deprecated rather than mutate the language in all these other ways in an attempt to avoid allocations.

I can't believe that this is too difficult a problem or not worth the cost- Apple did it with a 25 year old language and they had developers singing their praises for doing so. Both the CLR and C# compiler are open source now and you guys are supposed to be One Microsoft- goals should be able to align to come up with a better solution here.

@jaredpar
Member
jaredpar commented Feb 4, 2015

@Mr-Byte I definitely think there is a ref counting solution to be built on top of destructible types but i don't think it should be the primary mechanism.

@MgSam while I share many of your concerns about reducing the impact of the garbage collector I don't see how it's relevant to this issue. This is a language about deterministic reclamation of unmanaged resources. It involves no CLR work and is purely a language implementation.

Anything like ARC, especially if the goal is to remove the garbage collector, is likely to involve CLR work and is a very different feature. Could this be used to implement destructible like types? But this is a much larger effort and one that I think has less of a chance of making it in. Although it's a topic I'm more than happy to discuss, have a lot of interest in it.

@casperOne

In this case, the value needs to be moved into SomeMethod so that SomeMethod can take ownership of the destruction. If you want to be able to write a helper method that works with a destructible value but that doesn't assume ownership for the destruction, the value can be passed by reference:

void SomeMethod(ref OutputMessageOnDestruction omod2)
{
   ...
} // 'omod2' not destructed here
…
OutputMessageOnDestruction omod1 = new OutputMessageOnDestruction("Destructed!");
SomeMethod(ref omod1); // Ok, 'omod1' still valid

It looks like there's some behavior that's not accounted for when using the ref keyword to not pass ownership of the destructable. Consider this:

void SomeMethod(ref OutputMessageOnDestruction omod2)
{
   omod2 = new OutputMessageOnDestruction("Destructed!");
}
…
OutputMessageOnDestruction omod1 = new OutputMessageOnDestruction("Destructed!");
SomeMethod(ref omod1); // Ok, 'omod1' still valid

What happens to the reference pointed to by omod1? I can imagine the compiler throwing an error when this happens, but then there's the strange semantics of ref (which implies mutability) not being able to be applied (and even worse, bisecting the meaning depending on something outside of the method which isn't immediately apparent).

@AlgorithmsAreCool

@casperOne That is a great point about the overloading of the ref keyword creating strange semantics. Assigning to omod2 would under normal circumstances clobber the original value out of scope, but with destructible types ref indicates a borrow operation forbidding the recipient scope from obtaining true ownership and destructing the object. So if that assignment is allowed, where would the original value of omod1 be destroyed. And what of the new value? Does it belong to the inner scope or the outer scope?

If the compiler is going to produce an error on assignment to omod2 as @casperOne implies, then what if a destructible is passed into a method like this :

void Foo<T>(ref T stuff) where T : new()
{
    stuff = new T();
}

what does ref mean here?

@axel-habermaier
Contributor

@casperOne: Interesting case. I would expect omod1 to be destructed when the assignment takes place, and omod2 being destructed when omod1 goes out of scope. In my opinion, that would match the original semantics of ref. Or am I missing something?

@aelij
Contributor
aelij commented Feb 10, 2015

👍
I love the idea of having RAII in C#.

Regarding delegates, I've often wondered why they were not value types to begin with. They're immutable, so copying them shouldn't matter. (I know a delegate can contain a list of delegates, but this is a less common use case, and I think it can be achieved by having a linked list of boxed delegates.)

So maybe as part of supporting "destructible delegate types", allow declaring any delegate type as a value type, which would avoid all those unnecessary allocations.

@jaredpar
Member

@aelij

The problem with maxing delegates value types is object tearing. At their core delegates are a tuple of object and method address and logically can be represented by the following:

struct Delegate 
{ 
  object _object;
  IntPtr _methodAddress;
}

The problem is that assignments between a struct of this nature are not guaranteed to be atomic. The CLR only guarantees atomic assignment for values that are one pointer in size (or less). This struct would be two pointers.

Having non-atomic assignments means that it is possible via assignment and multithreading to observe a Delegate value which has say _methodAddress from the location being assigned to and _object from the new value being assigned into it. Such a Delegate value has essentially violated type safety at that point because there is no guarantee the object and method address are a valid pair of values. Invoking it could succeed, cause the process to crash or worse fail but keep the process running.

This is one of the core reasons why delegates can't be value types, without other changes to the CLR these values are simply not fully type safe.

@aelij
Contributor
aelij commented Feb 10, 2015

@jaredpar Thanks for the explanation! I wonder though if under some restrictions this might still be feasible (off the top of my head, only allow value type delegates on the stack and in read-only fields; this could cover many use cases -- such as LINQ.)

Sorry if I'm off on a tangent, this doesn't exactly pertain to this proposal.

@jaredpar
Member

@aelij unfortunately readonly fields aren't sufficient for the following reasons:

  • The CLR doesn't really enforce readonly. Sure it gives a verification error when it is violated but that's about it. Reflection, mishaving IL, etc ... can all assign after construction.
  • It is still possible to induce object tearing by without violating readonly inside a class. Have the constructor spawn off a thread which simply reads and invokes the delegate field. After spawning the thread the constructor just assigns the field different values inside of a loop. This is 100% legal and will produce torn objects.
  • A readonly field on a struct is even weaker than on a class. It is possible for the owner to effectively assign to fields by copying over the struct with a new value that differs in only the field that wants to be changed.
@MgSam
MgSam commented Feb 10, 2015

...while I share many of your concerns about reducing the impact of the garbage collector I don't see how it's relevant to this issue.

@jaredpar Re-read the background section. The entire issue is framed as being necessary because of the non-determinism of the garbage collector. If you're going to solve a problem- solve the root cause, don't just add language bloat that dances around the issue.

I understand it's hard to get the CLR team onboard for huge, new major features. However, I think the scope of this problem more than justifies the two teams sitting down and doing that. When people compare .NET to other languages/runtimes, the garbage collector is the number 1 problem. It's time to find a solution that doesn't involve kludgy language-only half fixes. That's what the using statement was.

The CLR is not a black box- .NET development will be at a competitive disadvantage going into the future if the teams at MS keep treating it like one.

@jaredpar
Member

@MgSam

I'm well aware of the background section of this feature. I'm one of the designers of destructible types and I've previously implemented it in C#.

If you're going to solve a problem- solve the root cause, don't just add language bloat that dances around the issue.

I'm sorry but you're still trying to solve a different problem than destructible types. The goal of destructible types is to ensure a resource is freed at a very specific point in the program. This does not require making major changes to the GC. Why would we bloat the runtime for a simple language feature?

@GitTorre

This is a great start! I vote Yay.

@RobJellinghaus

Can you speak more to the interaction of references with destructible types in this proposal? Is it valid to take a ref to a destructible type? On its face it seems to me that it should be valid -- especially a readonly ref -- but I know from our previous experience that there can be issues here, to say the least. Some clarity on the interaction between ref and destructible would greatly help this proposal.

@jaredpar
Member

@RobJellinghaus

Yes taking a ref to a destructible would be legal. In this model the ref case actually represents the borrowed case.

@paulomorgado

Even with @gafter's comment on #160, I can't help to feel that if you move the value out of a variable, that variable should be treated as not having a value, which is not the same as having the type's default value. The variable should be treated as not definitely assigned and, thus, not needing destruction.

As for fields, we can't get away with not being assigned.

@jaredpar
Member
jaredpar commented Mar 3, 2015

@paulomorgado that is how we ended up defining it in our previous implementation. Essentially if we know the value definitely has no value via a move then we are free to treat it is unassigned.

There are some odd ball corner cases that you need to deal with but as a whole it works out.

@danfma
danfma commented Mar 4, 2015

Hey guys,

As a naive guy I could just say to put a delete keyword to forcely deallocate some object, executing the destructor. It's simple, effective but not safe at all.

But, in complement with that, we could grab the ideas from the rust language which defines borrowed pointers and that is only guaranted by the compiler (so no modifications at the runtime are needed). Most of what was discussed here are defined there.

Give a look at http://static.rust-lang.org/doc/0.6/tutorial-borrowed-ptr.html.

We need just some adaptations to make that more C#ish but I like some of that features. And believe me, when creating some games, or doing some other interoperability with existent libraries, deterministic finalization is a feature needed.

@GitTorre
GitTorre commented Mar 4, 2015

@danfma "As a naive guy I could just say to put a delete keyword to forcely deallocate some object, executing the destructor. It's simple, effective but not safe at all."

I don't think that's naïve at all (it's exactly what you need when memory needs to be freed exactly when you need it to be freed due to, say, memory pressure in the environment you have no control over...), but I agree it obviously breaks the memory safety guarantee of C# and .NET.

There are many scenarios where "escaping" GC in .NET is a reasonable (even critical) thing to do. This is an interesting idea, Dan. Of course, the problem is bigger than this particular feature (destructible types), but I do think "safedelete" is a reasonable (and obviously related) feature to consider. Borrowing from C++, D, Rust, these are all very reasonable things to do!

@MgSam
MgSam commented Mar 4, 2015

A delete keyword is an obsolete solution before its even proposed- you used to have to do these kinds of things in Objective C before ARC came around. .NET should be looking at a more modern alternative for memory management that doesn't require all this explicit memory management that inevitably results in mistakes and thus bugs.

@RobJellinghaus

A known valid pattern is to have a destructible local representing some resource, then pass it down to other objects and methods as a readonly ref. This ensures that those methods can rely on that reference not being deleted as long as they have access to it.

(It does require that the methods not capture the reference's value and expect it to remain valid, unless proper copy construction is provided.)

Code which follows this pattern can do a great deal to avoid any dynamic allocation at all. Generally speaking, in high-performance code, the less GC, the better. Rust gets this right. It may be too late for C# to ever support a GC-free model, but at least deterministic resource management features will enable the GC to be under far, far less pressure.

Adding a delete keyword is indeed a bad idea -- it precisely enables destruction of a value while references to the value still exist. Scoped destructible values, which become inaccessible at time of destruction, do not have the same issue.

@paulomorgado

Just to be clear, @jaredpar, by definitely not assigned I mean all the rules for unassigned variables apply. It will be a compiler error, from then on, to read the variable without writing something to it first.

@jaredpar
Member
jaredpar commented Mar 4, 2015

@paulomorgado yes, that is what I mean.

@Grauenwolf

This whole thing is a really, really bad idea. By introducing the concept of a destructible type you have also introduced the question "Should my new type be disposable or destructible?".

That question is impossible to answer because you have no way of knowing how the consumer is going to use your type. And impossible design questions are pretty much a deal breaker for any new feature.

Furthermore, this is a lot of work to simply remove the need for a using statement. Semantically this is going to do exactly the same thing, it will just be less apparent when it will occur.

My third objection is that this will not play nicely with other languages. For this to work, the Common Language Specification will have to be updated to say that all compliant languages support the notion of destructible objects. This includes VB, F#, IronPython, IronRuby, and who knows how many other languages.

Finally, this is going to end up like checked exceptions. At some point we are going to get an escape valve of some sort that allows destructible objects to be wrapped in disposable objects. Once that happens, the meager guarantees this offers will evaporate.

@drowa
drowa commented Apr 28, 2015

I have some questions and points about this feature.

  1. In case of destructible types, why do we need the move keyword if there is no other option? We could just assume the move semantics when necessary (i.e. in assignments, parameters and return statements).

Instead of writing...

var var2 = move var1;

We would write...

var var2 = var1; // `move` is implicit
  1. Can a destructible type be a sub-type of a non-destructible type?

For example...

class A {}
destructible class B : A { } // is this valid?

Or...

destructible struct A { }
...
var var1 = new A();
object var2 = var1; // is this valid?
  1. You can define the delete operator almost for free. This operator would be useful in long scopes and it would give more freedom to the programmer.
delete var1;

Would be translated to...

{var tmp$ = move var1;}
@whoisj
whoisj commented May 11, 2015

Just a few points I'd like to have clarified in the proposal.

Given A a = new A(); destructible which of the following is the suggested usage (and by the way, I love the use A a = new A(); suggestion for callsite specification.

Taking a reference of a destructible reference is done via A b = a; or 'A b = ref a`? The later seeming significantly more expressive and deliberate.

Taking ownership of a destructible reference is done via A b = move a;, what will A b = a; result in? I'm very much hoping the later results in a compile time error -- or at least a warning.

Can ownership be taken from references? Is this allowed (I hope not)?
A a = new A();
A b = ref a;
A c = move b

Is it absolutely required that when move is used, the previous owner is left in an invalid state? Why not convert it into a reference state? Perhaps the syntax should be more like:

DT a = use DT(); // destructible, a owns it
DT b = own a; // b owns destructible, a is now a reference
DT c = ref b; // c is a reference
DT d = ref a; // d is a reference
DT e = own a; // exception, a is not current owners and cannot confer ownership

Regardless, I do think use of the special keywords for on assignment should be required for destructible. Without them, compile time checking is impossible / really difficult. Additionally, declaration time specification is ideal - class markup is also handy for requiring specific declaration mark up but I do not believe it should be the method of implementation.

Lastly, I didn't see this in the specification: what happens to all the references? Does the run-time set them all to null or is there some kind of destroyed operator being introduced? I very much hope that we're not expecting the developer to track the state of these things without support from the run-time.

@Grauenwolf I disagree completely. The disposable pattern has been a weakness of C# since its inception. The sooner we can be without it, the better. The fact that the Dispose method could be called by any interacting code has made the entire pattern fragile. For example: there are a number of Stream wrappers which dispose the underlying stream, expected or not. This is horrible because the owner of stream (the allocator) likely still needs the resource undisposed or they would have disposed it themselves.

@Grauenwolf

Any proposal based on abandoning IDisposable is doomed from the outset due to backwards compatibility issues.

@whoisj
whoisj commented May 11, 2015

@Grauenwolf I do not disagree. There is no possible way to abandon the disposable pattern for many years. However, given that the pattern is subject to misuse and can be an enforced cause of bugs it should be phased out over time.

To assume that because something is common that it must always be, is akin to assuming that because it is common for people to drive gasoline powered automobiles that electric powered automobiles should never be considered. I believe history will prove this assumption incorrect. 😄

@govert
govert commented May 12, 2015

I would like to suggest a poor-man's version of this feature, where the existing IDisposable / using mechanism is extended by compiler help and syntactic sugar to assist with some of the specific problems in current use. This would be a bit like the FxCop rules, but built into the compiler, improving the use of the current feature but not providing hard guarantees. For example:

  1. Add an attribute, say [RequiresUsing] to indicate that a class which implements IDisposable should only be constructed in a using block, or in an assignment to a field in another [RequiresUsing] type. (An alternative would be an interface that extends IDisposable.)
  2. Creating a [RequiresUsing] object outside a using block or some assignment to a field in a [RequiresUsing] type generates a compiler warning.
  3. In an IDisposable type, a field of a type that implements IDisposable can be marked as [Dispose]. The compiler will auto-generate a Dispose() (maybe with an existing Dispose() being called by the compiler-generated method).
  4. Some syntax like use x = new A(); is just shorthand for using (x = new A()) {...} where the block extent is as small as possible in the method - until just after the last use of x. Feature like async / await and exception handling already works right with using.
  5. Add any flow analysis that the compiler can easily do, to provide warnings for misuse, like cases where an object might be used after disposal - e.g. if it is passed to a method from inside a using block that stores the reference and would allow the reference to live beyond the call lifetime.

This does not address the move / ref ownership issues comprehensively, so provides no guarantee around deterministic disposal. But it has the great advantage of not adding a tricky new language concept, instead making the compiler more helpful in using the existing paradigm for deterministic disposal.

@paulomorgado
  1. So, instances of this cannot be created by factory methods.
  2. So, instances of this cannot be created by factory methods. And no wrapper classes are allowed.
  3. Can you elaborate on that?
  4. Can you provide sample code?
  5. How would you do that?
@govert
govert commented May 12, 2015
  1. Can the compiler check the call graph to ensure that the factory method is at some point made from a using callsite? Again, I see this at about the same level as a warning for a variable that is declared but never used, so use outside the expected scope is OK, but just generates a warning.
  2. Wrapper classes should be OK. But the same story - the compiler does what it can, generated warning where there are potential problems and makes no new guarantees.
  3. Might mean code like:
public class A : IDisposable
{
   [Dispose] B b;
}

would compile (checking the constraint B : IDisposable), and generate

public class A : IDisposable
{
   [Dispose] B b;
   public void IDisposable.Dispose()
   {
       b.Dispose();
       // maybe b=null;
       // maybe keep a flag isDisposed = true, throwing an exception if Dispose is called again.
   }
}

Point 4. Code like:

void DoStuff()
{
   use x = new A();
   x.DoStuff();
   DoOtherStuff();
}

is compiled as

void DoStuff()
{
   using (var x = new A())
   {
       x.DoStuff();
   }
   DoOtherStuff();
}

Point 5. The compiler can detect cases like:

[RequiresUsing]
public class X : IDisposable { ... }

void DoStuff()
{
   using (var x = new X())
   {
       DoSomethingWithX(X)
   }
   UseXLater();
}

X _myX;
void DoSomethingWithX(X x)
{
    _myX = x; // Compiler should generate a Warning here: Assignment of `[RequiresUsing]` type to non-local variable.
}

void UseXLater()
{
  _myX.DoStuff(); // Dangerous - _myX might have been disposed already
} 
@paulomorgado
  1. What if the factory method is in a library for third party use (like a class in the .NET framework)?
  2. What if the wrapper class is in a library for third party use (like a class in the .NET framework)?
  3. What if there are more than one disposable and order of disposal is important?
  4. What if the disposable is something like TransactionScope that is not really used inside the using statement?
  5. How is x being used later if it's out of scope?
@govert
govert commented May 12, 2015

For your 1. and 2. I understand your concern is some method like:

[RequiresUsing] public class A : IDisposable {...}

public A CreateA()
{
    var a = new A();
    a.PrepareFurther();
    return a;
}

This code should not raise a warning, since it returns the IDisposable. The warning is raised where an IDisposable is assigned to a local variable or non-special field, not returned from the function and not under a using.

For 3. The order of disposal is in the declaration order. For another order, implement Dispose() explicitly.

For 4. Then the use .... shortcut syntax is not appropriate - using is quite nice in that case, because it introduces the explicit syntactic block scope.

For 5. That's an example where the compiler might raise warning (the IDisposable escapes the scope of the using) but also shows where it becomes difficult to detect potential problems.

Anyway, I'm just trying to suggest exploring a lightweight approach to the problems addressed by this topic, but which is complementary to the existing IDisposable / using mechanism and without the severe complications added by the 'destructible' proposal offered before. I'm not sure this thread is the place, or that I could, work towards a comprehensive counter-proposal. Is there any merit in considering an attributes-and-compiler-warnings approach to the problem?

@bbarry
bbarry commented May 13, 2015

@govert I think that code should raise a warning. The IDisposable semantics are not being recognized in that method somehow. You should do something explicit to not get it:

Given:

[RequiresUsing] public class A : IDisposable {...}

This should give a warning to the effect of IDisposable instance marked with RequiresUsingAttribute not disposed

public A CreateA()
{
    var a = new A();
    a.PrepareFurther();
    return a;
}

In this case an analyzer could offer the fix (because A is the return type) of putting the attribute on the method:

[RequiresUsing] public A CreateA()
{
    var a = new A();
    a.PrepareFurther();
    return a;
}

You could also detect class level usages:

public class B {
    //warning: should implement IDisposable to use IDisposable field of type A
    //warning: should call _a.Dispose() in Dispose method
    //warning: should be marked with RequiresUsingAttribute
    private readonly A _a;
    public B() { _a = ...; }
}

//OK 
[RequiresUsing] public class B : IDisposable {
    private readonly A _a;
    private bool _disposed = false;
    public B() { _a = ...; }
    public void Dispose() { Dispose(true); }
    public virtual void Dispose(bool disposing) {
        if(!_disposed) { _a.Dispose(); } 
        _disposed = true;
    }
}

I think there is merit to such an approach and it could be done entirely with attributes and analyzers without changes to the compiler.

@gafter gafter added the 0 - Backlog label Nov 20, 2015
@drauch
drauch commented Nov 27, 2015

@bbarry: although the feature is already on the "probably never" list, I want to point out that you cannot easily workaround such using-enforcements, e.g., what to do in NUnit tests where you have a SetUp and a TearDown method? How to tell the system that the Dispose() method is called by reflection?

@bbarry
bbarry commented Nov 27, 2015

@drauch [SuppressMessage("Acme.RequiresUsing", "AR....", Justification = "Disposed via reflection.")]

@alrz
Contributor
alrz commented Jan 15, 2016

It would be nice to merge this proposal with #160 and somehow #181, so

// instead of
var foo = new Destructible();
var bar = move foo; // mandatory move

// we could just
let foo = new Whatever(); // owned by scope
{
  let bar = foo; // implicitly (temporarily, in this case) move 
  foo.Bar(); // ERROR: use of moved value
}
foo.Bar(); // OK

This would cause a compiler error when accessing the collection in foreach block, which is not possible with neither #160 nor #161, e.g.

let list = new List<T> { ... };
foreach(var item in list)  // list is borrowed
{
  WriteLine(item);
  list.Add( ... ); // ERROR: use of moved value
}
list.Add( ... ); // OK

Probably another keyword instead of let (perhaps, using?) but basically what Rust features. Note that if the type is IDisposable it'll be disposed when it gets out of scope, just like Rust's Drop — so you'll never forget to dispose IDisposable objects with let or whatever.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment