gui-cs / Terminal.Gui

Cross Platform Terminal UI toolkit for .NET
MIT License
9.55k stars 681 forks source link

Clean up, formalize, and enhance RunState #3677

Open dodexahedron opened 3 weeks ago

dodexahedron commented 3 weeks ago

Update:

Current plan is to:


Orignal:

TL;DR: The Terminal.Gui.RunState type should be sealed and NOT implement IDisposable.

Why?

It references but is not necessarily the owner of an IDisposable object.

It doesn't own any unmanaged resources.

It doesn't do anything in disposal anyway except throw a potentially uncatchable exception (see below).

And the protected virtual modifier on the Dispose(bool) method is unnecessary, as well, since nothing derives from it.

And GC.SuppressFinalize should only get called once.\ However, beyond that, the GC.SuppressFinalize is unnecessary for this class for a couple of reasons:

There are caveats for derived types, too, but I don't think that's relevant here.

At least in its current state, I don't think it should be IDisposable at all. It certainly doesn't actually follow the pattern. I also think it should be sealed. If someone really wants to rip it out and replace it with their own, they can type forward to do so or define their own with the same name to override it.

[^FinaWhat]: Finalizers look like C++ destructors and have a sorta similar purpose, just managed-style.

dodexahedron commented 3 weeks ago

As far as I can tell, the entire class, at present, can reduce down to this:

/// <summary>The execution state for a <see cref="Toplevel"/> view.</summary>
public sealed record RunState (Toplevel Toplevel) : IEqualityOperators<RunState, RunState, bool>
{
    /// <summary>The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.</summary>
    public Toplevel? Toplevel { get; internal set; } = Toplevel;
}

Honestly, though? I'd go one step farther and make it generic, for even better compile-time and JIT behavior/optimization possibilities, like this:

/// <summary>The execution state for a <see cref="Toplevel"/> view.</summary>
public sealed record RunState<T> (T? Toplevel) : IEqualityOperators<RunState<T>, RunState<T>, bool> where T : Toplevel
{
    /// <summary>The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.</summary>
    public T? Toplevel { get; internal set; } = Toplevel;
}

That requires small adjustments elsewhere to add the type parameters (pretty straightforward). I've got it half done already, since I was validating it all in code as I was reporting on it anyway.

tig commented 3 weeks ago

No reason other than me being stoopid, probably.

A PR would be appreciated.

dodexahedron commented 3 weeks ago

Bah, I'm sure it probably originally made sense and then just evolved away from it haha.

And yup - PR coming soonish. I backed up a little bit to revisit, with a more critical eye, the generic idea WRT this specific class.

Generic wouldn't be necessary if I change the property in that class to be get-only, with a method to change it instead of a set accessor.

Why?

That gets what I was going for in the first place (covariance, primarily), without needing to bother with generics. I'll do a quick PoC and see how that looks before continuing down the other path. It'll likely touch a lot fewer files, too, if I do it that way.

A read-write property requires an invariant type. Get-only allows covariant returns for overriding descendants, by virtue of them just being methods returning a value.

dodexahedron commented 3 weeks ago

Related to IDisposable, though...

So, RunState is an obvious type where change notification is potentially quite useful to anything using it...

Events (such as from INotifyPropertyChanged) and the observer pattern (using System.IObserver<T>[^kontra] and System.IObservable<T>[^co] are both non-exclusive options for that. Each of those brings back potential need for keeping IDisposable, to aid in proper cleanup for avoiding memory leaks and difficult-to-diagnose exceptions in cases where consumers fail to properly unsubscribe before ditching a subscriber/observer object instance.

If I were to implement either or both of those mechanisms, I'd put the IDisposable back in and just "do the needful" to be sure it does what it needs to do correctly.

Any thoughts/comments/questions/concerns/gripes/jeers about that?

[^co]: IObservable<T> is covariant on T. [^kontra]: IObserver<T> is contravariant on T.