Static, final, const, dynamic, var - differences and similarities

Explaining the different Dart keywords

Static, final, const, dynamic, var - differences and similarities
No time to read?
tl;dr
  • static makes a member (variable or method) available through the class instead of instances (object) of the class.
  • final modifies a variable, making it single-assignment variable. Meaning that the compiler expects exactly one assignment (no more, no less) to that variable.
  • const is a way to make a variable a compile-time constant. This means that the assigned value can neither change nor is it mutable.
  • var lets you define variables with a variable type. Variables being initiated with this keyword derive the type from the first assignment. After that an assignment with a value of a different type is not possible anymore.
  • dynamic is the most generic type. If you cast a variable to dynamic, the compiler will skip checking if the called method exists. Variables being initialized with dynamic can change their type though assignments indefinitely.

Dart is a pretty verbose, statically typed language. It offers a rich variety of keywords to let you as a developer express precisely what you want.

Especially when it comes to variable assignments, there are multiple keywords that have a similar but different meaning. Let’s look at these keywords and examine what they do exactly.

The static keyword

The static keyword can turn variables into class variables and methods into class methods. What that means is that the variable or method belongs to the class rather than an instance of the class.

In other words: static is only used for class-level variables and methods. It determines those which can be accessed without creating an object, just by referring to the class.

You can use it like this:

1class MyClass {
2  static int myInt = 1337;
3}
4
5int myNewInt = MyClass.myInt + 3;

Unlike Java, you can not use static to define a static class or a static block. Also, it’s not possible to use static outside a class context. Let’s look at what happens if we try to use it outside a class context nonetheless.

1void main() {
2  static int myNumber = 6;
3}

When we try to compile the above piece of code, the compiler responds with this:

Error: Can't have modifier 'static' here.
  static int bla = 6;
  ^^^^^^
Warning

Be aware that static variables and methods violate the object-oriented paradigm. To be more specific, it goes against data being encapsulated in objects.

Static methods can not be inherited nor overridden.

Also, static variables reside in the memory as long as the program runs, whereas instance variables’ memory is freed once the object is garbage collected.

You should also avoid defining a class that contains only static members. You can use top-level functions instead.

Use cases

Although from a strict OOP perspective, using static might often be a bad idea, there are cases, in which you will barely get around using it.

One of those cases is the usage of isolates.

Isolates are a way to implement actual concurrency in Dart. Everything inside Dart’s VM runs in an isolate. Every isolate has a separate event loop and an isolated memory heap. If you have heavy computational code that would otherwise put a big load on the main thread, you can use isolates to prevent this from happening. The function passed to the spawn() function, which creates a new isolate, needs to be a top-level function (meaning that it’s outside a class) or a static method. If you want the function to be part of a class, you won’t get around making it static.

The final keyword

final is a keyword that has the semantic meaning of a single assignment. It has to be assigned exactly once (if it’s used).

If we define a final variable, which we do not assign, the compiler will complain if there’s a usage (in the following example a print() statement):

1final int myNumber;
2  
3print(myNumber);

The consequence is a compiler error:

Error: Final variable 'myNumber' must be assigned before it can be used.
  print(myNumber);

If we define a final variable, which we assign more than once, the compiler will complain as well:

1final int myNumber = 7;
2  
3myNumber = 3;

This code block leads to the following compiler error:

Error: Can't assign to the final variable 'myNumber'.
  myNumber = 3;
  ^^^^^^^^
Error: Compilation failed.

It doesn’t matter if the assignment happens immediately during initialization, so this would have the same effect:

1final int myNumber;
2
3myNumber = 7;
4myNumber = 3;

The idea behind final variables is that the compiler prevents you from situations of an accidental value change of a variable although you already you know the value is not supposed to change.

It ’s a good practice to make every variable final at first. When you realize, you need to change its value during runtime, you can remove the keywords afterwards. It’s something the official linter would also complain about.

Notice
Keep in mind that while changing the type or value of a variable is not possible after the first assignment when using final. However, this does not make the object immutable!

Let’s consider the following example:

1final myNumbers = [1,2,3,4];
2
3myNumbers.add(5);
4
5myNumbers = 'a different type';

If you don’t specify a type like in the above code example, the compiler makes the variable the type of the first assignment (List<int> in this case). While the execution of the add() method on the List is perfectly fine because the value of myNumbers is not immutable, the assignment in line 5 is problematic as final prevents further assignments after the initialization. The execution of the code results in the following compiler error:

Error: Can't assign to the final variable 'myNumbers'.
  myNumbers = 'a different type';
  ^^^^^^^^^
Error: Compilation failed.

The const keyword

With const, you can tell the compiler, that the variable value you’re about to create, is supposed to be immutable and thus can be generated at compile-time.

Let’s look at an example to make things clearer:

1const List<int> myNumbers = <int>[1,2,3,4];
2
3myNumbers.add(5);

The execution of the above code results in an error:

Unhandled Exception: Unsupported operation: Cannot add to an unmodifiable list

It’s important to notice that this error comes up during runtime and not compile-time, which is a little bit confusing because it’s foreseeable at compile-time that this will fail. This issue is discussed on GitHub as well

Under the hood, the const keyword leads to the List becoming an UnmodifiableListMixin (dart-sdk/lib/internal/list.dart). In this abstract class, which implements List, every modifying method is overridden and throws an UnsupportedError.

Here is an example of the overridden add() method:

1abstract class UnmodifiableListMixin<E> implements List<E> {
2  
3  /** This operation is not supported by an unmodifiable list. */
4  void add(E value) {
5    throw new UnsupportedError("Cannot add to an unmodifiable list");
6  }
7  
8}

In the case of nested state like lists that contain lists or maps, the nested level is also immutable.

1const myList = [[1], [1,2]];
2myList[0].add(2);

Modifying nested values of a const variable value leads to the same error:

Unhandled Exception: Unsupported operation: Cannot add to an unmodifiable list

This is expected as the whole state of the variable value is being generated during compile-time.

The official documentation states:

"An unmodifiable list cannot have its length or elements changed. If the elements are themselves immutable, then the resulting list is also immutable."

The var and dynamic keyword

Although Dart is a statically typed language, we have the possibility to define a variable with a dynamic type. The keyword used for this is called var. If you know Javascript, you might know this keyword.

Defining a variable using the var keyword, causes two effects:

  • The variable has an initially undefined type (dynamic)
  • Once a value is assigned to the variable, it gets the type of the value, which can not be changed afterwards
1var myVar = 7;
2myVar = 'seven';

This leads to the following error during runtime.

Error: A value of type 'String' can't be assigned to a variable of type 'int'.
myVar = 'seven';
        ^
Error: Compilation failed.

By casting objects to dynamic, you can call every method without the compiler complaining. This is not the case for var as var is not a type.

1(myObject as dynamic).whatever(); // This is a valid cast and the compiler will not complain
2(myObject as var).whatever(); // This is invalid as var is not a type

If you use dynamic as the type during variable initialization instead of var, the behavior is similar but different. The key difference is that a variable that is initialized using dynamic can still change its type after its first assignment whereas var forbids this.

1dynamic myNumber = 1;   // myNumber is of type int.
2myNumber = 2;           // The assignment changes the value of myNumber from 1 to 2.
3myNumber = 'a';         // The assignment changes the value oof myNumber from 2 to 'a'.
4
5var myNumber = 1;       // myNumber is of type int.
6myNumber = 2;           // The assignment changes the value of myNumber from 1 to 2.
7myNumber = 'a';         // Error: A value of type 'String' can't be assigned to a variable of type 'int'.

Conclusion

In order to master a programming language, the developer needs to know the exact meaning of the main keywords and when it’s time to use them.

Because Dart has a rather sophisticated type system, there are quite a few keywords that revolve around this topic.

I recommend to always use static typing. This way you can avoid using var and dynamic and all the issues associated with these keywords completely.

final should be used by default for every newly defined variable as it protects the developer from unwanted reassignments of variables.

const should be used when the value is known during compile-time and never changes. Also a good choice for widgets with a static content.

Comment this 🤌

You are replying to 's commentRemove reference