The `?.` operator behaves similarly on the LHS to the RHS, making the language more consistent, which is always a good thing. In terms of readability, I would say that once you understand how the operator works (which is intuitive because the language already supports it on the RHS), it becomes more readable than wrapping conditionals in `if` statements.
There are downsides, such as the overuse I mentioned. But this is true for many other language features: it requires experience to know when to use a feature appropriately, rather than applying it everywhere.
However, the great thing about this particular enhancement is that it's mostly cosmetic. Nothing prevents teams from not adopting it; the old syntax still works and can be enforced. C# and .NET are incredibly versatile, which means code can look dramatically different depending on its context and domain. For some projects, this feature might not be needed at all. But many codebases do end up with various conditional assignments, and in those cases, this can be useful.
It feels like the C# designers have a hard time saying "no" to ideas coming their way. It's one of my biggest annoyances with this otherwise nice language. At this point, C# has over 120 keywords (incl. contextual ones) [0]. This is almost twice as much as Java (68) [1], and 5 times as much as Go (25) [2]. And for what? We're trading brevity for complexity.
[0]: https://learn.microsoft.com/en-us/dotnet/csharp/language-ref... keywords/
Hi there! C# language designer here :-)
In this case, it's more that this feature made the language more uniform. We've had `?.` for more than 10 years now, and it worked properly for most expressions except assignment.
During that time we got a large amount of feedback from users asking for this, and we commonly ran into it ourselves. At a language and impl level, these were both very easy to add in, so this was a low cost Qol feature that just made things nicer and more consistent.
> It feels like the C# designers have a hard time saying "no" to ideas coming their way.
We say no to more than 99% of requests.
> We're trading brevity for complexity
There's no new keyword here. And this makes usage and processing of `?.` more uniform and consistent. Imo, that is a good thing. You have less complexity that way.
Property reads were fine before (returning null if a part of the chain was null), method invocations were fine (either returning null or just being a no-op if a part of the chain was null). But assignments were not, despite syntactically every ?. being basically an if statement, preventing the right side from executing if the left side is null (yes, that includes side-effects from nested expressions, like arguments to invocations).
So this is not exactly a new feature, it just removes a gotcha from an old one and ensures we can use ?. in more places where it previously may have been useful, but could not be used legally due to syntax reasons.
As polyglot I have the advantage that I don't have to sell myself as XYZ Developer, and increasingly I don't think C# (the language itself) is going into the direction that I would like, for that complexity I rather keep using C++.
Just wait for when extension everything, plus whatever design union types/ADT end up having, and then what, are they going to add on top to justify the team size, and yearly releases?
Despite my opinion on Go's design, I think the .NET team should take a lesson out of them, and focus on improving the AOT story, runtime performance, and leave the language alone, other than when needed to support those points.
Also bring VB, F# and C++/CLI along, this doesn't not have to be C# Language Runtime, where it gets all the features of what designed as a polyglot VM.
Yes, actually. I did write it multiple times naturally only to realize it was not supported yet. The pattern is very intuitive.
You can argue that C# gets a lot of new features that are hard to keep up with, but I wouldn't agree this is one of them. This actually _reduces_ the "mental size" of C#.
IDK, if you read
Settings?.RetryPolicy = new ExponentialBackoffRetryPolicy();
as "there is a now a ExponentialBackoffRetryPolicy" then you could be caught out when there isn't. That one ? char can be ignored .. unless it can't. It's another case where "just because it compiles and runs doesn't mean that it does the thing".This to me is another thing to keep track of. i.e. an increase in the size of the mental map needed to understand the code.
https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
You can force it all the way down to ISO-1/2.
If this is still insufficient, then I question what your goals actually are. Other people using newer versions of C# on their projects shouldn't be a concern of yours.
Obviously you’re not alone to disagree, and there are even some good arguments you could potentially be making. But to say “I question what your motives really are” and tell someone what they should be concerned with is… odd?
It’s a very common position with ample practical examples. While there certainly are valid counter arguments, they are a little more involved than “nothing is stopping you.” There is. Collaborating with others, for example.
If I say an asseignment I expect the value to be "evaluated".
I could have grasped the expression in all the null values would have been replaced with new instances, but then it would have been too much invasive and magic to work, so - again - I understand why the designers night have been force to settle for the actual behaviour...
But maybe then the half-solution is not worth it
If it’s rarely used, people may misinterpret whether the RHS is evaluated or not when the LHS doesn’t exist (I don’t actually know which it is).
Optional operations and missing properties often require subtle consideration of how to handle them. You don’t want to make it too easy to say “whatever”.
I fully expect no RHS evaluation in that case. I think the fear is misplaced; it's one of those "why can't I do that when I can do this" IMO. If you're concerned, enable the analyzer to forbid it.
There are already some really overly paranoid analyzers in the full normal set that makes me wonder how masochistic one can be...
For example in my last gig, the original devs didn't understand typing, so they were forever writing typing code at low levels to check types (with marker interfaces) to basically implement classes outside of the classes. Then of course there was lots of setting of mutable state outside of constructors, so basically null was always in play at any moment at any time.
I would have loved this feature while working for them, but alas; they were still on 4.8.1 and refused to allow me to upgrade the codebase to .net core, so it wouldn't have helped anyway.
So no, c# are not constantly null-checking more than in Rust
More readable? I'm less convinced on that one.
Some of those edge cases and their effects can get pretty nuanced. I fear this will get overused exactly as the article warns, and I'm going to see bloody questions marks all over codebases. I hope in time the mental overhead to interpret exactly what they're doing will become muscle memory...
In all these examples I feel something must be very wrong with the data model if you're conditionally assigning 3 levels down.
At least the previous syntax the annoyingness to write it might prompt you to fix it, and it's clear when you're reading it that something ain't right. Now there's a cute syntax to cover it up and pretend everything is okay.
If you start seeing question marks all over the codebase most of us are going to stop transpiling them in our head and start subconsciously filtering them out and miss a lot of stupid mistakes too.
This is a fantastic way to make such nasty behavior easier.
And agreed on the question mark fatigue. This happened to a project in my last job. Because nullable types were disabled, everything had question marks because you can't just wish away null values. So we all became blind and several nullref exceptions persisted for far too long.
I'm not convinced this is any better.
if (This) {
if (is) {
if (much) {
if (better) {
println("I get paid by the brace")
}
}
}
}
what?.could?.possibly.go?.wrong = important_value()
Maybe we want code like this: if (!what)
what = new typeof(what); // default-construct representative instance
if (!what.could)
what.could = new typeof(what.could);
if (!what.could.possibly.go)
what.could.possibly.go = new typeof(what.could.posssibly.go)
// now assignment can take place and actually retain the stored value
// since we may have allocated what, we have to be sure
// we propagate it out of here.
what.could.possibly.go.wrong = important_value();
and not code which throws away the value (and possibly its calculation).Why would you ever write an assignment, but not expect that it "sticks"? Assignments are pretty important.
What if someone doesn't notice the question marks and proceeds to read the rest of the code thinking that the assignment always takes effect? Is that still readable?
It should be clear enough that this operator isn't going to run 'new' on your behalf. For layers you want to leave missing, use "?.". For layers you want to construct, use "??=".
> Why would you ever write an assignment, but not expect that it "sticks"? Assignments are pretty important.
If you start with the assignment, then it's important and you want it to go somewhere.
If you start with the variable, then if that variable doesn't have a home you don't need to assign it anything.
So whether you want to skip it depends on the situation.
> What if someone doesn't notice the question marks and proceeds to read the rest of the code thinking that the assignment always takes effect? Is that still readable?
Do you have the same objection with the existing null-conditional operators? Looking at the operators is important and I don't think this makes the "I didn't notice that operator" problem worse in a significant way.
(Gets a lot better if you enable nullable references and upgrade the nullable reference warnings to errors.)
you didn't null check possibly.go.
if (Actually
&& Actually.you
&& Actually.you.would
&& Actually.you.would.write
&& Actually.you.would.write.it
&& Actually.you.would.write.it.like) {
return this;
}
if (!same) {
return;
}
if (!number) {
return;
}
if (!of_braces) {
return;
}
println("but easier to read")
What?.could?.possibly?.go?.wrong?
Not so convinced: What?.could?.possibly?.go?.wrong = important_value()
Maybe the design is wrong if the code is asked to store values into an incomplete skeleton, and it's just okay to discard them in that case.Most companies don't care about this kind of stuff.
I work across Java, C#, JS/TS, C++, SQL, and whatever else might be needed, even stuff like Go and C, that I routinely criticise, because there is my opinion, and then there is the job market, and I rather pay my bills.
I struggle to even see how anyone would prefer that over an explicit if before assigning.
Having that on the right side (attribute reference) is great, but that was already available as far as I understood the post...
Maybe my feeling is just rooted in the fact I've never used a language which allowed ?. on assignment
So as a casual observer, I'd say it brings more consistency.
But also as a casual observer, my opinion is low-value.
But the code gets really hard to understand when you encounter code that uses a subset you aren't familiar with. I remember staring at C++ codebases for days trying to figure out what is going on there. There was nothing wrong with the code. I just wasn't too familiar with the particular features they were using.
* The above is just applying an existing (useful) feature to a different context. So there isn't really much learning needed, it now just 'works as expected' for assignments and I'd expect most C# engineers to start using this from the get go.
* As a C# and C++ developer, I am always excited to hear about new things coming in C++ that purportedly fix some old pain points. But in the last decade I'd say the vast majority of those have been implemented in awful ways that actually make the problem worse (e.g. modules, filesystem, ...). C#'s new features always seem pretty sound to me on the other hand.
Static abstract methods are probably the feature I see used least (so far!) and they’re not nearly as hard to understand as half of the stuff in a recent C++ standard.
It feels like Microsoft just wants C# to be Python or whatever and the language is losing its value and identity. It's becoming bland, messy, and complicated for all the same reasons they keep increasing padding between UI elements. It's very "me too" and I'm becoming less and less interested in what I used to consider my native language.
I used to write any and all little throwaway programs and utilities in C#, but it just keeps getting more and more fussy. Like Python, or maybe java. Nowadays I reach for C++. It's more complicated, but at least it's stable and not trying to rip itself apart.
Oh, how happy would I be if Python had a sliver of C# features. There's nothing like null-conditionals in Python, and there are many times I miss them
Additinoally, I think they are becoming hostage that every year C# gets a new release, thus the team has to keep pushing features no matter what.
Imagine how C# will look a decade from now with this rythm.
Slowly I am starting to think it is not that bad, that most of the .NET projects that our agency does are still stuck on Framework.
I’m glad it’s now a thing. It’s an easy win, helps readability and helps to reduce the verbosity of some functions. Love it. Now, make the runtime faster…
I feel like this is another step in the race to add every conceivable feature to a language, for the sake of it.
string x = null;
string y = x ?? throw new ArgumentException("x is null");
It would be interesting to try something like: customer?.Name = newName ?? throw new InvalidOperationException("customer is null");
But I don't know how the language would be able to determine which potential null it was throwing for: 'customer' could be null, but so could 'newName'. I guess... maybe you could do: (customer ?? throw new InvalidOperationException("customer is null")).Name = newName ?? throw new ArgumentException("newName is null");
But the language already supports that, and it's extremely ugly...I have a feeling this is going to make debugging code written just a few months ago incrementally difficult. At least the explicit if statements are easier to follow the intent from months ago.
The syntax is clean though. I'll give it that.
I couldn't imagine what a "Null-Conditional Assignment" would do, and now I see but I don't want this.
Less seriously, I think there's plenty of April Fools opportunity in this space. "Null-Conditional Function Parameters" for example. Suppose we call foo(bar?, baz?) we can now decide that because bar was null, this is actually executing foo(baz) even though that's a completely unrelated overload. Hilarity ensues!
Or what about "Null-Conditional Syntax". If I write ???? inside a namespace block, C# just assumes that when we need stuff which doesn't exist from this namespace it's probably just null anyway, don't stress. Instead of actual work I can just open up a file, paste in ???? and by the time anybody realises none of my new "classes" actually exist or work I've collected my salary anyway.
That is why although they are much requested, none of the proposals that I have seen are simple to understand or easy to implement, and thus are proceeding slowly.
I don't really see Discriminated union as being in "competition" with "a?.b = c" as that's a "quick win" extension to previous ?. and ?? syntax. It's not even close to being of the same magnitude.
I would settle for a good built-in Result<T, E> type, so that people don't roll their own crappy ones, or use various opinionated but incompatible libraries.
Isn't this over engineered? Why not allow the assignment but do nothing if any of the intermediate objects is null (that's how Kotlin does it).
customer?.Profile?.Avatar = "thing"
And will do nothing if the left hand side is null (not throw a null reference exception anymore)The motivation is that you don't want the side effects in some cases like GetNextId() but I think it's still strange. I hacven't thought deeply about it but i _think_ I'd rather keep the intuitive right-hand-first evaluation and explicitly have to use if (..) in case I have a RHS whose side effects I need to avoid when discarded.
> If config?.Settings is null, the assignment is skipped.
If the right hand expression has side effects, are they run? I guess they do, and that would make the code more predictable.
Side-Effect Prevention When a null-conditional statement assignment is evaluated, the right-hand side of the expression is not executed unless the left-hand side is defined.
I really dislike that, because it hides the control flow too much. Perhaps I'm biased by Racket, where it's easy to define something weird using macros, but you should not do unexpected weird things.
For example you can define vector-set/drop that writes a value to a position of a vector, but ignores the operation when the position is outside the vector. For example
(vector-set/drop v -2 (print "banana"))
With a macro is possible to skip (print "banana") because -2 is clearly out of range, but if you do that everyone will hate you.I think it is for situations where the programmer wants to check a child property but the parent object may be null. If the parent is expected to be null sometimes, the syntax lets the programmer express "try to get this value, but if we can't then move on" without the boilerplate of explicitly checking null (which may be a better pattern in some cases).
It's sort of like saying:
- Get the value if you can, else move on. We know it might not be there and it's not a big deal.
v.s.
- The value may not be there, explicitly check and handle it because it should not be null.
You can't banish the "absence of value" from any programming language. That wouldn't be a useful language. You can stop confusing "a string but perhaps not" as a single type "string" as C# did in the past though.
When you have an expression P which names a mutable place, and you execute P := X, the contract says that P now exhibits the value X, until it is assigned another value.
Conditional assignment fucks this up. When P doesn't exist, X is not stored. (Worse, it may even be that the expression X is not evaluated, depending on how deep the fuckery goes.)
Then when you access the same expression P, the conditional assignment becomes conditional access and you get back some default value like a nil.
Store X, get back nil.
That's like a hardware register, not a durable memory model.
It's okay for a config.connection?.retryPolicy to come up nil when there is no config.connection. It can be that the design makes nil a valid retry policy, representing some default. Or it could be that it is not the case, but the code which uses connection? handles the nil soon afterward.
But a store to config.connection?.retryPolicy not having an effect; that is dodgy.
What you need for config.connection? to do when the expression is being used to calculate a mutable place to assign to is to check that config.connection is null, and in that case, instantiate a representative instance of something which is then assigned to config.connnection, such that the config.connection.retryPolicy place then exists and the assignment can proceed.
This recognizable as a variation on COW (copy-on-write); having some default instance for reading, but allocating something on writing.
In a virtual memory system, freshly allocated memory can appear to contain zero bytes on access due to all of its pages being mapped to a single all-zero frame that exists in the entire system. Conceptually, the hardware could do away with even that all-zero frame and just have a page table entry which says "this is a zero-filled page", so the processor then fakes out the zero values without accessing anything. When the nonexistent page is written, then it gets the backing storage.
In order to instantiate settings.connection? we need to know what that has to be. If we have a static type system, it can come from that: the connection member is of some declared type of which a representative instance can be produced with all constructor parameters defaulted. Under a dynamic paradigm, the settings object can have a handler for this: a request to materialize a field of the object that is required for an assignment.
If you don't want a representative config.connection to be created when config.connection?.retryPolicy is assigned, preferring instead that config.connection stays null, and the assignment is sent to the bit buckets, you have incredibly bad taste and a poor understanding of software engineering and programming language design --- and the design of your program is scatter-brained accordingly.
This isn't the case, though, is it? A normal member access (or indexer) expression may point to a mutable location (field, property). However, with conditional access expressions you get either a member access _or nothing_. And that nothing is not a mutable place.
When you use any of the conditional operators, you split the following code into two paths, and dropping the assignment (since there's nothing to assign to) seems pretty consistent to me, since you'd also drop an invocation, or a property evaluation in similar expressions.
If you require P to point to something that actually exists because you want the assignment to succeed, then write code to ensure that P exists because the assignment has no way of knowing what the intention was on the left side.
And we all get to choose what we find ridiculous:
i = i + 1 ? No it does not. Never has, never will.
Connection is null? It's insane to type it as Connection then. null has type Null.
The config example isn't the best, but instead imagine if it was just connection.?retryPolicy. After you set connection?.retryPolicy it would be weird for reading it back to be null. But it would be just as weird for connection?.retryPolicy to not be null when we never established a connection in the first place.
The copy on write analogy is tempting but what you're describing only works when the default value is entirely made of nulls. If you need anything that isn't null, you need to actually make an object (either upfront or on first access). And if you do that, you don't need ?. anymore.
That's the mindset the feature is developed for (and by).
I just can't imagine Gen Z wanting to start a project in C#.
I realise there are still .NET shops, and I still talk to people who do it daily, but ours is a field driven by fashion whether we care to admit or not - and C# just does not feel as fashionable as it once did
(I'm a former C# dev, up until 2020)
JS has lost against TS which is basically C# for web (both designed by the same person) and Python is not really something you should build large applications with (execution speed + maintenance issues).
What do you believe is the current language du jour?
I didn't ask about whether it was good.
I asked about whether it's past its peak.
I wouldn't say it's past its peak because it's still improving and there is no good alternative for a language of its class. Go isn't it (I doubt there will be a good desktop/mobile app/game engine etc story for Go in the future). Swift could have been a competitor in the allround space but Apple doesn't seem interested in conquering the world outside its own garden. I'm not sure who it would be that would make the "next" C# and .NET. Only Microsoft and Apple are making commercial desktop environments, for example.
It's portable, fast, productive and well supported by a massive corp. It's not just a "language du jour", it's here to stay.
There are plenty of job in dotnet where I live: old, new, startups...
I am the momentum!