In 2013, Dart’s official 1.0 release generated some buzz—as most Google products do—but not everyone was immediately sold on as Google’s internal teams. However, Google’s significant revamp with Dart 2 five years later demonstrated their commitment to the language. Today, Dart continues to gain popularity, particularly among experienced Java and C# developers.
Several factors contribute to the Dart programming language’s significance:
- It offers the best of both worlds: Dart functions as both a compiled, type-safe language (similar to C# and Java) and a scripting language (like Python and JavaScript).
- It can be transpiled to JavaScript for web front-end development.
- Dart runs on various platforms and compiles to native mobile apps, making it a versatile choice.
- Its syntax resembles C# and Java, facilitating a quick learning curve.
Those of us accustomed to larger enterprise systems built with C# or Java understand the importance of type safety, compile-time error detection, and linters. The prospect of adopting a “scripty” language often raises concerns about sacrificing the structure, speed, accuracy, and debugging capabilities we rely on.
However, Dart development doesn’t require such sacrifices. We can build mobile apps, web clients, and back-end systems using the same language—all while retaining the aspects we appreciate in Java and C#!
To illustrate this, let’s explore some key Dart language examples that would be unfamiliar to C# or Java developers. We’ll summarize these examples in a Dart language PDF at the end.
Note: This article focuses solely on Dart 2.x. Version 1.x was not as mature; notably, its type system was advisory (similar to TypeScript) rather than mandatory (like C# or Java).
1. Code Organization
Let’s begin by examining one of the most notable differences: how code files are structured and referenced.
Source Files, Scope, Namespaces, and Imports
In C#, a collection of classes compiles into an assembly. Each class belongs to a namespace, often mirroring the source code’s file system organization. Ultimately, the assembly doesn’t retain information about the source code file locations.
In Java, source files reside within a package, and namespaces typically align with the file system structure. However, a package is essentially a collection of classes.
Both languages provide a way to maintain source code independence from the file system.
In contrast, Dart requires each source file to import all referenced elements, including other source files and third-party packages. Namespaces function differently, and file references often rely on file system paths. Variables and functions can exist at the top level, not just within classes. In these aspects, Dart exhibits a more script-like behavior.
Therefore, you’ll need to shift your perspective from “a collection of classes” to “a sequence of included code files.”
Dart supports both package-based and ad-hoc code organization. Let’s illustrate the sequence of included files using an example without packages:
| |
Each element referenced within a source file must be declared or imported within that file, as there’s no project-level scope or alternative method to include other source elements.
Dart’s use of namespaces is limited to assigning names to imports, influencing how you reference the imported code within that file.
| |
Packages
The previous examples demonstrate code organization without packages. When using packages, code follows a more specific structure. Here’s an example package layout for a package named apples:
apples/pubspec.yaml—defines the package’s name, dependencies, and other configurationslib/apples.dart—handles imports and exports; this is the file imported by package consumerssrc/seeds.dart—contains all other code
bin/runapples.dart—houses the main function, serving as the entry point (if the package is runnable or includes runnable tools)
With this structure, you can import entire packages instead of individual files:
| |
Non-trivial applications should always be structured as packages. This approach reduces the need to repeat file system paths in each referring file, improves performance, and simplifies package sharing on pub.dev. Other developers can easily access and utilize your shared package. Packages used by your app are copied to your file system, enabling you to debug deep into their code.
2. Data Types
Dart’s type system presents significant differences related to nulls, numeric types, collections, and dynamic types.
Nulls Everywhere
Coming from C# or Java, we’re accustomed to distinguishing between primitive or value types and reference or object types. Value types typically reside on the stack or in registers, and copies of the value are passed as function arguments. In contrast, reference types are allocated on the heap, and only pointers to the object are passed as function arguments. Given that value types always occupy memory, a value-typed variable cannot be null, and all its members must have initialized values.
Dart eliminates this distinction by treating everything as an object, with all types ultimately inheriting from the Object type. Consequently, the following code is valid:
| |
In fact, all primitives are implicitly initialized to null. Unlike C# or Java, you cannot assume default integer values to be zero, potentially necessitating null checks.
Interestingly, even Null is a type, and the word null represents an instance of Null:
| |
Not As Many Numeric Types
Unlike the diverse range of integer types in C# and Java (from 8 to 64 bits, with signed and unsigned variations), Dart primarily utilizes the int type, a 64-bit value. (For exceptionally large numbers, there’s the BigInt type.)
Since Dart lacks a built-in byte array type, binary file contents are processed as lists of integers, represented as List<Int>.
While this might seem inefficient, the language designers addressed this concern. The runtime employs different internal representations based on the actual integer value used, optimizing memory usage. If possible, the runtime avoids allocating heap memory for the int object and utilizes a CPU register in unboxed mode. Additionally, the byte_data library provides UInt8List and other optimized representations.
Collections
Collections and generics in Dart closely resemble their counterparts in C# and Java. However, one notable difference is the absence of fixed-size arrays. You can simply use the List data type wherever you would typically use an array.
Dart also offers syntactic sugar for initializing three collection types:
| |
Therefore, use Dart’s List where you’d use a Java array, ArrayList, or Vector; or a C# array or List. Similarly, use Set for Java/C# HashSet and Map for Java HashMap or C# Dictionary.
3. Dynamic and Static Typing
Dynamic languages like JavaScript, Ruby, and Python allow referencing members even if they don’t exist. Here’s a JavaScript example:
| |
Executing this code will result in person.age being undefined, but the code still runs.
Similarly, JavaScript allows changing a variable’s type:
| |
In contrast, Java enforces static typing, preventing code like the examples above. The compiler requires type information and verifies the legality of all operations, even when using the var keyword:
| |
Java exclusively supports static typing. (While introspection enables some dynamic behavior, it’s not directly part of the syntax.) Purely dynamic languages like JavaScript only permit dynamic typing.
Dart, on the other hand, accommodates both:
| |
Dart introduces the dynamic pseudo-type, which defers all type checking to runtime. The attempt to call a.foo() won’t raise flags during static analysis, but the code will fail at runtime due to the missing method.
C# initially mirrored Java’s static typing but later incorporated dynamic support, making Dart and C# comparable in this regard.
4. Functions
Function Declaration Syntax
Dart’s function syntax is more concise and flexible than C# or Java. Here are the various ways to declare a function:
| |
For instance:
| |
Parameter Passing
With everything being an object in Dart (including primitives like int and String), parameter passing might seem confusing. While there’s no ref parameter passing like in C#, all parameters are passed by reference. However, the function cannot modify the caller’s reference. Objects aren’t cloned when passed to functions, so a function can modify the object’s properties. However, this distinction is largely irrelevant for immutable types like int and String.
| |
Optional Parameters
C# and Java developers often encounter confusing situations with overloaded methods:
| |
C#’s optional parameters introduce another potential point of confusion:
| |
C# doesn’t enforce naming optional arguments at call sites, making refactoring methods with optional parameters potentially risky. If some call sites remain valid after refactoring, the compiler won’t detect any issues.
Dart provides a safer and more versatile approach. Firstly, it doesn’t support overloaded methods. Instead, optional parameters can be handled in two ways:
| |
It’s important to note that you cannot combine both styles within the same function declaration.
async Keyword Position
C#’s placement of the async keyword can be confusing:
| |
This syntax suggests an asynchronous function signature, but only the implementation is actually asynchronous. Both signatures below would be valid implementations of the interface:
| |
Dart places the async keyword more logically, indicating an asynchronous implementation:
| |
Scope and Closures
Like C# and Java, Dart adheres to lexical scoping. Variables declared within a block lose their scope outside that block. Consequently, Dart handles closures in a familiar manner.
Property syntax
While Java popularized the property get/set pattern, it lacks dedicated syntax for it:
| |
C# provides syntax for properties:
| |
Dart offers a slightly different syntax for working with properties:
| |
5. Constructors
Dart constructors are more flexible than their C# and Java counterparts. One notable feature is the ability to define multiple constructors with different names within the same class:
| |
You can invoke the default constructor using just the class name: var c = Client();
Dart offers two shorthand methods for initializing instance members before the constructor body executes:
| |
Constructors can call superclass constructors and redirect to other constructors within the same class:
| |
In Java and C#, constructors calling other constructors within the same class can lead to confusion when both have implementations. Dart addresses this by mandating that redirecting constructors cannot have bodies, promoting clearer constructor layering.
The factory keyword enables using a function like a constructor, but the implementation remains a regular function. This is useful for returning cached instances or instances of derived types:
| |
6. Modifiers
Java and C# utilize access modifiers like private, protected, and public. Dart simplifies this significantly. Members whose names start with an underscore are accessible within the package (including from other classes) but hidden from external callers. Otherwise, members are publicly visible. There are no explicit keywords like private for visibility control.
The final and const keywords control changeability but have distinct meanings:
| |
7. Class Hierarchy
Dart supports interfaces, classes, and a form of multiple inheritance. However, there’s no dedicated interface keyword. Instead, all classes implicitly function as interfaces. You can define an abstract class and then implement it:
| |
Multiple inheritance involves a primary lineage using the extends keyword and additional classes using the with keyword:
| |
In this declaration, the Employee class inherits from both Person and Salaried, but Person serves as the main superclass, while Salaried acts as a mixin (secondary superclass).
8. Operators
Dart offers some useful and interesting operators not found in C# or Java.
Cascades enable a chaining pattern on any object:
| |
The spread operator allows treating a collection as a list of its elements during initialization:
| |
9. Threads
Dart lacks traditional threads, a design choice facilitating its transpilation to JavaScript. Instead, it employs “isolates,” which resemble separate processes in that they don’t share memory. This inherent safety, stemming from the absence of multi-threaded programming, is considered one of Dart’s advantages. To communicate between isolates, you need to establish data streams between them, with received objects copied into the receiving isolate’s memory space.
Develop with the Dart Language: You Can Do This!
As a C# or Java developer, your existing knowledge will accelerate your Dart learning journey, as the language was designed for familiarity. To further aid your transition, we’ve compiled a Dart cheat sheet PDF highlighting key differences from C# and Java equivalents:
By combining the insights from this article with your current expertise, you’ll be well-equipped to become productive with Dart within your first couple of days. Happy coding!
