-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Conversation
@@ -44,12 +45,12 @@ public virtual string ToString() | |||
// Equal to this. Equality is defined as object equality for reference | |||
// types and bitwise equality for value types using a loader trick to | |||
// replace Equals with EqualsValue for value types). | |||
public virtual bool Equals(object obj) | |||
public virtual bool Equals(object? obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should ToString return string?
? This gets back to the question of how we want to handle base virtuals. I guarantee you that there are ToString overrides that return null (I'm pretty sure we even have a few in coreclr/corefx), even though design guidelines say you shouldn't do that.
cc: @terrajobst, @MadsTorgersen
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For other types we haven’t annotated the return type of ToString as nullable. However this depend on what we want to do. I guess if the design guidelines say you shouldn’t return null wouldn’t this be a good reason to leave it as non-nullable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess if the design guidelines say you shouldn’t return null wouldn’t this be a good reason to leave it as non-nullable?
Except then if something did return null, a caller that does ToString().Length won't get a warning and will null ref.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For other types we haven’t annotated the return type of ToString as nullable.
@jaredpar can correct me if I'm wrong, but I believe we allow for overrides to be more constrained than a base, e.g. an override could be string
with the base being string?
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll wait for @jaredpar to confirm, if overrides can be more contrained, then it makes sense to make Object.ToString()
return string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have expected that, if not matching was allowed, then narrowing (going from nullable to non-nullable) works, without a warning and widening (going from non-nullable to nullable) doesn't work (because virtuals and that means the base could be "lying").
- This is, of course, assuming both types in the hierarchy have opted-into nullable, since things all go out the window otherwise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent is to allow for parameters to allow widening on overrides, and narrowing on returns. There is some work in the compiler left to implement this completely but I thought that was in the interface
case only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For reference, here is sharplab showing both cases using last night's master bits:
https://sharplab.io/#v2:EYLgtghgzgLgpgJwD4AEAMACFBGA3AWACgiBiAOwFcAbKiYKuDOMuhoolAZiwCYMBhDAG8iGMVm4A3AJYIYFCFSzY0AfgwBZABQBKYaPGGUAdgyUaBQoYC+BsXYkYZchUpyYAcrv1XD4kxgARIGWNkS2xIRcvBgAIhggAj5G3AD2kogI0gAmjO6a3iK+flimwaHiEYYO0emZOXkq6l56RSViAeZUFWIR1kA=
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tannergooding We've not implemented the narrowing/widening rules in overrides yet.
Tracked by dotnet/roslyn#23268
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One comment/question, otherwise LGTM.
Merging without marking
|
I disagree. If something consistently returns null, you're going to catch it immediately in tests. If something only does sometimes, this feature helps alert you to your mistake when you dereference it. If we make Object.ToString non-nullable despite it easily returning null, I do not understand the benefit of this feature. We'll actually be making consuming code more likely to null ref, because developers will see it's non-null and won't guard. |
Your concern is for APIs that haven't yet been annotated and could potentially return null? Because when we annotated all of the |
No, it doesn't matter if they're annotated. A developer using the base class only sees the base class annotation. The base class says "this can't be null", and yet it can be, easily. |
@stephentoub you can also workaround any non-nullable by setting it to null by reflection or native code - I'd rather treat it as hint and what the expectation should be - if someone messes it up on purpose that's rather unexpected and perhaps good that we get some kind of unexpected error like NRE or something else |
This isn't about unsafe code or private reflection or trying to mess things up on purpose. ToString has existed for two decades. Implementations return null. Period. |
@terrajobst thoughts? I'm personally slightly toward this being non-nullable but not very strongly (I cannot see a good reason to return null from this API but from practical point of view this might (possibly will?) fall over) |
I agree that it doesn't make sense for I think the best scenario is for us to say this (the return of I don't think we should be in the business of breaking existing code that "just works" for people. Instead, we are only providing the metadata so that consumers can opt-into turning their code that would 100% fail at runtime into code that fails at compile time. |
Wouldn't a person overriding
I disagree because there are many many call sites where the consuming code will be calling I'm all for pragmatic choices but we also we need to consider the impact here: if we create too many false positives the feature also loses value. |
Only if they opt-in to enabling nullability warnings. There's potentially multiple parties involved here:
The former can have warnings enabled and the latter not. Which with the feature being opt-in seems pretty likely.
For new libraries, I agree. For existing libraries, I'm not sure the outcome would be to change the implementation to stop returning null; I would not be surprised if the majority of devs that get that warning would have a first reaction of just using
I walk up to an arbitrary |
Taking your argument to its logical conclusion it would mean that all virtual methods in the framework that return reference types must be annotated as nullable then. I assert that this isn't useful. |
Neither is the feature if it outright lies and simply can't be trusted. I honestly don't know what the benefit of the feature is if a method like ToString that can and does return null says that it can't. |
I think this largely depends. We likely have a number of methods where the contract of the API is indicative that the return type must not be null. We also likely have a number of APIs where the API is clear that the return type can be null. Unfortunately, I think we also have a category of APIs where the ideal would be non-null, but the documentation is ambiguous. Perhaps the ideal scenario would be to collect these up and do a special API review to determine their nullability contract publicly and see if anyone has any strong objections to various things. It probably shouldn't be too hard to look at a number of popular open source projects and determine if any of them return null for a given ToString override (either via IL reflection or a source code analyzer). In any case, we need to try and ensure nullability is being considered in API reviews going forward and maybe we should look at requiring API proposals to include this information moving forward. |
Signed-off-by: dotnet-bot <[email protected]>
Signed-off-by: dotnet-bot <[email protected]>
Signed-off-by: dotnet-bot <[email protected]>
Signed-off-by: dotnet-bot <[email protected]>
It wouldn't affect |
This is true. I don’t think I have ever seen code that checked the return value of This feature works in the same way really: It does not give you ultimate safety, that just doesn’t work (for reasons shown by @tannergooding above for example). But it will give you a sane idea on how things should behave, so you can work with that. If While I understand that this should reflect the reality, the reality is also that |
@stephentoub that's fair, I had misremembered that null handling was only for the string[] with params. Still we certainly have other places or wrappers for Format() where I would expect us to be non-nullable even if the BCL isn't. Certainly the main point remains, that you really don't see guard code around ToString (or at least it is pretty rare). |
I do not feel the "should not return Empty" should be as forbidden as null as there are many common cases where an empty string is the best return value, for example |
@isaacabraham We will have non-nullability assertions through a function. @stephentoub We should not consider the use of I don't have a say in CoreFX but given that docs and conventional use of |
If that's the bar, the feature is going to be failing lots of people. |
I stand by what I said. It will not be 100% even in your ms framework code non-virtual methods, unless you are sure that no code path returns a reference that could be publicly provided, because then it's legit possible for something in your framework annotated nonnull to return a null, since not every in the wild framework, that your opted-in-consumer is going to use will be opted-in, and that wild framework could be interacting with your framework classes. The expectation just won't be 100%, but 99.8% isn't bad, has worked for F#. |
@jbtule, my point is that when we own all of the code involved, we can have a goal of 100%, and anything short of that is a bug. If we don't own all the code involved, we can't have a goal of 100%. |
I went to sleep firmly in the camp that feels I am still certain that we should not make all existing overridable returns nullable. IMHO, that can only lead to low feature adoption and code that is overly Looking at the call sites to
Here is String.Concat being fully defensive:
And here is TypeConverter treating null object as converting to empty string, but otherwise letting a null from object.ToString() out. |
@stephentoub My point is owning 100% of the code in something with enough public exposed properties as corefx, is not a guarantee to owning 100% of the references. And while it is easier to think about breaking of the non nullable contract by third parties with virtual methods, it's not crazy to think that non-virtual methods contract could be inadvertanly broken by some third party dependency to the surprise of the end consumer (think dependency injection, autofac). So then corefx virtuals and non-virtuals aren't so different in respect to the nullablity contract and it really should be about intent. |
It doesn't matter if we "own" 100% of the references. If we're handed an input that has any chance of being null and we might return it out of something we've said is never null, then we better be validating that reference... if we're not, that's a bug. |
🤔 , you may have convinced me that corefx specifically, needs all virtuals and ( it probably just a given on interfaces) to return nullable refs. |
If this is the case and we say the bug is on our signature, then all public/protected virtuals must have nullable returns and we live in a very sad world where the feature loses an enormous amount of value. |
Well... what's the point of this feature if
So at runtime then. |
I'm not sure how you jumped to the "is on our signature" part. If we believe that the right answer is for the return type to be non-nullable, then the bug isn't in the signature, but rather the implementation, and we should have been validating the input and either throwing or producing some non-null result. I fully expect that we will have some signature bugs in this first go-around. But hopefully with the number of eyes on all of this, the number of those will be minimal, and the majority of bugs that might still exist would actually be in implementation.
I think that should be the default position we take, that the return type of abstract/virtual methods where we don't control the complete hierarchy should be nullable. I agree with @nguerrera that in cases where the likelihood of someone other than us deriving from that type is rare, we could reconsider, e.g. with System.Type. But everyone derives from object.
What's the point of the feature if an API says it can never return null and then does? "Trust will rapidly evaporate and people will end up null checking anyway." |
If we say something is non-nullable and an overrider returns Perhaps an ideal solution here would be for:
|
Which is exactly why we need to be super cautious about making such statements in the first place, and the default should be as I outlined.
My understanding is it already shouldn't be warning to return |
@stephentoub is it worth listing which types exactly are currently returning nulls from Another option is of course to fix those implementations (or some of them) to not return null. That would be a breaking change but it may be worth exploring. Finally, as I wrote above, unless I'm mistaken there's nothing stopping us from annotating nullable strings for types which return null while keeping it non-nullable for the rest (including |
Obviously I can't do that for the vast majority of types in the world. For coreclr/corefx specifically I've not audited the hundreds of ToString overrides. On a code reviewer earlier in the week for something unrelated I did happen to notice one, though:
You can do the opposite: have the base return |
|
So is If not and it needs a runtime check on the constructor, having ToString() be non-nullable would've alerted this bug. 🤔 |
No idea. But it can be. To be clear, for new APIs introduced after the nullability feature exists, I think it's ok to use the feature as a way to document new guarantees a virtual method intends, and then it's definitively a bug if an implementation violates that. But we missed the boat there for existing stuff. With Just quickly browsing around some of the repos I happen to already have cloned locally, here's an example from Roslyn: This is not rare. |
I'm far from an expert on the examples you're citing, but it seems like some are cases where |
It's a struct. default(SymbolDisplayPart).ToString() will return null. |
These examples are all things that look like they aren't intended to |
If we were starting today, sure, we could make it non-nullable, and then it would help overrides to get it right. But we're not. There a bazillion implementations already in the wild. And consumers see that same annotation. |
I filed dotnet/roslyn#34604 to make sure that the completion of |
I think we've collected enough feedback here. Well publish a more carefully written document with our current thinking as a Markdown file in the CoreFx repo. We'll tune this document as we find more interesting APIs as part of us annotating the code base and, of course, based on customer feedback consuming said annotations. To keep things organized, I'm going to lock this thread. |
This one is a simple one.Edit