Ruby.CodeCompared.To/Dart

An interactive executable cheatsheet for Rubyists learning Dart

Ruby 4.0 Dart 3.7
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.