Nullable compound assignments are inconvenient.
With the current Null Safety behavior, a nullable variable like int? x; cannot be incremented as easily as a non-nullable variable, even when you know that it currently contains a non-null value.
We have ++x and x += 1 as easy composite operations on non-nullable types, but for the nullable type, when you know it's not null, the only option is x = x! + 1 because we need to insert the ! in the middle of the expression that ++x or x += 1 expands to.
It would be nice to have something like x!+=1 which expands to x = x! + 1.
Draft writeup: https://github.com/dart-lang/language/blob/9a83a94c0278180674dfdd09d97c9eab3a624221/working/1113%20-%20null-asserting%20compound%20assignment/proposal.md
I would say x!++ or ++x! seem decent too.
@lrhn are you still planning to push on this?
FWIW I haven't seen much of a need for this in practice yet.
@Hixie 👍 me neither
Have written >100k loc in NNBD and not encountered it.
Theory: ++ is rarely used anyway, but when it is used, it is used mostly for loops. When you loop and have an index for your loop, that index will be non-nullable to begin with.
Examples:
for (var i = 0; i < n; i++)
var i = n;
while (i > 0) {
i--;
}
(same roughly goes for the other compound assignments)
For ++ specifically I'd be fine with just removing it from the language. :-D
For ++ specifically I'd be fine with just removing it from the language. :-D
Haha, off topic but any reason?
no good reason, though I tried to come up with one to justify https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#prefer-over
I don't think it's worth pushing it right now. We have enough on our plate for the release. It's a feature that can be safely added later since it introduces new syntax which is currently invalid.
I think it is worth keeping the issue open in case users end up wanting to use null-aware compound assignments. Then they have a place to report that.
As I was experimenting NNBD, I ran into a similar issue. I post here a sample code in case it is relevant. I think the lint warning should not happen here.
class A {
String? value;
}
class B {
A? a;
}
void main() {
var b = B();
b.a = A();
// Ok
b.a!.value = '123';
// Lint error!
// An expression whose value can be 'null' must be null-checked before it can be dereferenced.
b.a!.value += '456';
}
@alextekartik The lint is correct because the class member could change from the outside. See https://github.com/dart-lang/sdk/issues/42626.
So the flow analysis cannot be sure that a!.value was modified somewhere else, e.g. when you have a custom setter.
@creativecreatorormaybenot Ok I understand. I guess it was just puzzling but you are right it is correct.
I think it might make sense to simplify this issue to "the non-null assertion operator ! should not remove assignability".
If that could be achieved it would solve for all the cases x!++ (where x is a nullable int), someMap[someKey]!++, etc.
It's clear that the syntax is parsing appropriately for this already, since the error on trying to do it that way right now is Illegal assignment to non-assignable expression
Note, changing it this way would not introduce any new syntax, it would just make the existing syntax work the way a user would expect.
Note, changing it this way would not introduce any new syntax, it would just make the existing syntax work the way a user would expect.
(Puts on pedantic hat.) It is technically a change in syntax, which is why the current syntax yells at you. The thing to the left of ++ or = is not an expression, it's an assignment target. In C/C++ terms, it's the difference between an lvalue and an rvalue. It happens to be the case that all lvalues are a subset of the expression syntax, but there are many expressions that are not valid assignment targets, like:
1 + 2 = 3;
[4, 5, 6] = 7;
(parentheses) = value;
-negative = positive;
Assignment targets are also the things allowed to the left of an increment/decrement operator. The ! null-assertion operator is yet another expression that has no corresponding assignment target sibling. We would have to add new syntax (and semantics) to allow a ! there.
(Puts on pedantic hat.) It is technically a change in syntax, which is why the current syntax yells at you.
Semantic perspective difference I think. From the user perspective the syntax isn't new with that change, it's the existing syntax with different (better) behavior. But fair enough if from the dart developer perspective it's an addition of a different operator. :)
I've encounter this use case and was wondering why it didn't work:
enum Stat {
strength,
agility,
wisdom,
charisma,
}
@observable
ObservableMap<Stat, int> stats = ObservableMap.of({
Stat.strength: 1,
Stat.agility: 1,
Stat.charisma: 1,
Stat.wisdom: 1,
});
@observable
ObservableMap<Stat, int> updatedStats = ObservableMap.of({
Stat.strength: 0,
Stat.agility: 0,
Stat.charisma: 0,
Stat.wisdom: 0,
});
@action
void levelUp() {
//this works fine
stats[Stat.strength] = stats[Stat.strength]! + updatedStats[Stat.strength]!;
stats[Stat.agility] = stats[Stat.agility]! + updatedStats[Stat.agility]!;
stats[Stat.charisma] = stats[Stat.charisma]! + updatedStats[Stat.charisma]!;
stats[Stat.wisdom] = stats[Stat.wisdom]! + updatedStats[Stat.wisdom]!;
// but wanted to write this at first but doesn't compile ^^ I was a bit frustrated lol
stats[Stat.strength]! += updatedStats[Stat.strength]!;
....
}
Just wanted to share :)
Capturing from a chat discussion, I've seen this come up in a few places now specifically for counting maps, where the pattern is often to initialize everything to zero, then do a lot of foo[bar]++ (or foo[bar] += 1) for the counting. Having to put the lookup on both sides hurts maintainability since it creates the possibility of a bug where the key is changed in only one of the two places, and is presumably worse for efficiency as well (unless in efficiency terms it was actually expanding out to two lookup under the hood before?)
I've started running into this as well when working with package:vm_service. We have lots of nullable fields since the package needs to be able to support potentially missing properties, so where I could previously do:
function.inclusiveTicks++;
I now need to do:
function.inclusiveTicks = function.inclusiveTicks! + 1;
Which is significantly more verbose and doesn't feel very Dart-y :-(
Yes this is ugly. Should be able to do x!++;
I'm a novice coming mostly from Python so not sure if this even makes sense but another option could be (x==null) ? 0 : x++; or x ? x++ : 0;
Also ought to be able to do x?++, I think.
// x = x! + 42;
x! += 42;
// x == null ? null : (x = x! + 42);
x? += 42;
// x == null ? null : (x = x! + doSomethingWithSideEffects());
x? += doSomethingWithSideEffects();
// x = y == null || z == null ? null : y! + z!;
x = y? + z?;
// x == null ? null : (x = x! * y + z);
x? = x * y + z;
Hi all,
I also just came across this inconvenience. In my case it happened as part of a function that checks whether a string consists of the expected parts, where one of the parts should match the entry of a given list of strings. For keeping track of the entry, I have declared an integer-optionally-null variable at the top of the algorithm, that is initialized to 0 when the algorithm gets to the place where the string part needs to be compared to the entries of the list, and that is incremented when the current entry does not match to prepare the comparison for the next eventual entry in the list. My code looks something like this:
import 'package:collection/collection.dart';
({int entryIndex}) checkText(String text, List<String> names) {
int? entryIndex;
var state = 0, index = 0;
final caseInsensitiveStringComparer = CaseInsensitiveEquality();
while (true) {
late String expectedText;
late String checkedPartName;
switch (state) {
case 0:
expectedText = 'text';
checkedPartName = 'Start';
case 1:
case 3:
expectedText = '_';
checkedPartName = 'Separator';
case 2:
entryIndex ??= 0;
if (entryIndex < names.length) {
expectedText = names[entryIndex];
checkedPartName = 'Middle';
} else {
throw FormatException('Name not matching');
}
case 4:
expectedText = 'text';
checkedPartName = 'End';
default:
throw StateError('Unexpected state');
}
if (caseInsensitiveStringComparer.equals(text.substring(index, index + expectedText.length), expectedText)) {
state++;
index += expectedText.length;
} else {
if (state == 2) {
entryIndex!++; // <-- Not null operator combined with increment operator
if (state == 5) {
break;
}
} else {
throw FormatException('$checkedPartName is not matching the expected text');
}
}
}
late String missingPartName;
switch (state) {
case 0:
missingPartName = 'Start';
case 1:
case 3:
missingPartName = 'Separator';
case 2:
missingPartName = 'Middle';
case 4:
missingPartName = 'End';
case 5:
return (entryIndex: entryIndex);
default:
throw StateError('Unexpected state');
}
throw FormatException('$missingPartName is missing');
}
I would use such an operator here, if it existed. But, I also see that I can change the entryIndex variable to a non-nullable integer variable and initialize it to 0 with its declaration.
One thing you can do is
entryIndex!; // Promotes to non-null
entryIndex++; // Now works.