gui-cs / Terminal.Gui

Cross Platform Terminal UI toolkit for .NET
MIT License
9.66k stars 689 forks source link

Rename `StateChangedEventArgs<T>` to `CancelEventArgs<T>` #3561

Closed tig closed 3 months ago

tig commented 3 months ago

I mis-named this. I originally thought it was just for representing the property that represented the primary state of a View, but that's way to restrictive. This class is useful for any event that is tracking the state of a property.

dodexahedron commented 3 months ago

There is. It's just hidden. Because the code has only changed by the inclusion or exclusion of one element: [Nullable(0)] applied to the type parameter.

No execution differences exist.

The warnings you're getting are for a reason - because the execution model isn't what you actually expect and isn't consistent.

tig commented 3 months ago

There's no trouble if I don't add where T : notnull clause to the class.

dodexahedron commented 3 months ago

Weird that posted out of order...

Anyway it's a response to

There's no trouble if I don't add where T : notnull clause to the class.

Just copying it for convenience:

There is. It's just hidden. Because the code has only changed by the inclusion or exclusion of one element: [Nullable(0)] applied to the type parameter.

No execution differences exist.

The warnings you're getting are for a reason - because the execution model isn't what you actually expect and isn't consistent.

dodexahedron commented 3 months ago

The only reason they don't appear without it is because the analysis simply isn't being performed.

dodexahedron commented 3 months ago

Weird that posted out of order...

Anyway it's a response to

There's no trouble if I don't add where T : notnull clause to the class.

Just copying it for convenience:

There is. It's just hidden. Because the code has only changed by the inclusion or exclusion of one element: [Nullable(0)] applied to the type parameter.

No execution differences exist.

The warnings you're getting are for a reason - because the execution model isn't what you actually expect and isn't consistent.

But yeah, in reference to this...

Aside from the attribute, which isn't executed in any way at run time (and, in fact, won't even exist if the assembly is trimmed), you end up with identical IL with or without that type param constraint. Thus, it telling you there's something odd is legit.

Those generic method calls are resolved at compile time, even though the concrete types are created at JIT-time upon first encountering a new value type (reference types share one implementation in most cases).

To be run-time safe, anything we have that is generic that actually does anything with the data given to it with type T must exist either narrowly defined to only accept what will always work, for all types accepted, or has to have separate implementations for the cases that don't behave the same implicitly.

It's annoying and makes it understandable why the option to not use a generic is still sometimes chosen in new BCL features to this day.

tig commented 3 months ago

I guess I need you to provide a PR to my PR showing me exactly how to do this right because I have not been able to follow most of what you've explained. I will sleep on it and see if that helps, but I'm just not getting all the nuances. I struggle to understand some of your vague references and translate them into actual code:

dodexahedron commented 3 months ago

Well... The problem is it's a tough decision.

I, like you, really want and appreciate generics when possible.

But, they have some really irksome caveats like this, which require specific considerations and decisions to be made.

If you wanna do it that way, though, where you put it in as-is but minus the constraint, and the caveats can be addressed later, that's cool as far as I'm concerned.

But I'd make the events themselves accept the non-generic base class CancelEventArgs, instead, if you haven't already, both to avoid an API surface change and so consumers who don't need to pass the actual values in aren't forced to supply them.

dodexahedron commented 3 months ago

Or actually put it in with the constraint, so we have the warning as a reminder to fix it... That might be smarter.

But still with the event delegates accepting the immediate base class.

In any event-handlers for them, you simply do a type check to see if it's the thing you expect.

Because remember - if it got overridden and it's not what you expected, that's a run-time error.

dodexahedron commented 3 months ago

(Remember, I'm just whiteboarding here. This all isn't explicit answers/recommendations, and that's intentional - just options)

One of the issues that makes this annoying AF on our end is there are a ton of ways to approach it, but all have pros and cons. There's no perfect solution. There are even entire libraries out there that try to deal with this exact set of issues.

To get actually consistent behavior between struct and class, which is the bigger issue at play, here, I'd typically try to favor reference semantics OR value semantics, but not both (note I didn't say types). It's easier to support and easier to use. Reference semantics are a lot easier to implement. Value is harder both for us AND the consumer (even though C# is already pass/return by value...reference types muddy that a lot).

If you want a longer explanation with my general and preliminary opinions, I can certainly give you one.

But suffice it to say that the syntactic issues and the very legitimate warnings from analysis due to nullability (or lack thereof, for structs) are symptoms, and the disease is a clash of expectations with what the language and CLR actually do or can do, on its own, without extra work, which avoiding/reducing is part of the reason for wanting and using generics in the first place, right?. Just kinda moves where the work has to be done. 🤷‍♂️

dodexahedron commented 3 months ago

Or we just throw up our hands and let consumers deal with it, which is the lightest touch approach.

But then we also shouldn't do anything internally in the library that assumes one or the other without a graceful fallback, if that's the chosen path. And I'd also suggest you explicitly annotate the type parameter as nullable, if you do that (so, <T?>), to let the analyzer know null is allowed. Otherwise it is assuming it isn't.

tig commented 3 months ago

if you do that (so, <T?>

You can't do that. Gives a syntax error.

tig commented 3 months ago

BTW, after sleeping on it and writing a bunch of code, I now get what you've bee putting down. Mostly.

dodexahedron commented 3 months ago

Coolio. Yeah I saw the PR and it looks like you're going the right way.

Still some stuff we can shore up, but not important enough to hold it up IMO.

Maybe drop an appropriate [ComponentGuarantees] on that event, as well, for now, in case those tweaks are breaking?