Dart at a Glance
Learning Objectives
- You know the basic structure of a Dart program.
- You know of compiled and interpreted languages, and hybrid execution models including AOT + JIT that Dart uses.
- You know the difference between statically and dynamically typed languages, and how Dart uses type inference.
- You know the term object-oriented programming, and you know of null safety.
- You know of program execution using call stack and heap, and you know of memory management and garbage collection.
Hello Dart
Syntax-wise, Dart is similar to other high-level programming languages such as Java, C#, and Kotlin. A “Hello world!” program in Dart looks as follows — you can run the program by pressing the play icon (►) in the upper right corner of the code editor.
The above outlines the basic structure of a Dart program. The main
function is the entry point to the program, and the print
function is used to output text to the console. The void
keyword indicates that the function does not return a value.
Dart uses curly braces {}
to define blocks of code, and statements are terminated with a semicolon ;
. Unlike e.g. with Python, indentation does not influence the program’s behavior and code is intended just to make it more readable for humans. Similarly, unlike e.g. with Python and Scala, semicolons are not optional — they are required to terminate statements.
A single line can have multiple statements, but it is generally considered good practice to have one statement per line to improve readability.
Dart comes with the basic data types you would expect from a modern programming language, such as integers, floating-point numbers, strings, and booleans. The following example showcases the basic data types in Dart.
The above also shows how string interpolation works in Dart. String interpolation allows you to embed expressions within strings by using the dollar sign $
followed by the expression. Dart evaluates the expression and replaces it with the result when the string is printed.
Compiled and interpreted languages
Programming languages are typically divided into two main categories based on how they are executed: compiled languages and interpreted languages. In compiled languages, the source code is translated into machine code before execution, allowing the resulting binary to run directly on the target hardware. In interpreted languages, the code is translated into machine-readable instructions during execution.
There exists also just-in-time (JIT) compilation, where code is compiled at runtime. JIT compilation can provide performance benefits by optimizing code based on runtime information. As an example, while Java compiles programs to bytecode that is executed by the Java Virtual Machine (JVM), the JVM uses JIT compilation to optimize code for execution on the target hardware at runtime.
Many modern languages also employ a hybrid execution model, combining elements of both compiled and interpreted languages, often using a combination of ahead-of-time (AOT) and JIT compilation techniques. This approach can provide the benefits of both compiled and interpreted languages.
Dart, for example, uses AOT compilation to compile code to native machine code for performance when creating production builds, and JIT compilation for hot reloading and optimization during development.
Tooling, such as integrated development environments (IDEs) and plugins, can help catch errors in code regardless of whether the language is compiled or interpreted. For example, static analyzers and linters can identify many potential issues in interpreted languages before execution.
Compiling code can take time, particularly on older hardware. In the early 2000s, when computers were slower, lengthy compilation processes were a common frustration among developers. This challenge is humorously illustrated in Figure 1.
Program execution and memory management
Programs are executed using a call stack that keeps track of functions that are currently being executed. The call stack is a LIFO (Last In, First Out) data structure that consists of stack frames. Each stack frame represents a function call, containing information such as the function’s return address, parameters, and local variables.
Whenever a function is called, a new stack frame is pushed onto the call stack, and the program execution jumps to the function’s entry point. Then, the function’s body is executed, and when the function returns, the stack frame is popped off the call stack. Variables that are introduced within the function are stored in the stack frame, which means that they are only accessible within the function — this is known as the scope of the variable. A function can also return a value, which is implemented by passing the value back to the calling function.
In addition to the call stack, programs also use the heap to store objects and data that are not associated with a specific function. The heap is a region of memory that is shared within the program, which is used to store objects that have a longer lifetime than local variables. Objects with a longer lifetime contain e.g. dynamic data structures such as lists and maps, as well as objects created by the developer. References to the objects in the heap are stored in the stack frame.
When a program is executed, the operating system allocates memory for the program, including the call stack and the heap. The operating system provides functions that can be used to reserve memory in the programming language, which then may provide functions to allocate and deallocate memory for the developer.
In languages with automatic memory management, such as Dart, the programming language is responsible for managing memory allocation and deallocation, while for languages without automatic memory management, such as C and C++, the developer is responsible for managing memory. Automatic memory management includes also garbage collection, which is the process of removing unused objects from memory --- improper memory handling can lead to e.g. use-after-free where memory is still used after it has been released.
Typing and type inference
Languages can also be categorized by how they handle variable types: statically typed and dynamically typed languages. In statically typed languages, variable types are known at compile time, enabling the compiler to catch type errors early. In dynamically typed languages, variable types are determined at runtime, offering greater flexibility but less type safety.
Dart is a statically typed language. For example, the Dart program below defines a program that sums two integers and prints the result.
If you try to assign a value of a different type to a variable, the compiler will produce an error. For example, the following code snippet would produce an error because the variable b
is of type int
, while "2"
is a string.
It is also possible to declare variables without explicitly stating their type, which allows the compiler to infer the type based on the value assigned to the variable. This feature is known as type inference. In Dart, the var
keyword can be used to declare variables with inferred type.
Expressions and Values
Let’s scrutinize what was actually meant by
infer the type based on the value
Here, the word value is used misleadingly as what is actually meant is expression.
Expressions are regions of code, which produce a value after evaluation — e-value-ation…
For example, 1 + 1
is an expression, but not a value, and int sum = a + b;
is not an expression, but is a statement.
123
and "2"
are both expressions and values.
Dart has a static type system which means that type checking does not evaluate expressions, but looks at the form and parts of the expression and their types during type inference.
The confusion arises from looking at the sample statement int sum = a + b;
, where when we say that “a value is assigned to a variable” one might think that by value we mean a + b
.
Variables do indeed store values and not expressions (in most programming languages, like Dart).
What is actually meant is “the value of evaluating the right-hand-side expression is assigned to a variable”.
See this StackOverflow question or this Wikipedia article for more reading.
For example, in the following code snippet, we declare a variable message
, assign a string value to it, and finally print the value.
The runtime type of a variable, i.e. the type of the value that the variable holds, can be accessed using the runtimeType
property of the variable. The following code snippet has a program that prints the runtime type of a variable.
As Dart is a statically typed language, once the type of a variable has been defined, either through type inference or explicit type annotations, the variable cannot be assigned a value of a different type. As an example, the following code snippet would produce an error because the variable message
is inferred to be of type String
and cannot be assigned an integer value.
This is in contrast to languages like Python and JavaScript, which are dynamically typed and allow variables to change type at runtime.
Like many programming languages, Dart also supports defining final
variables, which are variables that can only be assigned a value once. The following code snippet demonstrates misusing a final
variable by trying to reassign a value to it.
Everything is an object
Dart is an object-oriented programming language. In object-oriented programming, objects are instances of classes that encapsulate data and behavior. Dart has a unified object model, which means that everything is an object. Even simple data types like numbers and booleans are objects — this means that you can call methods on numbers and booleans, just like you can with more complex objects — the objects also have common properties like runtimeType
that we used earlier.
For example, the following code snippet demonstrates how you can call the abs
method on an integer to get its absolute value.
As everything is an object, Dart has a rich set of built-in classes and libraries that provide functionality for common tasks. For example, Dart has classes for working with collections, dates, and times, as well as classes for performing input and output operations. By using these classes and libraries, you can write code that is more concise and easier to read.
There are potential risks to having everything as an object, including reference to null and the potential for null reference errors. However, Dart has a feature called null safety that helps prevent null reference errors by ensuring that variables are non-nullable by default.
Null safety
Many programming languages allow variables to be assigned a null
value, which represents the absence of a value. However, using null can lead to runtime errors if not handled properly. Even the inventor of the null reference, Tony Hoare, has expressed regret over creating null:
“My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”
Null safety is a feature in programming languages designed to eliminate null reference errors. In Dart, null safety ensures that variables are non-nullable by default, which means they cannot contain a null
value unless the programmer explicitly declares a variable nullable. That is, the following program will not compile.
To explicitly declare a variable as nullable, the ?
operator is used after type declaration. The following code snippet demonstrates how to declare a nullable variable.
A nullable variable can be assigned a null
value, but the compiler will produce an error if you try to access a property or method on a nullable variable without first checking if it is null
. As an example, in the following program, we try to access the length
property of a nullable variable without checking if it is null
.
To use a nullable variable safely, you can use the null-aware operator ?.
, which only calls a method or accesses a property if the variable is not null
. The following code snippet demonstrates how to use the null-aware operator. The program prints the value of the length
property if the variable name is not null
, and null
otherwise.
Assigning the value of a nullable variable to a non-nullable variable requires checking if the nullable variable is null
. The null-aware operator ??
can be used to provide a default value if the variable is null
. The following code snippet demonstrates how to assign the value of a nullable variable to a non-nullable variable.
When you run the program, you see a warning and the output Dart
.