The Dart Language: A More Powerful Alternative to Java and C#

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// file1.dart
int alice = 1; // top level variable
int barry() => 2; // top level function
var student = Charlie(); // top level variable; Charlie is declared below but that's OK
class Charlie { ... } // top level class
// alice = 2; // top level statement not allowed

// file2.dart
import 'file1.dart'; // causes all of file1 to be in scope
main() {
    print(alice); // 1
}

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.

1
2
3
4
5
// file2.dart
import 'file1.dart' as wonderland; 
main() {
    print(wonderland.alice); // 1
}

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 configurations
    • lib/
      • apples.dart—handles imports and exports; this is the file imported by package consumers
      • src/
        • 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:

1
import 'package:apples';

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:

1
int i = null;

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:

1
print(null.runtimeType); // prints 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:

1
2
3
final a = [1, 2, 3]; // inferred type is List<int>, an array-like ordered collection
final b = {1, 2, 3}; // inferred type is Set<int>, an unordered collection
final c = {'a': 1, 'b': 2}; // inferred type is Map<string, int>, an unordered collection of name-value pairs

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:

1
2
3
4
5
var person = {}; // create an empty object
person.name = 'alice'; // add a member to the object
if (person.age < 21) { // refer to a property that is not in the object
  // ...
}

Executing this code will result in person.age being undefined, but the code still runs.

Similarly, JavaScript allows changing a variable’s type:

1
2
var a = 1; // a is a number
a = 'one'; // a is now a string

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:

1
2
var b = 1; // a is an int
// b = "one"; // not allowed in Java

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:

1
2
3
4
5
6
// dart
dynamic a = 1; // a is an int - dynamic typing
a = 'one'; // a is now a string
a.foo(); // we can call a function on a dynamic object, to be resolved at run time
var b = 1; // b is an int - static typing
// b = 'one'; // not allowed in Dart

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:

1
2
3
4
5
6
7
// functions as declarations
return-type name (parameters) {body}
return-type name (parameters) => expression;

// function expressions (assignable to variables, etc.)
(parameters) {body}
(parameters) => expression

For instance:

1
2
3
4
5
void printFoo() { print('foo'); };
String embellish(String s) => s.toUpperCase() + '!!';

var printFoo = () { print('foo'); };
var embellish = (String s) => s.toUpperCase() + '!!';

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var id = 1;
var name = 'alice';
var client = Client();

void foo(int id, String name, Client client) {
	id = 2; // local var points to different int instance
	name = 'bob'; // local var points to different String instance
	client.State = 'AK'; // property of caller's object is changed
}

foo(id, name, client);
// id == 1, name == 'alice', client.State == 'AK'

Optional Parameters

C# and Java developers often encounter confusing situations with overloaded methods:

1
2
3
4
5
6
// java
void foo(string arg1) {...}
void foo(int arg1, string arg2) {...}
void foo(string arg1, Client arg2) {...}
// call site:
foo(clientId, input3); // confusing! too easy to misread which overload it is calling

C#’s optional parameters introduce another potential point of confusion:

1
2
3
4
5
6
7
// c#
void Foo(string arg1, int arg2 = 0) {...}
void Foo(string arg1, int arg3 = 0, int arg2 = 0) {...}
 
// call site:
Foo("alice", 7); // legal but confusing! too easy to misread which overload it is calling and which parameter binds to argument 7
Foo("alice", arg2: 9); // better

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// positional optional parameters
void foo(string arg1, [int arg2 = 0, int arg3 = 0]) {...}

// call site for positional optional parameters
foo('alice'); // legal
foo('alice', 12); // legal
foo('alice', 12, 13); // legal

// named optional parameters
void bar(string arg1, {int arg2 = 0, int arg3 = 0}) {...}
bar('alice'); // legal
bar('alice', arg3: 12); // legal
bar('alice', arg3: 12, arg2: 13); // legal; sequence can vary and names are required

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:

1
2
Task<int> Foo() {...}
async Task<int> Foo() {...}

This syntax suggests an asynchronous function signature, but only the implementation is actually asynchronous. Both signatures below would be valid implementations of the interface:

1
2
3
interface ICanFoo {
    Task<int> Foo();
}

Dart places the async keyword more logically, indicating an asynchronous implementation:

1
Future<int> foo() async {...} 

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:

1
2
3
4
// java
private String clientName;
public String getClientName() { return clientName; }
public void setClientName(String value}{ clientName = value; }

C# provides syntax for properties:

1
2
3
4
5
6
// c#
private string clientName;
public string ClientName {
    get { return clientName; }
    set { clientName = value; }
}

Dart offers a slightly different syntax for working with properties:

1
2
3
4
// dart
string _clientName;
string get ClientName => _clientName;
string set ClientName(string s) { _clientName = s; }

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:

1
2
3
4
class Point {
    Point(double x, double y) {...} // default ctor
    Point.asPolar(double angle, double r) {...} // named ctor
}

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:

1
2
3
4
5
6
7
8
class Client {
    String _code;
    String _name;
    Client(String this._name) // "this" shorthand for assigning parameter to instance member
        : _code = _name.toUpper() { // special out-of-body place for initializing
        // body
    }
}

Constructors can call superclass constructors and redirect to other constructors within the same class:

1
2
3
Foo.constructor1(int x) : this(x); // redirect to the default ctor in same class; no body allowed
Foo.constructor2(int x) : super.plain(x) {...} // call base class named ctor, then run this body
Foo.constructor3(int x) : _b = x + 1 : super.plain(x) {...} // initialize _b, then call base class ctor, then run this body

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:

1
2
3
4
5
6
7
8
class Shape {
    factory Shape(int nsides) {
        if (nsides == 4) return Square();
        // etc.
    }
} 

var s = Shape(4); 

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:

1
2
3
4
var a = 1; // a is variable, and can be reassigned later
final b = a + 1; // b is a runtime constant, and can only be assigned once
const c = 3; // c is a compile-time constant
// const d = a + 2; // not allowed because a+2 cannot be resolved at compile time

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:

1
2
3
4
5
6
abstract class HasDesk {
    bool isDeskMessy(); // no implementation here
}
class Employee implements HasDesk {
    bool isDeskMessy() { ...} // must be implemented here
}

Multiple inheritance involves a primary lineage using the extends keyword and additional classes using the with keyword:

1
class Employee extends Person with Salaried implements HasDesk {...}

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:

1
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();

The spread operator allows treating a collection as a list of its elements during initialization:

1
2
var smallList = [1, 2];
var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]

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:

Dart language cheat sheet PDF

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!

Licensed under CC BY-NC-SA 4.0