4

I read the accepted answer to a similar question, part of the answer is:

when structs are passed as parameters, they get passed by value: they are copied. Now you've got two structs with the same internal fields, and they're both going to attempt to clean up the same object. One will happen first, and so code that is using the other one afterwards will start to fail mysteriously... and then its own cleanup will fail

Doesn't this same problem apply to Dispose()? If structs can implement IDisposable, what is the reasoning behind not allowing them to have finalizers?

If the whole point of a finalizer is to call Dispose(false) in case the programmer forgot to call Dispose(), and structs can have IDisposable.Dispose(), then why disallow finalizers for structs but allow them for reference types?

David Klempfner
  • 8,700
  • 20
  • 73
  • 153
  • 2
    IDisposable is just regular interface. Structs can implement interfaces, so they can implement IDisposable. – Evk May 02 '18 at 10:32
  • `IDisposable` is not a language mechanism, as such. It's just an interface. Structs can implement interfaces, so they can implement `IDisposable`. The only relation between `.Dispose()` and finalizers is the relation put there by the programmers. (This is a glib remark, though. A proper answer would highlight why it's even potentially *useful* to have a struct that implements `IDisposable`, whereas finalizers simply don't ever apply.) – Jeroen Mostert May 02 '18 at 10:33
  • 1
    ["A struct cannot have a destructor. A destructor is just an override of object.Finalize in disguise, and structs, being value types, are not subject to garbage collection."](https://stackoverflow.com/a/8276083/5894241) – Nisarg Shah May 02 '18 at 10:33
  • @Igor The OP referenced that very question, and asked a related question. Not a duplicate. – Luaan May 02 '18 at 10:37
  • @Evk I know structs can implement interfaces, I'm more interested in the reasoning behind not allowing them to have finalizers if they are allowed to implement IDisposable. – David Klempfner May 03 '18 at 04:08
  • Why they cannot have finalizers is explained in answer you linked. IDisposable is not related to finalizer in any way, why it's allowed is explained in answers below. – Evk May 03 '18 at 05:51
  • But the reasons applied to finalizers, can be equally applied to calling Dispose, since all a finalizer does is call Dispose. – David Klempfner May 03 '18 at 07:38

2 Answers2

2

Because IDisposable is just an interface. There's no special handling to it. Structs can implement interfaces, so they can implement IDisposable.

However, that doesn't mean there's no sense to it. The purpose of IDisposable is to release unmanaged resources. A struct can have a reference to an unmanaged resource, and that will benefit from a Dispose (needless to say, that reference itself should implement IDisposable and have a finalizer).

As a bonus, Dispose is often used as part of the using pattern. You create the instance just for the using block, you keep a reference until the Dispose, no weirdness involved. There's no reason to prohibit that, really.

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • 1
    But if a struct has a reference to an unmanaged resource, the OP's worry is justified: disposing one copy of the struct will break all other copies too. –  May 02 '18 at 10:39
  • @hvd Depends on what you call "break". But in any case, it will not behave differently from a class with multiple references, as long as you don't modify any local fields (which you shouldn't). – Luaan May 02 '18 at 10:41
  • Classes by default can't be copied, so in that sense it does behave differently: the ability to create copies provides an extra opportunity for the programmer to get things wrong. But I do see your point. –  May 02 '18 at 10:44
  • @hvd Granted, but only the references to the unmanaged object are copied - there's still only the one instance of it. It should go without saying that you should never have an *unmanaged* reference to the unmanaged object, though (e.g. a struct wrapping an `IntPtr` is just asking for trouble). – Luaan May 02 '18 at 10:46
2

Doesn't this same problem apply to Dispose()?

Sort of, but not entirely. Specifically, the "then its own cleanup will fail" is possible but unlikely, since Dispose() must be safe to call multiple times on the same object, and it will normally not be a problem to call it multiple times on different copies of the same object.

If structs can't have finalizers, why are they allowed to implement IDisposable?

Allowing it is the natural behaviour; it gives the language simpler rules. Since this is not something that a programmer is likely to get wrong by accident, the benefit in writing extra code in the compiler to reject this is small.


Jeroen Mostert adds that it can even make a lot of sense for structs to implement IDisposable:

IDisposable may be implemented simply because it is a requirement of some other code, even though Dispose()'s implementation on this specific type will do absolutely nothing. In this case, there is no risk in accidentally calling it on a different copy. An example of this is when a struct implements IEnumerator<T>, where IEnumerator<T> in turn implements IDisposable.

  • 2
    There's actually a good(ish) use case for allowing it, as opposed to merely not forbidding it: `IEnumerator` implements `IDisposable`. Trivial enumerators don't have disposable resources and almost no state, so that implementing them as a struct makes sense, and it is in fact used in the Framework itself. (For instance: `Dictionary+Enumerator`.) – Jeroen Mostert May 02 '18 at 10:44
  • @JeroenMostert You meant `IEnumerator`, not `IEnumerator`, but excellent point. Will make a mention of it. –  May 02 '18 at 10:45
  • Oh, I missed that. That actually muddies the issue a bit because disposing an `IEnumerator` is still done, by checking if the instance happens to implement the interface. For some reason, with the generic variant they decided to bake it into the interface, but that was not strictly necessary to do. (Of course, a lot is weird with how enumerators were originally implemented.) – Jeroen Mostert May 02 '18 at 10:50
  • @JeroenMostert It should have been done the same with `IEnumerator` too, but since that had already shipped without `IDisposable`, fixing that oversight later would have broken too much existing code. https://stackoverflow.com/questions/232558/why-does-ienumeratort-inherit-from-idisposable-while-the-non-generic-ienumerat –  May 02 '18 at 10:52