Syntax Basics
Hello World
puts "Hello, Dart!" void main() {
print('Hello, Dart!');
} Every Dart program starts with a top-level
main() function β there is no implicit script mode. The void return type indicates that main produces no value. The print() function automatically appends a newline, mirroring Ruby's puts. Dart uses single or double quotes for strings interchangeably; single quotes are idiomatic in the Dart style guide.Comments
# Single-line comment
count = 42 # inline comment
=begin
Multi-line comment block
(rarely used in practice)
=end
puts count void main() {
// Single-line comment
var count = 42; // inline comment
/* Multi-line
comment block */
/// Doc comment β appears in dartdoc output.
/// Use on classes, methods, and top-level members.
print(count);
} Dart supports
// for single-line comments, /* ... */ for block comments, and /// for documentation comments. The triple-slash /// is the idiomatic Dart doc comment β the dartdoc tool collects these to generate API documentation. Unlike Ruby's rarely-used =begin / =end, Dart's block comment syntax is common and nestable.Semicolons
# Ruby: no semicolons needed
greeting = "Hello"
name = "World"
puts "#{greeting}, #{name}!"
# Semicolons allow multiple statements per line (rare)
x = 1; y = 2; puts x + y void main() {
// Dart: semicolons are required to end every statement
var greeting = 'Hello';
var name = 'World';
print('$greeting, $name!');
// Multiple statements on one line (uncommon but valid)
var x = 1; var y = 2; print(x + y);
} Dart requires a semicolon at the end of every statement β omitting one is a syntax error. Ruby infers statement boundaries from newlines and never needs semicolons in normal code. This is one of the most immediate syntactic differences a Rubyist encounters when reading Dart code.
Variables
var / type inference
count = 10 # inferred as Integer
message = "hello" # inferred as String
price = 3.99 # inferred as Float
count += 5
puts count
puts message.class
puts price.class void main() {
var count = 10; // inferred as int
var message = 'hello'; // inferred as String
var price = 3.99; // inferred as double
count += 5;
print(count);
print(message.runtimeType);
print(price.runtimeType);
} Dart's
var keyword triggers type inference β the compiler determines the type from the initializer and locks it in. Once a var variable is inferred as int, assigning a String to it later is a compile error. Ruby's variables are dynamically typed and can hold any value at any time. Dart also allows explicit type annotations (int count = 10;) when clarity is preferred over brevity.final vs const
MAX_SIZE = 100 # constant by convention
puts MAX_SIZE
# Ruby has no runtime-final vs compile-time-const distinction
greeting = "Hello"
# greeting = 42 # allowed but would change type β bad practice void main() {
// final: set once at runtime, then immutable
final greeting = 'Hello';
final DateTime now = DateTime.now();
print(greeting);
print(now.year);
// const: compile-time constant β value must be known at compile time
const int maxSize = 100;
const double pi = 3.14159;
print(maxSize);
print(pi);
} Dart distinguishes two kinds of immutability. A
final variable is assigned exactly once β the value can be computed at runtime, but the binding cannot be reassigned afterward. A const variable must be a compile-time constant: a literal value or an expression composed entirely of other compile-time constants. The Dart style guide recommends preferring final for most immutable locals and reserving const for true compile-time values.late variables
# Ruby: variables are nil until assigned β no special syntax needed
class Connection
def initialize
# @socket is implicitly nil until connect() is called
end
def connect(host)
@socket = "Socket(#{host})"
end
def send_data(data)
puts "Sending '#{data}' via #{@socket}"
end
end
conn = Connection.new
conn.connect("localhost")
conn.send_data("ping") class Connection {
// late: non-nullable but initialized after declaration
late String socket;
void connect(String host) {
socket = 'Socket($host)';
}
void sendData(String data) {
print("Sending '$data' via $socket");
}
}
void main() {
var conn = Connection();
conn.connect('localhost');
conn.sendData('ping');
} The
late modifier tells Dart's null-safety checker "I promise this non-nullable variable will be assigned before it is first read." It is useful for instance variables that are initialized in a method rather than the constructor, or for expensive lazy initialization. If a late variable is read before being assigned, a LateInitializationError is thrown at runtime. In Ruby, uninitialized instance variables simply evaluate to nil.Types
Basic types
count = 42 # Integer
price = 9.99 # Float
active = true # TrueClass
name = "Alice" # String
nothing = nil # NilClass
puts count.class
puts price.class
puts active.class
puts name.class
puts nothing.class void main() {
int count = 42;
double price = 9.99;
bool active = true;
String name = 'Alice';
// Dart has no null type for non-nullable variables
// (see Null Safety section for nullable types)
print(count.runtimeType); // int
print(price.runtimeType); // double
print(active.runtimeType); // bool
print(name.runtimeType); // String
} Dart's built-in types are
int, double, bool, String, List, Map, Set, and Null. Unlike Ruby's Integer / Float hierarchy, Dart does not have a separate integer subtype for arbitrary precision β int compiles to a 64-bit signed integer on native platforms and a JavaScript number on the web. Every Dart value (including int and bool) is an object, just like Ruby.dynamic type
# Ruby is always dynamic β any variable holds any type
value = 42
puts value
value = "now a string"
puts value
value = [1, 2, 3]
puts value.inspect void main() {
// dynamic opts out of static type checking β use sparingly
dynamic value = 42;
print(value);
value = 'now a string';
print(value);
value = [1, 2, 3];
print(value);
// Object? accepts any non-null or null value, but still type-safe
Object? anything = 'hello';
print(anything.runtimeType);
} The
dynamic type is Dart's escape hatch from static type checking β it behaves like Ruby's default: any operation is allowed at compile time and errors surface only at runtime. Using dynamic disables null safety checks too. The preferred alternative when a broadly-typed variable is needed is Object?, which accepts any value but still requires explicit type checks before calling type-specific methods.Type casting (as / is)
value = "42"
# Check type
puts value.is_a?(String) # true
puts value.is_a?(Integer) # false
# Convert / cast
number = value.to_i
puts number # 42
puts number.class # Integer
# Numeric coercions
puts 7 / 2 # 3 (integer division)
puts 7.to_f / 2 # 3.5 void main() {
Object value = 'hello';
// is: type test (returns bool)
print(value is String); // true
print(value is int); // false
// as: type cast (throws if wrong type)
String text = value as String;
print(text.toUpperCase());
// Numeric conversions
print(7 ~/ 2); // 3 β integer division operator
print(7 / 2); // 3.5 β always double
print(int.parse('42')); // parse string to int
print(3.14.toInt()); // 3 β truncate double to int
} Dart's
is operator tests the runtime type and returns a bool, similar to Ruby's is_a?. The as operator performs a checked downcast β it throws a TypeError if the value is not actually of that type. A key difference from Ruby: Dart's / operator on integers always produces a double, so 7 / 2 is 3.5. Use the ~/ (truncating division) operator for integer division.Null Safety
Non-nullable by default
# Ruby: all variables can be nil β no enforcement
name = "Alice"
puts name.upcase # fine
name = nil
# puts name.upcase # => NoMethodError at runtime
puts name.nil? # true
puts name&.upcase # nil β safe navigation void main() {
// String is non-nullable β cannot be null
String name = 'Alice';
print(name.toUpperCase());
// String? is nullable β can hold null or a String
String? nickname = null;
print(nickname?.toUpperCase()); // null β safe access with ?.
// Assigning null to a non-nullable variable is a compile error:
// String broken = null; // ERROR: A value of type 'Null' can't be assigned
} Dart has sound null safety β a variable of type
String can never be null, and the compiler enforces this statically. To allow null, the type must be explicitly marked with a question mark: String?. This eliminates an entire class of null-reference errors at compile time. Ruby has no such guarantee; any variable can hold nil and null-reference errors are always a runtime possibility.?? null-coalescing operator
username = nil
display_name = username || "Anonymous"
puts display_name # Anonymous
# ||= assigns only if nil or false
cache = nil
cache ||= "computed value"
puts cache # computed value void main() {
String? username = null;
String displayName = username ?? 'Anonymous';
print(displayName); // Anonymous
// ??= assigns only if the variable is currently null
String? cache;
cache ??= 'computed value';
print(cache); // computed value
// Chaining ?? operators
String? first;
String? second;
String result = first ?? second ?? 'fallback';
print(result); // fallback
} Dart's
?? operator returns the left-hand side if it is not null, otherwise returns the right-hand side β equivalent to Ruby's || for the common nil-check pattern. The difference is precision: Ruby's || triggers on any falsy value (nil or false), while Dart's ?? triggers only on null. The ??= compound assignment is the equivalent of Ruby's ||= but again, only null-checks rather than falsy-checks.?. safe navigation & ! null assertion
user = nil
puts user&.name # nil β no crash
puts user&.name&.upcase # nil β chains safely
# Ruby has no ! null assertion β use explicit check instead
user = "Alice"
puts user&.upcase # ALICE void main() {
String? name = null;
// ?. safe navigation β returns null if receiver is null
print(name?.toUpperCase()); // null
String? city = 'London';
print(city?.toUpperCase()); // LONDON
// ! null assertion β asserts value is non-null at runtime
// Throws Null check operator used on a null value if wrong
String? known = 'Paris';
print(known!.toUpperCase()); // PARIS β safe here
// Combining ?? and ?.
String? nickname;
print(nickname?.toUpperCase() ?? 'STRANGER'); // STRANGER
} Dart's
?. safe navigation operator mirrors Ruby's &. operator introduced in Ruby 2.3 β both short-circuit to null/nil when the receiver is absent. Dart also provides !, the null assertion operator, which tells the compiler "I know this nullable value is not null right now." Using ! on a null value throws a runtime error, so it should be used only when the developer has certainty the value is present. Prefer ?? and ?. over ! when possible.Strings
Literals & interpolation
name = "Alice"
age = 30
puts "Hello, #{name}! You are #{age} years old."
puts 'Single quotes β no interpolation: #{name}'
puts "Arithmetic: #{2 + 2}"
puts "Method call: #{name.upcase}" void main() {
var name = 'Alice';
var age = 30;
print('Hello, $name! You are $age years old.');
print("Double quotes work identically in Dart");
// Use ${} for expressions (not just identifiers)
print('Arithmetic: ${2 + 2}');
print('Method call: ${name.toUpperCase()}');
// Single variable β no braces needed
print('Name: $name');
} Dart string interpolation uses
$variableName for simple identifiers and ${expression} for any expression β analogous to Ruby's #{} which always requires braces. Unlike Ruby, both single and double quotes in Dart produce identical strings with full interpolation support; there is no distinction between raw and interpolated quoting. Dart also supports adjacent string literals that concatenate at compile time: 'foo' 'bar' becomes 'foobar'.Multi-line strings
poem = <<~HEREDOC
Roses are red,
Violets are blue,
Ruby is expressive,
And Dart is too.
HEREDOC
puts poem void main() {
// Triple-quoted strings preserve newlines and indentation
var poem = '''
Roses are red,
Violets are blue,
Ruby is expressive,
And Dart is too.
''';
print(poem.trim());
// Triple double-quotes work identically
var query = """
SELECT *
FROM users
WHERE active = true
""";
print(query.trim());
} Dart uses triple-quoted strings (
'''...''' or """...""") for multi-line content, similar to Python. Ruby uses heredoc syntax (<<~HEREDOC) which strips leading indentation automatically. Dart's triple-quoted strings include all whitespace literally, so trim() is often called to remove the leading newline from the first line. Interpolation works inside triple-quoted strings exactly as in single-line strings.Common string methods
text = " Hello, World! "
puts text.length # 18
puts text.strip # "Hello, World!"
puts text.upcase # " HELLO, WORLD! "
puts text.include?("World") # true
puts text.split(",").inspect # [" Hello", " World! "]
puts text.gsub("World", "Dart")
puts "dart".start_with?("da") # true void main() {
var text = ' Hello, World! ';
print(text.length); // 18
print(text.trim()); // Hello, World!
print(text.toUpperCase()); // " HELLO, WORLD! "
print(text.contains('World')); // true
print(text.split(',')); // [ Hello, World! ]
print(text.replaceAll('World', 'Dart'));
print('dart'.startsWith('da')); // true
print('dart'.endsWith('rt')); // true
print('dart'.indexOf('a')); // 1
} Dart's string API closely mirrors Ruby's, with predictable camelCase naming. The notable differences: Dart uses
contains() where Ruby uses include?, replaceAll() where Ruby uses gsub, and startsWith() / endsWith() where Ruby uses start_with? / end_with?. Dart strings are immutable β every transformation method returns a new string rather than modifying in place.Lists (Arrays)
Creation & access
numbers = [1, 2, 3, 4, 5]
puts numbers[0] # 1
puts numbers[-1] # 5 β negative index
puts numbers.length # 5
puts numbers.first # 1
puts numbers.last # 5
puts numbers[1..3].inspect # [2, 3, 4] void main() {
var numbers = [1, 2, 3, 4, 5]; // List<int> inferred
print(numbers[0]); // 1
print(numbers.last); // 5
print(numbers.length); // 5
print(numbers.first); // 1
print(numbers.last); // 5
print(numbers.sublist(1, 4)); // [2, 3, 4]
// Explicit type annotation
List<String> names = ['Alice', 'Bob', 'Carol'];
print(names[1]); // Bob
} Dart's
List<T> is the equivalent of Ruby's Array. The type parameter T specifies the element type, and the compiler infers it from literals. Dart does not support negative indexing (numbers[-1] throws a RangeError); use numbers.last instead. Slicing uses sublist(start, end) where end is exclusive, compared to Ruby's inclusive range syntax.Add / remove
fruits = ["apple", "banana"]
fruits.push("cherry") # or fruits << "cherry"
fruits.unshift("mango") # prepend
puts fruits.inspect
fruits.pop # remove last
fruits.shift # remove first
puts fruits.inspect
fruits.delete("banana")
puts fruits.inspect void main() {
var fruits = ['apple', 'banana'];
fruits.add('cherry'); // append
fruits.insert(0, 'mango'); // prepend
print(fruits);
fruits.removeLast(); // remove last
fruits.removeAt(0); // remove first
print(fruits);
fruits.remove('banana'); // remove by value
print(fruits);
} Dart list mutation methods follow Java-style naming:
add() for Ruby's push/<<, insert(index, value) for Ruby's unshift (more flexible), removeLast() for Ruby's pop, and removeAt(0) for Ruby's shift. Dart lists are growable by default; a fixed-length list can be created with List.filled(length, value, growable: false).map / where / fold
numbers = [1, 2, 3, 4, 5]
doubled = numbers.map { |n| n * 2 }
puts doubled.to_a.inspect # [2, 4, 6, 8, 10]
evens = numbers.filter { |n| n.even? }
puts evens.inspect # [2, 4]
total = numbers.reduce(0) { |sum, n| sum + n }
puts total # 15 void main() {
var numbers = [1, 2, 3, 4, 5];
// map returns an Iterable β call toList() to materialize
var doubled = numbers.map((number) => number * 2).toList();
print(doubled); // [2, 4, 6, 8, 10]
// where is Dart's filter / select
var evens = numbers.where((number) => number.isEven).toList();
print(evens); // [2, 4]
// fold is Dart's reduce with an explicit initial value
var total = numbers.fold(0, (sum, number) => sum + number);
print(total); // 15
} Dart's functional collection methods correspond closely to Ruby's:
map() is the same, where() is Ruby's filter/select, and fold() is Ruby's reduce with a mandatory initial value. An important difference is that Dart's map() and where() return lazy Iterable objects β call .toList() to force evaluation and get a materialized list. This lazy evaluation avoids intermediate allocations in chained pipelines.Spread operator & List.generate
first = [1, 2, 3]
second = [4, 5, 6]
combined = [*first, *second]
puts combined.inspect # [1, 2, 3, 4, 5, 6]
squares = (1..5).map { |n| n ** 2 }.to_a
puts squares.inspect # [1, 4, 9, 16, 25] void main() {
var first = [1, 2, 3];
var second = [4, 5, 6];
// Spread operator ... inserts all elements inline
var combined = [...first, ...second];
print(combined); // [1, 2, 3, 4, 5, 6]
// Null-aware spread ...? skips if the list is null
List<int>? extras;
var safe = [0, ...?extras, 99];
print(safe); // [0, 99]
// List.generate creates a list by index
var squares = List.generate(5, (index) => (index + 1) * (index + 1));
print(squares); // [1, 4, 9, 16, 25]
} Dart's spread operator
... (introduced in Dart 2.3) works inside list, map, and set literals to inline the contents of another collection β identical to Ruby's splat * in array literals. Dart's null-aware spread ...? silently skips a null collection, avoiding a null check. List.generate(count, builder) is the idiomatic equivalent of Ruby's (0...n).map { ... }.to_a.Maps (Hashes)
Creation & access
person = { name: "Alice", age: 30, city: "London" }
puts person[:name] # Alice
puts person[:age] # 30
puts person[:missing] # nil β no error
puts person.fetch(:city) # London
puts person.key?(:name) # true
puts person.length # 3 void main() {
// Map<String, dynamic> inferred
var person = {'name': 'Alice', 'age': 30, 'city': 'London'};
print(person['name']); // Alice
print(person['age']); // 30
print(person['missing']); // null β missing key returns null
print(person.containsKey('name')); // true
print(person.length); // 3
// Typed map
Map<String, int> scores = {'Alice': 95, 'Bob': 87};
print(scores['Alice']); // 95
} Dart maps are typed as
Map<KeyType, ValueType>. Unlike Ruby's symbol keys (:name), Dart maps conventionally use string keys. Accessing a missing key returns null rather than raising an error, which is similar to Ruby's default Hash behavior. Note that the return type of map[key] is always nullable (V?), so the result must be null-checked before use if the value type is non-nullable.Iteration & common methods
scores = { alice: 95, bob: 87, carol: 92 }
scores.each { |name, score| puts "#{name}: #{score}" }
puts scores.keys.inspect
puts scores.values.inspect
high_scores = scores.select { |_, score| score >= 90 }
puts high_scores.inspect
scores[:dave] = 88 # add entry
scores.delete(:bob) # remove entry
puts scores.inspect void main() {
var scores = {'alice': 95, 'bob': 87, 'carol': 92};
scores.forEach((name, score) => print('$name: $score'));
print(scores.keys.toList());
print(scores.values.toList());
// Filter entries into a new map
var highScores = Map.fromEntries(
scores.entries.where((entry) => entry.value >= 90)
);
print(highScores);
scores['dave'] = 88; // add entry
scores.remove('bob'); // remove entry
print(scores);
} Dart maps expose
keys, values, and entries as lazy iterables. The forEach() method receives a two-argument callback with the key and value. Filtering a map requires iterating entries (each entry is a MapEntry<K, V> with .key and .value) and reconstructing via Map.fromEntries(). Dart 2.7+ also supports map collection-if and collection-for syntax inside map literals for more compact construction.Control Flow
if / else
score = 78
if score >= 90
puts "A"
elsif score >= 80
puts "B"
elsif score >= 70
puts "C"
else
puts "F"
end
# Inline / postfix if
puts "Passing!" if score >= 60 void main() {
var score = 78;
if (score >= 90) {
print('A');
} else if (score >= 80) {
print('B');
} else if (score >= 70) {
print('C');
} else {
print('F');
}
// Parentheses and braces are required in Dart
if (score >= 60) print('Passing!');
} Dart's
if/else if/else structure works identically to Ruby's if/elsif/else, with two syntactic differences: the condition must be wrapped in parentheses, and the body must be wrapped in braces (single-statement branches may omit braces, but the style guide recommends always using them). Dart uses else if rather than Ruby's elsif. Dart has no postfix if modifier; if (cond) statement; replaces it.switch expression (Dart 3.0+)
status = :pending
message = case status
when :pending then "Waiting..."
when :active then "Running!"
when :done then "Complete."
else "Unknown"
end
puts message void main() {
var status = 'pending';
// Dart 3.0+ switch expression β returns a value
var message = switch (status) {
'pending' => 'Waiting...',
'active' => 'Running!',
'done' => 'Complete.',
_ => 'Unknown',
};
print(message);
// Traditional switch statement also available
switch (status) {
case 'pending':
print('Still waiting');
break;
default:
print('Other status');
}
} Dart 3.0 introduced switch expressions that return a value, making them equivalent to Ruby's
case/in expression syntax. The underscore _ is the wildcard/default arm. Dart's switch expressions also support exhaustiveness checking on sealed classes and enums. The older statement-form switch still exists and requires explicit break statements to prevent fallthrough β the expression form is preferred for new code.Ternary operator
age = 20
status = age >= 18 ? "adult" : "minor"
puts status
# Ruby also has unless
message = "hello"
puts message unless message.empty? void main() {
var age = 20;
var status = age >= 18 ? 'adult' : 'minor';
print(status); // adult
// Dart has no unless β use negated if or ternary
var message = 'hello';
if (message.isNotEmpty) print(message);
// Combining ternary and null-coalescing
String? nickname;
var displayName = nickname != null ? nickname : 'Anonymous';
print(displayName); // Anonymous
// Equivalently:
print(nickname ?? 'Anonymous');
} The ternary operator
condition ? trueValue : falseValue works identically in Ruby and Dart. Dart does not have Ruby's unless keyword β the idiomatic Dart alternative is a negated if with ! or using the convenience method isNotEmpty / isNotNull. When the ternary is purely a null fallback, Dart's ?? operator is more concise and idiomatic.Loops
for-in / each
fruits = ["apple", "banana", "cherry"]
fruits.each { |fruit| puts fruit }
# each_with_index
fruits.each_with_index do |fruit, index|
puts "#{index}: #{fruit}"
end
# For-in loop (less idiomatic in Ruby)
for fruit in fruits
puts fruit.upcase
end void main() {
var fruits = ['apple', 'banana', 'cherry'];
// for-in is idiomatic in Dart
for (var fruit in fruits) {
print(fruit);
}
// forEach with arrow function
fruits.forEach((fruit) => print(fruit.toUpperCase()));
// Indexed iteration with asMap()
fruits.asMap().forEach((index, fruit) {
print('$index: $fruit');
});
} Dart's
for (var element in collection) loop is directly analogous to Ruby's for element in collection, though Dart programmers use it far more than Ruby programmers do (Ruby prefers each). Both styles are available in Dart: the statement for-in and the method forEach(). For indexed iteration, asMap() converts the list to a Map<int, T> keyed by index, which can then be iterated with forEach.while / C-style for
count = 0
while count < 5
print "#{count} "
count += 1
end
puts
# Times loop
5.times { |i| print "#{i} " }
puts
# Range loop
(1..5).each { |i| print "#{i} " }
puts void main() {
var count = 0;
while (count < 5) {
print(count);
count++;
}
// C-style for loop
for (var i = 0; i < 5; i++) {
print(i);
}
// do-while β body executes at least once
var number = 0;
do {
print('do-while: $number');
number++;
} while (number < 3);
} Dart provides
while, do-while, and the C-style for (init; condition; increment) loop. Ruby has while and until but no C-style for β Ruby programmers reach for times, upto, and each instead. Dart's break and continue work identically to Ruby's β break exits the loop, continue skips to the next iteration.Functions
Return types & arrow syntax
def greet(name)
"Hello, #{name}!" # implicit return
end
def square(number) = number ** 2 # one-liner (Ruby 4.0)
puts greet("Alice")
puts square(7) // Return type is declared before the function name
String greet(String name) {
return 'Hello, $name!'; // explicit return required
}
// Arrow function => for single-expression bodies
int square(int number) => number * number;
// void for no return value
void printGreeting(String name) => print('Hi, $name');
void main() {
print(greet('Alice'));
print(square(7));
printGreeting('Bob');
} Dart requires explicit return type annotations before the function name β the compiler can sometimes infer them for local functions but not for top-level or class-level functions. The
=> arrow syntax is equivalent to a block with a single return statement, similar to Ruby 4.0's one-liner method syntax. Unlike Ruby, Dart functions do not return the last expression implicitly β a return statement is required in multi-line bodies.Optional positional params
def connect(host, port = 3000, protocol = "https")
puts "#{protocol}://#{host}:#{port}"
end
connect("example.com")
connect("example.com", 8080)
connect("example.com", 8080, "http") // Optional positional params go in square brackets []
void connect(String host, [int port = 3000, String protocol = 'https']) {
print('$protocol://$host:$port');
}
void main() {
connect('example.com');
connect('example.com', 8080);
connect('example.com', 8080, 'http');
} Dart wraps optional positional parameters in square brackets
[] at the end of the parameter list. Default values are specified with =, just like Ruby. Optional positional parameters must always come after required parameters and cannot be mixed with named parameters in the same brackets. Without a default value, an optional positional parameter has a nullable type (int?) and defaults to null.Named params & required
def create_user(name:, age:, role: "viewer")
puts "#{name} (#{age}), role: #{role}"
end
create_user(name: "Alice", age: 30)
create_user(age: 25, name: "Bob", role: "admin") // Named params go in curly braces {}
// required keyword enforces them at compile time
void createUser({
required String name,
required int age,
String role = 'viewer',
}) {
print('$name ($age), role: $role');
}
void main() {
createUser(name: 'Alice', age: 30);
createUser(age: 25, name: 'Bob', role: 'admin');
} Dart wraps named parameters in curly braces
{}. By default, named parameters are optional β add the required keyword to make them mandatory at compile time. This is equivalent to Ruby's bare keyword arguments (name:) which are required, and keyword arguments with defaults (role: "viewer") which are optional. Call sites look identical: createUser(name: 'Alice', age: 30) in both languages.Closures & Higher-Order Functions
Functions as values
double_it = ->(number) { number * 2 }
puts double_it.call(5) # 10
puts double_it.(5) # same thing
# Pass function as argument
def apply(value, operation)
operation.call(value)
end
puts apply(7, double_it) # 14
puts apply(7, ->(n) { n ** 2 }) # 49 void main() {
// Anonymous function assigned to a variable
var doubleIt = (int number) => number * 2;
print(doubleIt(5)); // 10
// Higher-order function that takes a function as argument
int apply(int value, int Function(int) operation) {
return operation(value);
}
print(apply(7, doubleIt)); // 14
print(apply(7, (number) => number * number)); // 49
// Closing over outer variables
var multiplier = 3;
var tripleIt = (int number) => number * multiplier;
print(tripleIt(5)); // 15
} In Dart, functions are first-class objects β they can be assigned to variables, passed as arguments, and returned from other functions, just like Ruby lambdas and procs. The function type syntax
int Function(int) describes a function that takes an int and returns an int. Dart closures capture surrounding variables by reference, identical to Ruby closures. The (params) => expression anonymous function syntax corresponds to Ruby's ->(params) { expression }.any / every / reduce
numbers = [1, 2, 3, 4, 5]
puts numbers.any? { |n| n > 4 } # true
puts numbers.all? { |n| n > 0 } # true
puts numbers.none? { |n| n > 10 } # true
puts numbers.count { |n| n.even? } # 2
puts numbers.sum # 15
puts numbers.min # 1
puts numbers.max # 5 void main() {
var numbers = [1, 2, 3, 4, 5];
print(numbers.any((number) => number > 4)); // true
print(numbers.every((number) => number > 0)); // true
// No built-in none β negate every or any
print(!numbers.any((number) => number > 10)); // true
print(numbers.where((n) => n.isEven).length); // 2
print(numbers.reduce((a, b) => a + b)); // 15
print(numbers.reduce((a, b) => a < b ? a : b)); // 1 (min)
print(numbers.reduce((a, b) => a > b ? a : b)); // 5 (max)
} Dart's
any() matches Ruby's any?, and every() matches Ruby's all?. Dart has no direct none() β negate any(). For summing, Dart lacks a built-in sum() on plain lists (it exists in package:collection), so reduce((a, b) => a + b) is the idiomatic approach. The reduce() method (unlike fold()) uses the first element as the initial accumulator, which matches Ruby's reduce without an initial value.Classes
Class declaration & constructor
class Person
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
def greet
"Hi, I'm #{@name} and I'm #{@age} years old."
end
def to_s
"Person(#{@name}, #{@age})"
end
end
alice = Person.new("Alice", 30)
puts alice.greet
puts alice.name
alice.age = 31
puts alice class Person {
String name;
int age;
// Constructor with initializer shorthand (this.name, this.age)
Person(this.name, this.age);
String greet() {
return "Hi, I'm $name and I'm $age years old.";
}
@override
String toString() => 'Person($name, $age)';
}
void main() {
var alice = Person('Alice', 30);
print(alice.greet());
print(alice.name);
alice.age = 31;
print(alice);
} Dart's
this.fieldName constructor parameter shorthand assigns the argument directly to the instance field, removing the need to write this.name = name manually β much more concise than Ruby's @name = name pattern. Instance fields are public by default (Ruby uses attr_accessor to expose them). The @override annotation is idiomatic when overriding a method; unlike Ruby, Dart does not require super.method in toString unless you want the parent's behavior.Named constructors & named params
class Point
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def self.origin
new(0, 0)
end
def self.from_map(coords)
new(coords[:x], coords[:y])
end
def to_s = "Point(#{@x}, #{@y})"
end
puts Point.new(3, 4)
puts Point.origin
puts Point.from_map({ x: 5, y: 10 }) class Point {
final double x;
final double y;
// Primary constructor
Point(this.x, this.y);
// Named constructor β ClassName.descriptiveName(...)
Point.origin() : x = 0, y = 0;
// Named constructor with named parameters
Point.fromMap({required double x, required double y})
: x = x, y = y;
@override
String toString() => 'Point($x, $y)';
}
void main() {
print(Point(3, 4));
print(Point.origin());
print(Point.fromMap(x: 5, y: 10));
} Dart supports named constructors β additional constructors with a descriptive suffix, written as
ClassName.name(). This replaces the Ruby idiom of defining class methods that call new. The initializer list (after the :) runs before the constructor body and is used to initialize final fields. Dart constructors can also be combined with named parameters by using {required this.x} directly in the parameter list.Getters / setters / private / static
class BankAccount
@@interest_rate = 0.05 # class variable
def initialize(owner, balance)
@owner = owner
@balance = balance # private by convention
end
def balance # getter
@balance
end
def deposit(amount)
@balance += amount if amount > 0
end
def self.interest_rate
@@interest_rate
end
end
account = BankAccount.new("Alice", 1000)
account.deposit(500)
puts account.balance
puts BankAccount.interest_rate class BankAccount {
static const double interestRate = 0.05; // class-level constant
final String owner;
double _balance; // _prefix = library-private in Dart
BankAccount(this.owner, this._balance);
// Getter β accessed as a property, not a method call
double get balance => _balance;
// Setter with validation
set balance(double amount) {
if (amount >= 0) _balance = amount;
}
void deposit(double amount) {
if (amount > 0) _balance += amount;
}
}
void main() {
var account = BankAccount('Alice', 1000);
account.deposit(500);
print(account.balance); // 1500.0
print(BankAccount.interestRate); // 0.05
} In Dart, a leading underscore (
_balance) makes a member library-private β accessible within the same .dart file but not importable from other files. This is stronger than Ruby's convention-only @_variable style. Dart's get and set keywords define computed properties called without parentheses at the call site, identical to Ruby's def balance and def balance=(value). Static members belong to the class itself, accessed as ClassName.member in both languages.Inheritance & Mixins
extends & @override
class Animal
def initialize(name)
@name = name
end
def speak
"..."
end
def to_s
"#{self.class.name}(#{@name})"
end
end
class Dog < Animal
def speak
"Woof!"
end
end
class Cat < Animal
def speak
"Meow!"
end
end
animals = [Dog.new("Rex"), Cat.new("Whiskers")]
animals.each { |animal| puts "#{animal}: #{animal.speak}" } class Animal {
final String name;
Animal(this.name);
String speak() => '...';
@override
String toString() => '${runtimeType}($name)';
}
class Dog extends Animal {
Dog(super.name); // super parameter β passes to parent constructor
@override
String speak() => 'Woof!';
}
class Cat extends Animal {
Cat(super.name);
@override
String speak() => 'Meow!';
}
void main() {
var animals = [Dog('Rex'), Cat('Whiskers')];
for (var animal in animals) {
print('$animal: ${animal.speak()}');
}
} Dart uses
extends for single inheritance, mirroring Ruby's <. The @override annotation is not strictly required but strongly recommended by the Dart style guide β it causes a compile warning if the method doesn't actually override anything in the parent. Dart 2.17 introduced super parameters (Dog(super.name)), which automatically forward constructor arguments to the parent, replacing the verbose Dog(String name) : super(name) pattern.Mixins
module Swimmable
def swim
"#{self.class.name} is swimming!"
end
end
module Flyable
def fly
"#{self.class.name} is flying!"
end
end
class Duck
include Swimmable
include Flyable
def to_s = "Duck"
end
duck = Duck.new
puts duck.swim
puts duck.fly mixin Swimmable {
String swim() => '$runtimeType is swimming!';
}
mixin Flyable {
String fly() => '$runtimeType is flying!';
}
// with applies mixins β multiple mixins allowed
class Duck with Swimmable, Flyable {
@override
String toString() => 'Duck';
}
// A class can extend AND use mixins
class SwimmingDog extends Object with Swimmable {
@override
String toString() => 'SwimmingDog';
}
void main() {
var duck = Duck();
print(duck.swim());
print(duck.fly());
var dog = SwimmingDog();
print(dog.swim());
} Dart mixins are declared with the
mixin keyword and applied with with β a close parallel to Ruby's module and include. Dart mixins can declare abstract methods that the mixing-in class must implement, and they can be restricted to apply only to certain base classes using mixin Foo on BaseClass. Unlike Ruby modules, Dart mixins cannot be instantiated and cannot have constructors that take parameters.Error Handling
try / catch / finally
begin
result = 10 / 0
rescue ZeroDivisionError => error
puts "Caught: #{error.message}"
rescue RuntimeError => error
puts "Runtime error: #{error.message}"
ensure
puts "Always runs (like finally)"
end
# raise an exception
begin
raise ArgumentError, "Value must be positive"
rescue ArgumentError => error
puts error.message
end void main() {
// on ExceptionType catch β type-specific catch
try {
var result = 10 ~/ 0;
print(result);
} on IntegerDivisionByZeroException catch (error) {
print('Caught: $error');
} on RangeError catch (error) {
print('Range error: $error');
} catch (error) {
print('Unknown error: $error');
} finally {
print('Always runs (finally)');
}
// throw any object β Exception or Error classes are conventional
try {
throw ArgumentError('Value must be positive');
} catch (error) {
print(error);
}
} Dart's
try/catch/finally mirrors Ruby's begin/rescue/ensure. The on ExceptionType catch (error) syntax is the type-specific equivalent of Ruby's rescue ErrorClass => error. Dart's throw can throw any non-null object β but by convention only Exception or Error subclasses are used. Dart distinguishes Error (programmer mistakes like null dereference) from Exception (expected failures like I/O errors).Custom exception classes
class InsufficientFundsError < StandardError
attr_reader :amount, :balance
def initialize(amount, balance)
@amount = amount
@balance = balance
super("Cannot withdraw #{amount}; balance is only #{balance}")
end
end
def withdraw(balance, amount)
raise InsufficientFundsError.new(amount, balance) if amount > balance
balance - amount
end
begin
withdraw(100, 150)
rescue InsufficientFundsError => error
puts error.message
puts "Tried to withdraw: #{error.amount}"
end class InsufficientFundsException implements Exception {
final double amount;
final double balance;
const InsufficientFundsException(this.amount, this.balance);
@override
String toString() =>
'Cannot withdraw $amount; balance is only $balance';
}
double withdraw(double balance, double amount) {
if (amount > balance) {
throw InsufficientFundsException(amount, balance);
}
return balance - amount;
}
void main() {
try {
withdraw(100, 150);
} on InsufficientFundsException catch (error) {
print(error);
print('Tried to withdraw: ${error.amount}');
}
} Dart custom exceptions implement the
Exception interface (or extend Error for bugs). Using implements rather than extends is idiomatic because Exception is an interface, not a class with shared implementation. The @override toString() method controls how the exception prints. Ruby uses inheritance from StandardError and calls super(message) to set the message β Dart's pattern is similar but relies on overriding toString() instead.Async / Futures
async / await basics
# Ruby: Ractor and Fiber exist, but basic async uses threads
require "thread"
result_queue = Queue.new
worker = Thread.new do
# Simulate async work
result_queue << "Hello from async work!"
end
worker.join
puts result_queue.pop // Future<T> represents a value that will be available later
Future<String> fetchGreeting() async {
// await suspends until the Future resolves
var result = await Future.value('Hello from async work!');
return result;
}
// main can be async too
Future<void> main() async {
var greeting = await fetchGreeting();
print(greeting);
// Future.value() creates an already-resolved Future
var number = await Future.value(42);
print('The answer is $number');
} Dart's concurrency model is built around
Future<T> β a promise that a value of type T will eventually be available. The async keyword marks a function as asynchronous, and await suspends execution until the awaited Future resolves. Dart runs on a single-threaded event loop (like JavaScript), so async code never truly runs in parallel β it interleaves cooperatively. Ruby's Thread does provide true parallelism in CRuby 3.x and above with the GVL relaxed for I/O.Async function with error handling
# Ruby async error handling with threads
def risky_operation(value)
raise ArgumentError, "Negative input!" if value < 0
value * 10
end
begin
puts risky_operation(5)
puts risky_operation(-1)
rescue ArgumentError => error
puts "Caught: #{error.message}"
end Future<int> riskyOperation(int value) async {
if (value < 0) {
throw ArgumentError('Negative input!');
}
return await Future.value(value * 10);
}
Future<void> main() async {
// try/catch works normally with await
try {
print(await riskyOperation(5)); // 50
print(await riskyOperation(-1)); // throws
} on ArgumentError catch (error) {
print('Caught: $error');
}
} Exceptions thrown inside an
async function travel through the Future β when await is used to unwrap the result, the exception is re-thrown at the await call site and can be caught with an ordinary try/catch block. This makes async error handling in Dart feel nearly identical to synchronous error handling, which is one of Dart's ergonomic advantages over callback-based or raw-Future.catchError patterns.Future.wait β parallel async
# Ruby: run tasks in parallel with threads
results = []
mutex = Mutex.new
threads = [1, 2, 3].map do |number|
Thread.new do
value = number * 100
mutex.synchronize { results << value }
end
end
threads.each(&:join)
results.sort!
puts results.inspect Future<int> compute(int number) async {
return await Future.value(number * 100);
}
Future<void> main() async {
// Future.wait runs all futures concurrently and
// collects results in the same order as the input list
var results = await Future.wait([
compute(1),
compute(2),
compute(3),
]);
print(results); // [100, 200, 300]
// Destructuring the results
var [first, second, third] = results;
print('$first, $second, $third');
} Future.wait() takes a list of futures and returns a single future that resolves when all of them complete, preserving the order of results. This is the Dart equivalent of running multiple threads in parallel and joining them β but without mutation, locks, or race conditions, since Dart's event loop is single-threaded. If any future in the list throws, Future.wait() propagates the first error. Dart 3.0's list pattern destructuring (var [first, second, third] = results) provides a clean way to unpack the results.