Dart: Null-safety and retrieving values from maps - flutter

there is one thing I can't quite wrap my head around concerning null-safety in Dart, and that concerns how to safely retrieve values from Map<String,dynamic> (I've read the FAQ from the Dart docs).
Basically, the following code in the DartPad with null-safty enabled is valid:
void main() {
int i;
Map<String, dynamic> map = {"key": 1};
i = map["key"];
print(i);
}
Which I do not understand. Why can I assign map["key"] to i without the compiler shouting at me? From the docs:
Code should be safe by default. If you write new Dart code and don’t
use any explicitly unsafe features, it never throws a null reference
error at runtime.
But exactly this is happening. If, in the code above, the key is not in the map, or contains some random type, the program will crash on runtime, which I though is what should never happen with null safety.
I'm particular interested in this since I'm writing a Flutter app and don't understand how to properly deserialize the JSON data I fetch from the DB (try..catch? Special syntax like ??= ?). Even though I don't have the 'non-nullable' language feature enabled (I can't even write int? val without getting a warning), the compiler does not seem to mind that I assign nullable values to non-nullable variables, and will happily crash on runtime if they are null.
Basically, my question does not only concern null-safety, but the type system in general, since from my understanding it shouldn't be possible to assign a dynamic value to an int variable, but obviously this works with Map. Any explanation is greatly appreciated!

A dynamic variable can contain any data type, "dynamic" keyword is used when you don't know the specific data type that might be returned.
and what you're actually doing here:
i = map["key"];
is assigning a "dynamic" variable to an "int" variable and since dynamic in this case the value in the key/value pair of your Map is an integer which matches the data type "int" of variable "i" it won't crash because of type inference done at runtime and not compile time. if the "dynamic" variable was a String it would crash at runtime because of a type mismatch. Hope this is explanatory.

Related

Dart/Flutter linter rule: the type to index a map should be the key type of map?

For example, I have Map<int, int> m;. Then I can write down m['hello'] without any compile-time error, but of course, cannot find any element at runtime. I hope it will produce an error (or warning) at compile-time or lint time.
This is a big problem in many cases. For example, when I refactor Map<A, int> m into Map<B, int> m, I want to have compile-time errors for all accesses like m[some_var_of_type_A], instead of no compile-time errors and suddenly it explodes at runtime. As another example, the de-serialized JSON is of type Map<String, ...> but the key is actually a int. So it is tempting to do var userId=42; deserializedJson[userId] but only to find errors. Actually need to do deserializedJson[userId.toString()].
You know, dart's type system is so strong (even null safe!), and I really enjoy it since it catchs a LOT of bugs at compile-time. So I hope this problem can also be addressed at compile-time.
Thanks for any suggestions!
There currently is no lint to warn about doing lookups on a Map with arguments of the wrong type. This has been requested in https://github.com/dart-lang/linter/issues/1307.
Also see https://github.com/dart-lang/sdk/issues/37392, which requests a type-checked alternative to Map.operator []. In the meantime, Dart's extension mechanism allows anyone to easily add such an alternative themselves. For example, package:basics provides a type-checked Map.get extension.
NOTE:
The original answer was wrong and has been edited to:
point out the right/better answer
explain why the original answer was wrong
Thank you #jamesdlin for pointing this out.
Better answer
As pointed by #jamesdlin in his answer, the lint rule mentioned in the question has been requested in the flutter Github issues, and not in production yet.
Original Answer (wrong but kind of related to the question)
Why it is wrong:
The question was asking about the lint rule when using an index of Map. The answer however gave the lint rule about initializing a map using the wrong index (By the wrong index, I mean different data type).
Below is the answer:
There is a lint rule for this.
For example, if you define a Map like this ->
final Map<String, String> m = {
1: 'some random value',
};
It shows an error right away and this won't compile. This is the error ->
Error: A value of type 'int' can't be assigned to a variable of type 'String'.
1: 'error because index is of type String but assigned value is of type int',
^
Error: Compilation failed.
See the official docs where this lint rule, map_key_type_not_assignable is defined.
I have tested this in dartpad and vs code. Both IDEs show this error.
There could be some issues in your IDE configuration if you're not seeing this lint error.
As for your question, there is already a lint rule for this as explained above.

Is Map really a type in Flutter? Unfinished Map declarations break other Map declarations

I understand that in Flutter, I can declare a Map using a map constructor':
eg.
var map_name = new Map();
and then use it:
map_name[key] = value
or using Map literals:
var details = {'Username':'Fede','Password':'pass#123'};
However, I have seen perfectly valid code in Dart such as:
Map<String, int> phoneBook ={
'Fede': 12345678,
'Juli': 5467899,
'Pablo' : 56788654,
};
This kind of declaration can be accepted by the compiler in normal cases:
code accepted by compiler
but (after hours of debugging) I have seen that not finishing the declaration of one map in this way by not assigning a name for it, the compiler (in Android Studio) will yield an error telling that "Map isn't a type" in other valid declarations, even in other files calling that file where the Map declaration was not finished! That is, the error is quite spread.
crashed code
map isnt't a type
In other words, the unfinished declaration of one Map breaks the possibility to declare any other Maps in this way, anywhere linked to that unfinished sentence giving a 'map isn't a type' error. The problem dissappears when you just put a name to the unfinished Map declaration and Maps are treated as types again. So my question is: Are Maps a type for Flutter, or is it just a minor bug?
In your "crashed code" picture, i seen that you have not provide name for second map.Your error is kind of syntax, please provide correct declaration to make its work. I tried your code in my IDE and its working perfectly.The syntax you tried is totally valid and it should working.

Is there a way in Dart to mark a function as throwing an exception?

I was trying to find a way in Flutter/Dart to mark a function that may throw an exception during its execution. After some time searching in the documentation and Google I did not find any way of doing this.
In other language, for example Swift, Java, Kotlin, etc I know we have such mechanism.
Sample code in Swift is:
func doSomething() throws { ... }
Does anyone know if this exists in Dart?
I think it will be useful.
If it does not exist due to Dart language desing then maybe anyone can explain the reason behind this decision.
Thanks in advance!
There is no way in Dart to mark a function as potentially throwing.
All functions should be assumed to potentially throw (if for no other reason, then because of an out-of-memory or stack-overflow situation).
If you look at Swift, the throws is about exceptions, not errors. Dart does not distinguish the two, you can throw anything. Swift has put itself in a place between Java ("have to declare all thrown exceptions") and Dart or C# ("Can't declare exceptions").
Marking a function as "throwing" doesn't help the compiler in any way because it has to assume that all other functions might too. The Swift approach is there to ensure that distinctively marked exceptions are not ignored. Unless you want to, then you can try! them and turn the exception into an error.
If a function does throw as part of normal usage, you should document it in the function's documentation.
Dart also have the issue of function types. Is a function from int to int the same type as another function from int to int if the latter can throw? Separating function types into throwing and non-throwing get complicated quickly. Even more so if you want to specify what it throws. It's not impossible, but it's one more complication.
The one thing that you will get with the Dart null safety update (currently being worked on), is a way to state that a function always throws. If you make the return type Never in null-safe code, then the type system will prevent you from returning any value, and since a function call must end by either returning a value or throwing, a call to a function with return type Never can only end by throwing.

dart + flutter: why doesn't the compiler recognize type mismatches

I'm encapsulating the data of my flutter app inside a class called AppData. It looks a bit like this:
class AppData with ChangeNotifier{
List<word.Word> _words;
UnmodifiableListView<word.Word> words;
AppData(){
// at some point this will be loaded from disk
// for now I'm just using a hardcoded list of words
_words = word.words;
// the following code would work:
// words = UnmodifiableListView(_words);
// this doesn't, but it's also not a compiler error !?
words = _words;
}
// ...
}
The AppData class keeps track of a list of words. For the consumers of AppData, this is visible as an UnModifiableListView.
My code had a very obvious bug: I assigned _words to words without encapsulating the List in the UnModifiableListView properly.
Why is this not found by the compiler?
The types should be an obvious mismatch. From the dart docs (emphasis mine):
Dart’s type system, like the type systems in Java and C#, is sound. It
enforces that soundness using a combination of static checking
(compile-time errors) and runtime checks. For example, assigning a
String to int is a compile-time error. Casting an Object to a string
using as String fails with a runtime error if the object isn’t a
string.
Update, to respond to Rémi's answer:
The error message is:
The following assertion was thrown building MultiProvider:
type 'List<Word>' is not a subtype of type 'UnmodifiableListView<Word>'
This seems like an issue of Covariance vs Contravariance.
If I know that my reference to List actually contains an UnmodifiableListView, then I could do the cast myself.
Why would the compiler add an implicit cast for me?
It seems to me as if this bypasses a lot of the type soundness mentioned in the docs above. Especially when I change my type hierarchies and do extensive refactoring, I rely on the compiler to tell me: You've got the types mixed up.
It's very possible that their inheritance tree still reaches a common ancestor at some point. But they are definitely not the same.
At least to me, it is even more surprising, since this is not the way other "typical" OOP languages work (Java, C#, ...).
So I still wonder: Why doesn't the compiler find this, what is the reason behind this design?
What happens, in reality, is an implicit cast.
Since UnmodifiableListView is a List, assigning List to UnmodifiableListView does an automatic cast.
You can disable that on the analysis_options.yaml like so:
analyzer:
strong-mode:
implicit-casts: false

Why are null checks bad / why would I want an optional to succeed if it's null?

I've read more than a few answers to similar questions as well as a few tutorials, but none address my main confusion. I'm a native Java coder, but I've programmed in Swift as well.
Why would I ever want to use optionals instead of nulls?
I've read that it's so there are less null checks and errors, but these are necessary or easily avoided with clean programming.
I've also read it's so all references succeed (https://softwareengineering.stackexchange.com/a/309137/227611 and val length = text?.length). But I'd argue this is a bad thing or a misnomer. If I call the length function, I expect it to contain a length. If it doesn't, the code should deal with it right there, not continue on.
What am I missing?
Optionals provide clarity of type. An Int stores an actual value - always, whereas an Optional Int (i.e. Int?) stores either the value of an Int or a nil. This explicit "dual" type, so to speak, allows you to craft a simple function that can clearly declare what it will accept and return. If your function is to simply accept an actual Int and return an actual Int, then great.
func foo(x: Int) -> Int
But if your function wants to allow the return value to be nil, and the parameter to be nil, it must do so by explicitly making them optional:
func foo(x: Int?) -> Int?
In other languages such as Objective-C, objects can always be nil instead. Pointers in C++ can be nil, too. And so any object you receive in Obj-C or any pointer you receive in C++ ought to be checked for nil, just in case it's not what your code was expecting (a real object or pointer).
In Swift, the point is that you can declare object types that are non-optional, and thus whatever code you hand those objects to don't need to do any checks. They can just safely just use those objects and know they are non-null. That's part of the power of Swift optionals. And if you receive an optional, you must explicitly unpack it to its value when you need to access its value. Those who code in Swift try to always make their functions and properties non-optional whenever they can, unless they truly have a reason for making them optional.
The other beautiful thing about Swift optionals is all the built-in language constructs for dealing with optionals to make the code faster to write, cleaner to read, more compact... taking a lot of the hassle out of having to check and unpack an optional and the equivalent of that you'd have to do in other languages.
The nil-coalescing operator (??) is a great example, as are if-let and guard and many others.
In summary, optionals encourage and enforce more explicit type-checking in your code - type-checking that's done by by the compiler rather than at runtime. Sure you can write "clean" code in any language, but it's just a lot simpler and more automatic to do so in Swift, thanks in big part to its optionals (and its non-optionals too!).
Avoids error at compile time. So that you don't pass unintentionally nulls.
In Java, any variable can be null. So it becomes a ritual to check for null before using it. While in swift, only optional can be null. So you have to check only optional for a possible null value.
You don't always have to check an optional. You can work equally well on optionals without unwrapping them. Sending a method to optional with null value does not break the code.
There can be more but those are the ones that help a lot.
TL/DR: The null checks that you say can be avoided with clean programming can also be avoided in a much more rigorous way by the compiler. And the null checks that you say are necessary can be enforced in a much more rigorous way by the compiler. Optionals are the type construct that make that possible.
var length = text?.length
This is actually a good example of one way that optionals are useful. If text doesn't have a value, then it can't have a length either. In Objective-C, if text is nil, then any message you send it does nothing and returns 0. That fact was sometimes useful and it made it possible to skip a lot of nil checking, but it could also lead to subtle errors.
On the other hand, many other languages point out the fact that you've sent a message to a nil pointer by helpfully crashing immediately when that code executes. That makes it a little easier to pinpoint the problem during development, but run time errors aren't so great when they happen to users.
Swift takes a different approach: if text doesn't point to something that has a length, then there is no length. An optional isn't a pointer, it's a kind of type that either has a value or doesn't have a value. You might assume that the variable length is an Int, but it's actually an Int?, which is a completely different type.
If I call the length function, I expect it to contain a length. If it doesn't, the code should deal with it right there, not continue on.
If text is nil then there is no object to send the length message to, so length never even gets called and the result is nil. Sometimes that's fine — it makes sense that if there's no text, there can't be a length either. You may not care about that — if you were preparing to draw the characters in text, then the fact that there's no length won't bother you because there's nothing to draw anyway. The optional status of both text and length forces you to deal with the fact that those variables don't have values at the point where you need the values.
Let's look at a slightly more concrete version:
var text : String? = "foo"
var length : Int? = text?.count
Here, text has a value, so length also gets a value, but length is still an optional, so at some point in the future you'll have to check that a value exists before you use it.
var text : String? = nil
var length : Int? = text?.count
In the example above, text is nil, so length also gets nil. Again, you have to deal with the fact that both text and length might not have values before you try to use those values.
var text : String? = "foo"
var length : Int = text.count
Guess what happens here? The compiler says Oh no you don't! because text is an optional, which means that any value you get from it must also be optional. But the code specifies length as a non-optional Int. Having the compiler point out this mistake at compile time is so much nicer than having a user point it out much later.
var text : String? = "foo"
var length : Int = text!.count
Here, the ! tells the compiler that you think you know what you're doing. After all, you just assigned an actual value to text, so it's pretty safe to assume that text is not nil. You might write code like this because you want to allow for the fact that text might later become nil. Don't force-unwrap optionals if you don't know for certain, because...
var text : String? = nil
var length : Int = text!.count
...if text is nil, then you've betrayed the compiler's trust, and you deserve the run time error that you (and your users) get:
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Now, if text is not optional, then life is pretty simple:
var text : String = "foo"
var length : Int = text.count
In this case, you know that text and length are both safe to use without any checking because they cannot possibly be nil. You don't have to be careful to be "clean" -- you literally can't assign anything that's not a valid String to text, and every String has a count, so length will get a value.
Why would I ever want to use optionals instead of nulls?
Back in the old days of Objective-C, we used to manage memory manually. There was a small number of simple rules, and if you followed the rules rigorously, then Objective-C's retain counting system worked very well. But even the best of us would occasionally slip up, and sometimes complex situations arose in which it was hard to know exactly what to do. A huge portion of Objective-C questions on StackOverflow and other forums related to the memory management rules. Then Apple introduced ARC (automatic retain counting), in which the compiler took over responsibility for retaining and releasing objects, and memory management became much simpler. I'll bet fewer than 1% of Objective-C and Swift questions here on SO relate to memory management now.
Optionals are like that: they shift responsibility for keeping track of whether a variable has, doesn't have, or can't possibly not have a value from the programmer to the compiler.