Back-end13 minute read

The Dart Language: When Java and C# Aren't Sharp Enough

Five years after Dart 1.0, Google’s rewritten open-source language is attracting increasing numbers of developers. What do C# and Java developers need to know to get started with it?


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Five years after Dart 1.0, Google’s rewritten open-source language is attracting increasing numbers of developers. What do C# and Java developers need to know to get started with it?


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Star Ford
Verified Expert in Engineering
6 Years of Experience

Star is an architect and developer with specializations in business processes, requirements writing, databases, C#, and web development.

Read More

Expertise

Share

Way back in 2013, Dart’s official 1.0 release got some press—as with most Google offerings—but not everyone was as eager as Google’s internal teams to create business-critical apps with the Dart language. With its well-thought-out rebuild of Dart 2 five years later, Google seemed to have proven its commitment to the language. Indeed, today it continues to gain traction among developers—especially Java and C# veterans.

The Dart programming language is important for a few reasons:

  • It has the best of both worlds: It’s a compiled, type-safe language (like C# and Java) and a scripting language (like Python and JavaScript) at the same time.
  • It transpiles to JavaScript for use as a web front end.
  • It runs on everything, and compiles to native mobile apps, so you can use it for nearly anything.
  • Dart is similar to C# and Java in syntax, so it’s quick to learn.

Those of us from the C# or Java world of larger enterprise systems already know why type safety, compile-time errors, and linters are important. Many of us are hesitant to adopt a “scripty” language for fear of losing all the structure, speed, accuracy, and debugability that we are used to.

But with Dart development, we don’t have to give up any of that. We can write a mobile app, web client, and back end in the same language—and get all the things we still love about Java and C#!

To that end, let’s walk through some key Dart language examples that would be new to a C# or Java developer, which we’ll sum up in a Dart language PDF at the end.

Note: This article only covers Dart 2.x. Version 1.x wasn’t “fully cooked” - in particular, the type system was advisory (like TypeScript) instead of required (like C# or Java).

1. Code Organization

First, we will get into one of the most significant differences: how code files are organized and referenced.

Source Files, Scope, Namespaces, and Imports

In C#, a collection of classes is compiled to an assembly. Each class has a namespace, and often namespaces reflect the organization of source code in the file system—but in the end, the assembly does not retain any information about the source code file location.

In Java, source files are part of a package and the namespaces usually conform to the file system location, but in the end, a package is just a collection of classes.

So both languages have a way to keep the source code somewhat independent of the file system.

By contrast, in the Dart language, each source file must import everything that it refers to, including your other source files and third-party packages. There are no namespaces in the same way, and you often refer to files via their file system location. Variables and functions can be top-level, not just classes. In these ways, Dart is more script-like.

So you will need to change your thinking from “a collection of classes” to something more like “a sequence of included code files.”

Dart supports both package organization and ad-hoc organization without packages. Let’s start with an example without packages to illustrate the sequence of included files:

// 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
}

Everything you refer to in a source file has to be declared or imported within that file, as there is no “project” level and no other way to include other source elements in the scope.

The only use of namespaces in Dart is to give imports a name, and that affects how you refer to the imported code from that file.

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

Packages

The examples above organize code without packages. In order to use packages, code gets organized in a more specific way. Here’s an example package layout for a package named apples:

  • apples/
    • pubspec.yaml—defines the package name, dependencies, and some other things
    • lib/
      • apples.dart—imports and exports; this is the file imported by any consumers of the package
      • src/
        • seeds.dart—all other code here
    • bin/
      • runapples.dart—contains the main function, which is the entry point (if this is a runnable package or includes runnable tools)

Then you can import whole packages instead of individual files:

import 'package:apples';

Nontrivial applications should always be organized as packages. This alleviates a lot of having to repeat file system paths in each referring file; plus, they run faster. It also makes it easy to share your package on pub.dev, where other developers can very easily grab it for their own use. Packages used by your app will cause source code to be copied to your file system, so you can debug as deep into those packages as you wish.

2. Data Types

There are major differences in Dart’s type system to be aware of, regarding nulls, numeric types, collections, and dynamic types.

Nulls Everywhere

Coming from C# or Java, we are used to primitive or value types as distinct from reference or object types. Value types are, in practice, allocated on the stack or in registers, and copies of the value are sent as function parameters. Reference types are instead allocated on the heap, and only pointers to the object are sent as function parameters. Since value types always occupy memory, a value-typed variable cannot be null, and all value-type members must have initialized values.

Dart eliminates that distinction because everything is an object; all types ultimately derive from the type Object. So, this is legal:

int i = null;

In fact, all primitives are implicitly initialized to null. This means you cannot assume that default values of integers are zero as you are used to in C# or Java, and you might need to add null checks.

Interestingly, even Null is a type, and the word null refers to an instance of Null:

print(null.runtimeType); // prints Null

Not As Many Numeric Types

Unlike the familiar assortment of integer types from 8 to 64 bits with signed and unsigned flavors, Dart’s main integer type is just int, a 64-bit value. (There’s also BigInt for very large numbers.)

Since there is no byte array as part of the language syntax, binary file contents can be processed as lists of integers, i.e. List<Int>.

If you’re thinking this must be terribly inefficient, the designers thought of that already. In practice, there are different internal representations depending on the actual integer value used at runtime. The runtime does not allocate heap memory for the int object if it can optimize that away and use a CPU register in unboxed mode. Also, the library byte_data offers UInt8List and some other optimized representations.

Collections

Collections and generics are a lot like what we are used to. The main thing to note is that there are no fixed-size arrays: Just use the List data type wherever you would use an array.

Also, there is syntactic support for initializing three of the collection types:

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

So, use the Dart List where you would use a Java array, ArrayList, or Vector; or a C# array or List. Use Set where you would use a Java/C# HashSet. Use Map where you would use a Java HashMap or C# Dictionary.

3. Dynamic and Static Typing

In dynamic languages like JavaScript, Ruby, and Python, you can reference members even if they don’t exist. Here’s a JavaScript example:

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
  // ...
}

If you run this, person.age will be undefined, but it runs anyway.

Likewise, you can change the type of a variable in JavaScript:

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

By contrast, in Java, you cannot write code like the above because the compiler needs to know the type, and it checks that all operations are legal—even if you use the var keyword:

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

Java only allows you to code with static types. (You can use introspection to do some dynamic behavior, but it’s not directly part of the syntax.) JavaScript and some other purely dynamic languages only allow you to code with dynamic types.

The Dart language allows both:

// 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 has the pseudo-type dynamic which causes all the type logic to be handled at runtime. The attempt to call a.foo() will not bother the static analyzer and the code will run, but it will fail at runtime because there is no such method.

C# was originally like Java, and later added dynamic support, so Dart and C# are about the same in this regard.

4. Functions

Function Declaration Syntax

The function syntax in Dart is a little lighter and more fun than in C# or Java. The syntax is any of these:

// functions as declarations
return-type name (parameters) {body}
return-type name (parameters) => expression;

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

For example:

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

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

Parameter Passing

Since everything is an object, including primitives like int and String, parameter passing might be confusing. While there is no ref parameter passing like in C#, everything is passed by reference, and the function cannot change the caller’s reference. Since objects are not cloned when passed to functions, a function may change properties of the object. However, that distinction for primitives like int and String is effectively moot since those types are immutable.

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

If you’re in the C# or Java worlds, you’ve probably cursed at situations with confusingly overloaded methods like these:

// 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

Or with C# optional parameters, there is another kind of confusion:

// 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# does not require naming optional arguments at call sites, so refactoring methods with optional parameters can be dangerous. If some call sites happen to be legal after the refactor, the compiler won’t catch them.

Dart has a safer and very flexible way. First of all, overloaded methods are not supported. Instead, there are two ways to handle optional parameters:

// 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

You cannot use both styles in the same function declaration.

async Keyword Position

C# has a confusing position for its async keyword:

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

This implies the function signature is asynchronous, but really only the function implementation is asynchronous. Either of the above signatures would be a valid implementation of this interface:

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

In the Dart language, async is in a more logical place, denoting the implementation is asynchronous:

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

Scope and Closures

Like C# and Java, Dart is lexically scoped. This means a variable declared in a block goes out of scope at the end of the block. So Dart handles closures the same way.

Property syntax

Java popularized the property get/set pattern but the language does not have any special syntax for it:

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

C# has syntax for it:

// c#
private string clientName;
public string ClientName {
    get { return clientName; }
    set { clientName = value; }
}

Dart has a slightly different syntax supporting properties:

// dart
string _clientName;
string get ClientName => _clientName;
string set ClientName(string s) { _clientName = s; }

5. Constructors

Dart constructors have quite a bit more flexibility than in C# or Java. One nice feature is the ability to name different constructors in the same class:

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

You can call a default constructor with just the class name: var c = Client();

There are two kinds of shorthand for initializing instance members prior to the constructor body being called:

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 run superclass constructors and redirect to other constructors in the same class:

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

Constructors that call other constructors in the same class in Java and C# can get confusing when they both have implementations. In Dart, the limitation that redirecting constructors cannot have a body forces the programmer to make the layers of constructors clearer.

There’s also a factory keyword that allows a function to be used like a constructor, but the implementation is just a regular function. You can use it to return a cached instance or an instance of a derived type:

class Shape {
    factory Shape(int nsides) {
        if (nsides == 4) return Square();
        // etc.
    }
} 

var s = Shape(4); 

6. Modifiers

In Java and C#, we have access modifiers like private, protected, and public. In Dart, this is drastically simplified: If the member name starts with an underscore, it’s visible everywhere inside the package (including from other classes) and hidden from outside callers; otherwise, it is visible from everywhere. There are no keywords like private to signify visibility.

Another kind of modifier controls changeability: The keywords final and const are for that purpose, but they mean different things:

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

The Dart language supports interfaces, classes, and a kind of multiple inheritance. However, there is no interface keyword; instead, all classes are also interfaces, so you can define an abstract class and then implement it:

abstract class HasDesk {
    bool isDeskMessy(); // no implementation here
}
class Employee implements HasDesk {
    bool isDeskMessy() { ...} // must be implemented here
}

Multiple inheritance is done with a main lineage using the extends keyword, and other classes using the with keyword:

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

In this declaration, the Employee class derives from Person and Salaried, but Person is the main superclass and Salaried is the mixin (the secondary superclass).

8. Operators

There are some fun and useful Dart operators that we are not used to.

Cascades allow you to use a chaining pattern on anything:

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

The spread operator allows a collection to be treated as a list of its elements in an initializer:

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

9. Threads

Dart has no threads, which allows it to transpile to JavaScript. It has “isolates” instead, which are more like separate processes, in the sense that they cannot share memory. Since multi-threaded programming is so error-prone, this safety is seen as one of Dart’s advantages. To communicate between isolates, you need to stream data between them; the received objects are copied into the receiving isolate’s memory space.

Develop with the Dart Language: You Can Do This!

If you are a C# or Java developer, what you already know will help you learn the Dart language quickly, since it was designed to be familiar. To that end, we’ve put together a Dart cheat sheet PDF for your reference, specifically focusing on important differences from C# and Java equivalents:

Dart language cheat sheet PDF

The differences shown in this article combined with your existing knowledge will help you become productive within your first day or two of Dart. Happy coding!

Understanding the basics

  • What is the purpose of Dart?

    Dart is a modern, type-safe, and easily accessible language developed by Google. It compiles to native desktop and mobile platforms and transpiles to JavaScript for web apps.

  • What is the Dart language used for?

    As a multi-platform general-purpose language, Dart can be used for web and mobile front ends and back ends, database applications, and scripting. The Google AdWords user interface was built with Dart.

  • Is Dart a good language?

    Dart is a language that combines many of the best features of C#, Java, Python, and JavaScript, such as dynamic and static typing, async support, and lambda functions. It has been carefully designed to avoid bug-prone complexities and to be learned quickly.

  • Is C# a good first language?

    Python and JavaScript are often recommended as first languages to learn because practice scripts can be run quickly with a low learning curve. However, any language can be first, as most skills from one language are transferable to others. Starting with C# can be beneficial because it teaches type safety early.

  • What language is Dart similar to?

    Dart’s syntax looks and behaves a lot like C#, its closest cousin. It also supports interpreting scripts from source code, much like Python.

  • Is Dart a compiled language?

    Dart can be either compiled ahead of time (AOT) or just in time (JIT). The deployment of a Dart application allows for either compiling to a native platform or running directly from source code.

  • Does Dart compile to JavaScript?

    Dart optionally transpiles to JavaScript, allowing Dart (and Flutter) apps to run in a browser. It also supports compiling to native executables.

Hire a Toptal expert on this topic.
Hire Now
Star Ford

Star Ford

Verified Expert in Engineering
6 Years of Experience

Las Vegas, NM, United States

Member since October 12, 2016

About the author

Star is an architect and developer with specializations in business processes, requirements writing, databases, C#, and web development.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.