Stream and IDisposable in C#2.0, design blunder (well, 100-year sleepis more like it)

H

Helge Jensen

In C# 2.0 System.IO.Stream is declared as:

public class Stream: ..., IDisposable {
...
public void Dispose();
public void Dispose(bool);
IDisposable.Dispose();
}

Which must be a design-blunder, if not a 100-year sleep. It prevents
polymorphic disposing of Stream instances through ((Stream)s).Dispose().

Have a look at some ways to *try* to implement a Stream which needs a
custom dispose:

public class MyStream: Stream {
...
/* first, the natural, right and proper way: */
public override void Dispose(); // Dispose is non-virtual
/* bring on the hacks: */
public new Dispose(); // wrong semantics on ((Stream)s).Dispose()
void IDisposable.Dispose(); // using(Stream s = new MyStream...) ?!
}

The bottom-line is that Stream implementations with custom Dispose
*cannot* possibly behave correctly in *all* usage scenarios, and even
worse in the pretty-common scenario where the user knows the instance as
Stream by polymorphism and invokes Dispose yields *wrong* semantics,
*silently* using all possible implementations of MyStream.

To find out what happens in the "using" case, I turned to the ECMA
draft spec.
(http://download.microsoft.com/download/8/1/6/81682478-4018-48fe-9e5e-f87a44af3db9/standard.pdf,
p. 238). It is not really easy to read. To me ECMA sounds like it
declares "public void Dispose()" as "the magic protocol for using", with
"IDisposable.Dispose()" as a backup method. I understand that reasoning
for value-types, in order to actually mutate the value, instead of
disposing a Boxed copy of the value-type.

Writing a test-program which is found at the bottom of the post I
dreaded and predicted the output:

Bar.Dispose()
Bar.Dispose()
Foo.Dispose()
Foo.Dispose()
Baz.Dispose()
Baz.Dispose()

Since the resource-type "R", in this case, Foo and Bar respectively
declares a single public, void-returning, parameterless method named
Dispose.

But the actual output from vs2005 is (as I *hoped* ECMA would have
specified):

Bar.Dispose()
Bar.IDisposable.Dispose()
Foo.Dispose()
Bar.IDisposable.Dispose()
Baz.Dispose()
Baz.IDisposable.Dispose()

Which is almost good enough... only the virtual dispatch in
((Foo)bar).Dispose() is missing -- because of the non-virtual
Foo.Dispose blunder. Note that this explicitly *does* *not* invoke
Baz.Dispose() when using(baz).

I can't see (in my current anger anyway :) how a correct ECMA
implementation could produce the above output.

I see no reason to allow "using" of non-IDisposable reference-types, and
a hacky protocol through "public void Dispose()" for value-types. A
slightly less intrusive protocol would be to execute using without
boxing values as a special-case, limiting the special-cases to a
specific interface instead of contaminating the normal name-space for
methods.

<having a fit>
On top of *that*, Stream has now got a Dispose(bool). what good can that
possibly do? Stream.Dispose() is about *semantics* not implementation in
managed vs. unmanaged space.

What use is it to be able to *possibly* "release managed resources"
polymorphicly in a semantic statement declaring the Stream dead.
</having a fit>

OK, not that's off my chest I better go find the right place to report
this so it can get fixed.

====> test program <====
using System;
using System.IO;

class VirtualDisposeExample
{
public class Foo : IDisposable
{
public void Dispose()
{ Console.WriteLine("Foo.Dispose()"); }
void IDisposable.Dispose()
{ Console.WriteLine("Foo.IDisposable.Dispose()"); }
}
public class Bar : Foo, IDisposable
{
public new void Dispose() { Console.WriteLine("Bar.Dispose()"); }
void IDisposable.Dispose()
{ Console.WriteLine("Bar.IDisposable.Dispose()"); }
}
public struct Baz : IDisposable
{
public void Dispose()
{ Console.WriteLine("Baz.Dispose()"); }
void IDisposable.Dispose()
{ Console.WriteLine("Baz.IDisposable.Dispose()"); }
}
static void Main(string[] args)
{
Bar bar = new Bar();
bar.Dispose();
using (bar)
; // Dispose bar through using
Foo foo = bar;
foo.Dispose();
using (foo)
; // Dispose foo through using
Baz baz = new Baz();
baz.Dispose();
using (baz)
; // Dispose baz through using
}
}
 
M

Marc Gravell

I'm not sure I see the big problem - you just override Disposing, which is
called by the base Dispose (and IIRC finalizer) implementations; the
following works fine; all 4 usages dispose correctly...

Am I missing something in your post?

using System;
using System.IO;

namespace WindowsApplication2 {
static class Program {
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main() {
using (MyStream stream1 = new MyStream()) {

}
MyStream stream2 = new MyStream();
stream2.Dispose();
Stream stream3 = new MyStream();
stream3.Dispose();
IDisposable stream4 = new MyStream();
stream4.Dispose();
Console.ReadLine();
}
}

public class MyStream : Stream {
protected override void Dispose(bool disposing) {
System.Diagnostics.Debug.WriteLine("Disposing");
base.Dispose(disposing);
}

// *** the rest is just junk to get it to compile

public override int Read(byte[] buffer, int offset, int count) {
throw new Exception("The method or operation is not
implemented.");
}
public override void Write(byte[] buffer, int offset, int count) {
throw new Exception("The method or operation is not
implemented.");
}
public override bool CanRead {
get { throw new Exception("The method or operation is not
implemented."); }
}
public override bool CanWrite {
get { throw new Exception("The method or operation is not
implemented."); }
}
public override bool CanSeek {
get { throw new Exception("The method or operation is not
implemented."); }
}
public override void Flush() {
throw new Exception("The method or operation is not
implemented.");
}
public override long Seek(long offset, SeekOrigin origin) {
throw new Exception("The method or operation is not
implemented.");
}
public override void SetLength(long value) {
throw new Exception("The method or operation is not
implemented.");
}
public override long Length {
get { throw new Exception("The method or operation is not
implemented."); }
}
public override long Position {
get {
throw new Exception("The method or operation is not
implemented.");
}
set {
throw new Exception("The method or operation is not
implemented.");
}
}
}

}
 
N

Nicholas Paldino [.NET/C# MVP]

Helge,

Forgive me for saying so, but you haven't actually read the
documentation on the suggested implementation of IDisposable, have you?

If you had, you would have seen that the only difference between the
public Dispose method, and the Dispose method which you are allowed to
override is that the Dispose method calls Close, which then calls the
overridable Dispose method, which then indicates to the CLR that the
instance should not be finalized.

You should be able to do all that you want in the Dispose method that
you are able to override.

Even if you wanted to have the finalizer run after you call Dispose (for
what reason, I don't know, as I think you have issues with the lifetime
management aspect of your design if you do), you can always override Close,
which is marked as virtual.

I don't see how you haven't been left an option in this case.

--
- Nicholas Paldino [.NET/C# MVP]
- (e-mail address removed)

Helge Jensen said:
In C# 2.0 System.IO.Stream is declared as:

public class Stream: ..., IDisposable {
...
public void Dispose();
public void Dispose(bool);
IDisposable.Dispose();
}

Which must be a design-blunder, if not a 100-year sleep. It prevents
polymorphic disposing of Stream instances through ((Stream)s).Dispose().

Have a look at some ways to *try* to implement a Stream which needs a
custom dispose:

public class MyStream: Stream {
...
/* first, the natural, right and proper way: */
public override void Dispose(); // Dispose is non-virtual
/* bring on the hacks: */
public new Dispose(); // wrong semantics on ((Stream)s).Dispose()
void IDisposable.Dispose(); // using(Stream s = new MyStream...) ?!
}

The bottom-line is that Stream implementations with custom Dispose
*cannot* possibly behave correctly in *all* usage scenarios, and even
worse in the pretty-common scenario where the user knows the instance as
Stream by polymorphism and invokes Dispose yields *wrong* semantics,
*silently* using all possible implementations of MyStream.

To find out what happens in the "using" case, I turned to the ECMA draft
spec.
(http://download.microsoft.com/download/8/1/6/81682478-4018-48fe-9e5e-f87a44af3db9/standard.pdf,
p. 238). It is not really easy to read. To me ECMA sounds like it declares
"public void Dispose()" as "the magic protocol for using", with
"IDisposable.Dispose()" as a backup method. I understand that reasoning
for value-types, in order to actually mutate the value, instead of
disposing a Boxed copy of the value-type.

Writing a test-program which is found at the bottom of the post I dreaded
and predicted the output:

Bar.Dispose()
Bar.Dispose()
Foo.Dispose()
Foo.Dispose()
Baz.Dispose()
Baz.Dispose()

Since the resource-type "R", in this case, Foo and Bar respectively
declares a single public, void-returning, parameterless method named
Dispose.

But the actual output from vs2005 is (as I *hoped* ECMA would have
specified):

Bar.Dispose()
Bar.IDisposable.Dispose()
Foo.Dispose()
Bar.IDisposable.Dispose()
Baz.Dispose()
Baz.IDisposable.Dispose()

Which is almost good enough... only the virtual dispatch in
((Foo)bar).Dispose() is missing -- because of the non-virtual Foo.Dispose
blunder. Note that this explicitly *does* *not* invoke Baz.Dispose() when
using(baz).

I can't see (in my current anger anyway :) how a correct ECMA
implementation could produce the above output.

I see no reason to allow "using" of non-IDisposable reference-types, and a
hacky protocol through "public void Dispose()" for value-types. A slightly
less intrusive protocol would be to execute using without boxing values as
a special-case, limiting the special-cases to a specific interface instead
of contaminating the normal name-space for methods.

<having a fit>
On top of *that*, Stream has now got a Dispose(bool). what good can that
possibly do? Stream.Dispose() is about *semantics* not implementation in
managed vs. unmanaged space.

What use is it to be able to *possibly* "release managed resources"
polymorphicly in a semantic statement declaring the Stream dead.
</having a fit>

OK, not that's off my chest I better go find the right place to report
this so it can get fixed.

====> test program <====
using System;
using System.IO;

class VirtualDisposeExample
{
public class Foo : IDisposable
{
public void Dispose()
{ Console.WriteLine("Foo.Dispose()"); }
void IDisposable.Dispose()
{ Console.WriteLine("Foo.IDisposable.Dispose()"); }
}
public class Bar : Foo, IDisposable
{
public new void Dispose() { Console.WriteLine("Bar.Dispose()"); }
void IDisposable.Dispose()
{ Console.WriteLine("Bar.IDisposable.Dispose()"); }
}
public struct Baz : IDisposable
{
public void Dispose()
{ Console.WriteLine("Baz.Dispose()"); }
void IDisposable.Dispose()
{ Console.WriteLine("Baz.IDisposable.Dispose()"); }
}
static void Main(string[] args)
{
Bar bar = new Bar();
bar.Dispose();
using (bar)
; // Dispose bar through using
Foo foo = bar;
foo.Dispose();
using (foo)
; // Dispose foo through using
Baz baz = new Baz();
baz.Dispose();
using (baz)
; // Dispose baz through using
}
}
 
B

Barry Kelly

Helge Jensen said:
In C# 2.0 System.IO.Stream is declared as:

public class Stream: ..., IDisposable {
...
public void Dispose();
public void Dispose(bool);
IDisposable.Dispose();
}

Which must be a design-blunder, if not a 100-year sleep. It prevents
polymorphic disposing of Stream instances through ((Stream)s).Dispose().

You can polymorphically close a stream with ((IDisposable)s).Dispose()
or ((Stream)s).Close(). I don't understand your problem.
Have a look at some ways to *try* to implement a Stream which needs a
custom dispose:

You're supposed to override 'void Dispose(bool)'. The documentation is
quite clear on the pattern for this.

-- Barry
 
H

Helge Jensen

Helge said:
In C# 2.0 System.IO.Stream is declared as:

Thanks to everyone for the pointers to the documentation. I have read it
and realize that I can achieve what I need by overriding Dispose(bool
disposing). I rest my case on that point.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top