Approfondimento su Dart: Funzioni, Operazioni, Tipizzazione, Null Safety, Asincronismo e Collezioni
Questa lezione offre un approfondimento su Dart, il linguaggio alla base di Flutter, presentando esempi pratici e commentati per chiarire concetti fondamentali ed avanzati.
Introduzione a Dart
Dart è un linguaggio open source sviluppato da Google. Ha una sintassi in stile C, è orientato agli oggetti ed utilizza un garbage collector. Supporta concetti avanzati quali interfacce, mixin, classi astratte, generics e type inference, rendendolo ideale per la creazione di applicazioni moderne, in particolare con Flutter.
Storia di Dart
Creato da Lars Bak e Kasper Lund e presentato nel 2011, Dart ha visto la sua prima versione stabile nel 2013. Originariamente progettato per essere eseguito su una VM in Chrome (idea poi abbandonata), Dart si è evoluto verso la compilazione in JavaScript e, successivamente, in codice nativo. Le versioni successive hanno introdotto le seguenti che vale la pena ricordare:
- Un sistema di tipi più rigoroso.
- La compilazione nativa multipiattaforma tramite
dart2native
. - L’integrazione con Flutter.
- Con Dart 3.0, l’introduzione della null safety, record, pattern matching e nuovi modificatori di classe.
- Con Dart 3.4, il supporto alla compilazione in WebAssembly.
Una volta creato il progetto Hello World potete creare un file example.dart, nella root principale o all’interno di una cartella examples, dove potete testare i vari concetti
1. Funzioni e Uso di print()
La funzione print()
è utilizzata per inviare output alla console, fondamentale per il debugging.
// Punto di ingresso dell'applicazione
void main() {
// Stampa un messaggio di benvenuto
print("Benvenuto in Dart!");
// Dichiarazione e utilizzo di una variabile con interpolazione
int numero = 42;
print("Il valore di numero è: $numero");
}
2. Operazioni Matematiche e Logiche
Operazioni Matematiche
Dart supporta operazioni aritmetiche di base, utili per eseguire calcoli.
void main() {
int a = 10;
int b = 3;
// Esempi di operazioni matematiche
print("Somma: ${a + b}"); // 10 + 3 = 13
print("Differenza: ${a - b}"); // 10 - 3 = 7
print("Moltiplicazione: ${a * b}"); // 10 * 3 = 30
print("Divisione intera: ${a ~/ b}"); // 10 ~/ 3 = 3 (divisione intera)
print("Divisione: ${a / b}"); // 10 / 3 ≈ 3.33
print("Modulo: ${a % b}"); // 10 % 3 = 1 (resto)
}
Operazioni Logiche
Le operazioni logiche consentono di combinare condizioni booleane.
void main() {
bool condizione1 = true;
bool condizione2 = false;
// Operatore AND: true solo se entrambe le condizioni sono vere
print("AND: ${condizione1 && condizione2}"); // false
// Operatore OR: true se almeno una condizione è vera
print("OR: ${condizione1 || condizione2}"); // true
// Operatore NOT: inverte il valore booleano
print("NOT condizione1: ${!condizione1}"); // false
}
3. Commentare il Codice
Documentare il codice è essenziale per migliorare la leggibilità e la manutenzione.
Commenti su una singola linea
// Questo è un commento su una singola linea.
print("Codice eseguito."); // Commento inline
Commenti su più righe
/*
Questo è un commento su più righe.
Utile per spiegazioni dettagliate o per disabilitare temporaneamente blocchi di codice.
*/
print("Codice eseguito.");
4. Interpolazione di Stringhe con ${}
L’interpolazione permette di unire testo e variabili in modo chiaro.
void main() {
String nome = "Luca";
int anni = 28;
// Interpolazione semplice
print("Ciao, sono $nome e ho $anni anni.");
// Interpolazione con espressione
int a = 7, b = 8;
print("La somma di a e b è: ${a + b}"); // 7 + 8 = 15
}
5. Tipizzazione e Gestione delle Variabili
Dart è a tipizzazione statica; ogni variabile ha un tipo definito che aiuta a prevenire errori.
Tipi di Base
void main() {
int eta = 30; // Intero
double prezzo = 9.99; // Decimale
String saluto = "Ciao"; // Stringa
bool attivo = true; // Booleano
print("$saluto, ho $eta anni e il prezzo è $prezzo. Attivo: $attivo");
}
var
vs dynamic
void main() {
var numero = 15; // Il tipo è inferito a int e non può essere cambiato
// numero = "stringa"; // Errore: non è possibile cambiare il tipo
dynamic variabile = 15; // Tipo dinamico, può cambiare a runtime
variabile = "Ora sono una stringa";
print(variabile);
}
final
vs const
void main() {
// 'final' è assegnato una sola volta e valutato in runtime
final oggi = DateTime.now();
// 'const' è una costante nota a compile-time
const pi = 3.1415;
print("Oggi: $oggi");
print("Pi: $pi");
}
Mutabilità vs Immutabilità
void main() {
var numeroMutabile = 20;
numeroMutabile = 25; // Modifica consentita
final numeroImmutabile = 30;
// numeroImmutabile = 35; // Errore: variabile immutabile
print("Mutabile: $numeroMutabile");
print("Immutabile: $numeroImmutabile");
}
Metodi Utili per Stringhe e Numeri
void main() {
String frase = "Dart è fantastico!";
// Lunghezza della stringa
print("Lunghezza: ${frase.length}");
// Conversione in maiuscolo
print("Maiuscolo: ${frase.toUpperCase()}");
// Estrazione di una sottostringa
print("Substring: ${frase.substring(0, 4)}"); // "Dart"
// Formattazione dei numeri
double num = 3.14159;
print("Numero formattato: ${num.toStringAsFixed(2)}"); // "3.14"
}
6. Controllo del Flusso e Operatori
Costrutti Condizionali
void main() {
int x = 5;
// Condizione if-else
if (x > 0) {
print("x è positivo");
} else if (x < 0) {
print("x è negativo");
} else {
print("x è zero");
}
// Operatore ternario
String risultato = (x > 0) ? "Positivo" : "Non positivo";
print("Risultato (ternario): $risultato");
// Esempi avanzati di switch in Dart
// 1. Esempio base con lo switch (simile a JavaScript ma con regole più rigide)
int day = 3;
switch (day) {
case 1:
print("Lunedì");
break;
case 2:
print("Martedì");
break;
case 3:
print("Mercoledì");
break;
default:
print("Giorno non valido");
}
// 2. Switch con raggruppamento dei casi
// In Dart non c'è fall-through implicito come in JavaScript;
// per raggruppare casi, è possibile elencarli insieme senza break tra di essi.
String fruit = "Apple";
switch (fruit) {
case "Apple":
case "Mela":
print("È una mela/Apple");
break;
case "Banana":
print("È una banana");
break;
default:
print("Frutto sconosciuto");
}
// 3. Switch expression (Dart 3+)
// Questa forma compatta consente di restituire direttamente un valore.
int numero = 2;
String descrizione = switch (numero) {
1 => "Uno",
2 => "Due",
3 => "Tre",
_ => "Altro",
};
print("Il numero è: \$descrizione");
// 4. Uso di pattern matching con lo switch
// Dart permette di verificare il tipo della variabile direttamente nei case.
Object input = "ciao";
switch (input) {
case int i:
print("È un intero: \$i");
break;
case String s when s.isNotEmpty:
print("È una stringa non vuota: \$s");
break;
case String s:
print("È una stringa vuota");
break;
default:
print("Tipo sconosciuto");
}
}
Costrutti Iterativi
void main() {
// Loop for tradizionale
for (int i = 0; i < 3; i++) {
print("Iterazione for: $i");
}
// Loop for-in per iterare su una lista
List<String> nomi = ["Anna", "Marco", "Luca"];
for (var nome in nomi) {
print("Nome: $nome");
}
// Loop while
int j = 0;
while (j < 3) {
print("Iterazione while: $j");
j++;
}
// Loop do-while
int k = 0;
do {
print("Iterazione do-while: $k");
k++;
} while (k < 3);
}
-
break
Termina immediatamente l’esecuzione del ciclo in corso e passa al codice successivo dopo il ciclo. -
continue
Salta il resto del codice nel ciclo corrente e passa direttamente all’inizio della prossima iterazione.
Operatori di Decremento e Incremento
void main() {
int a = 5;
// Post-decremento: il valore viene restituito prima del decremento
print("Post-decremento: ${a--}"); // Stampa 5, poi a diventa 4
a = 5; // Reset del valore
// Pre-decremento: decrementa prima di restituire il valore
print("Pre-decremento: ${--a}"); // a diventa 4 e stampa 4
// Esempi con incremento:
a = 5;
print("Post-incremento: ${a++}"); // Stampa 5, poi a diventa 6
a = 5;
print("Pre-incremento: ${++a}"); // a diventa 6 e stampa 6
}
7. Null Safety
La null safety previene errori legati all’uso di valori null, obbligando a dichiarare esplicitamente variabili che possono essere null.
void main() {
// Dichiarazione di una variabile nullable (può essere null)
int? numeroNullable;
print("Numero nullable iniziale: $numeroNullable"); // Stampa null
// Operatore null-coalescing: se numeroNullable è null, usa il valore 0
int risultato = numeroNullable ?? 0;
print("Risultato (null-coalescing): $risultato");
// Safe navigation: evita errori se la variabile è null
print("Safe navigation: ${numeroNullable?.toString()}");
// Assegnazione di un valore e uso dell'operatore di null assertion
numeroNullable = 20;
print("Dopo assegnazione: ${numeroNullable!.toString()}"); // Usa '!' per affermare non essere null
}
8. Funzioni Avanzate e Closures
Le funzioni in Dart sono oggetti di prima classe e possono essere passate o restituite come variabili.
<datatype> nome_funzione() {}
I parametri possono essere:
- Posizionali obbligatori: I parametri vengono passati in ordine, e sono obbligatori.
- Opzionali posizionali: Racchiusi tra parentesi quadre, possono essere omessi; è possibile assegnare loro un valore di default.
- Nominali: Racchiusi tra parentesi graffe, possono essere passati specificando il nome del parametro. Possono essere resi obbligatori usando la keyword
required
.
// La funzione 'presentati' richiede che tutti i parametri nominali siano forniti,
// grazie alla keyword 'required'.
void presentati(int altezza, {required String nome, int? eta}) {
print("Mi chiamo $nome e ho ${eta ?? 20} anni.");
}
void presentati(int altezza, [String? nome, int? eta]) {
print("Mi chiamo $nome e ho ${eta ?? '20'} anni.");
}
void main() {
presentati(173,nome: "Giulia", eta: 28); // Funziona correttamente
// presentati(nome: "Giulia"); // Genera un errore: manca il parametro obbligatorio 'eta'
}
Lambda
void main() {
// Funzione anonima (lambda) come callback
List<int> numeri = [1, 2, 3];
numeri.forEach((numero) {
print("Numero: $numero");
});
//Uso della Lambda per Semplicità
List<String> parole = ['dart', 'flutter', 'funzioni'];
// Lambda compatta per trasformare e stampare ogni parola in maiuscolo
parole.forEach((parola) => print(parola.toUpperCase())); //Detta anche arrow function in js
}
Closure
Una closure è una funzione che “cattura” le variabili del contesto in cui è stata definita.
void main() {
// Funzione che restituisce una closure che somma un addendo al valore fornito
Function creaSommatore(int addendo) {
return (int valore) => valore + addendo;
}
var sommaDi5 = creaSommatore(5);
var sommaDi10 = creaSommatore(10);
print("Somma di 10 con 5: ${sommaDi5(10)}"); // Output: 15
print("Somma di 10 con 10: ${sommaDi10(10)}"); // Output: 20
}
Le closure in Dart:
- Permettono di creare funzioni che mantengono uno stato interno.
- Consentono di scrivere codice modulare e riutilizzabile.
- Sono particolarmente utili per callback ed eventi, dove è necessario “ricordare” il contesto in cui una funzione è stata creata.
Esempio Avanzato: Funzione che Ritorna un’Altra Funzione con Capturing
// Funzione che crea una funzione moltiplicatrice catturando il fattore
Function creaMoltiplicatore(int fattore) {
return (int valore) {
int risultato = valore * fattore;
print("Moltiplicando \$valore per \$fattore otteniamo \$risultato");
return risultato;
};
}
void main() {
var moltiplicaPer3 = creaMoltiplicatore(3);
var moltiplicaPer4 = creaMoltiplicatore(4);
moltiplicaPer3(5); // Stampa: Moltiplicando 5 per 3 otteniamo 15
moltiplicaPer4(5); // Stampa: Moltiplicando 5 per 4 otteniamo 20
}
Passaggio di Funzioni come Argomenti
Le funzioni possono essere passate come parametri per creare comportamenti dinamici.
// Funzione che applica una funzione passata come parametro a un valore
void applicaFunzione(int valore, int Function(int) funzione) {
int risultato = funzione(valore);
print("Risultato: \$risultato");
}
int incrementa(int x) => x + 1;
void main() {
applicaFunzione(7, incrementa); // Output: 8
applicaFunzione(7, (x) => x * 2); // Output: 14
}
9. Record (Tuple)
I record (o tuple) in Dart sono strutture leggere che permettono di raggruppare insieme più valori di tipi differenti in un’unica entità, senza la necessità di definire una classe dedicata.
Caratteristiche Principali
- Immutabilità
- Accesso Posizionale
$1
,$2
- Destructuring
- Campi Nominati (Dart 3)
Sintassi di Base
var recordSemplice = (42, "Alice", true);
Dichiarazione del Tipo Record
Puoi specificare esplicitamente il tipo di un record quando ne definisci una funzione o una variabile:
(int, String, bool) getPersonInfo() {
int age = 30;
String name = "Alice";
bool isActive = true;
return (age, name, isActive);
}
Accesso agli Elementi del Record
I record offrono due modi principali di accesso ai valori:
1. Accesso Posizionale
void main() {
var info = (30, "Alice", true);
print("Età: \${info.$1}"); // Stampa: 30
print("Nome: \${info.$2}"); // Stampa: Alice
print("Attivo: \${info.$3}"); // Stampa: true
}
2. Destructuring
void main() {
var (age, name, isActive) = getPersonInfo();
print("Età: \$age, Nome: \$name, Attivo: \$isActive");
}
Record con Campi Nominati
void main() {
// Record che contiene un campo posizionale ed uno o più campi nominati
var recordConNomine = (42, name: "Alice", isActive: true);
// Accesso al valore posizionale
print("Valore posizionale: \${recordConNomine.$1}"); // 42
// Accesso ai campi nominati
print("Nome: \${recordConNomine.name}"); // Alice
print("Attivo: \${recordConNomine.isActive}"); // true
}
(int, {String name, bool isActive}) getPersonData() {
int age = 30;
String personName = "Alice";
bool active = true;
return (age, name: personName, isActive: active);
}
void main() {
var data = getPersonData();
print("Età: \${data.$1}");
print("Nome: \${data.name}");
print("Attivo: \${data.isActive}");
}
Sono particolarmente utili per:
- Restituire più valori da una funzione in modo semplice e conciso.
- Organizzare dati temporanei senza dover creare una classe dedicata.
- Semplificare il codice grazie al destructuring e alla sintassi compatta.
10. Gestione degli Errori: try-catch-finally
Utilizza i blocchi try-catch-finally per gestire eccezioni e garantire l’esecuzione di codice di cleanup.
void main() {
try {
// Questo provocherà un errore: divisione per zero
int risultato = 100 ~/ 0;
print("Risultato: $risultato");
} catch (e) {
// Gestione dell'errore: stampa l'eccezione
print("Errore catturato: $e");
} finally {
// Questo blocco viene eseguito sempre
print("Blocco finally eseguito.");
}
}
Conclusioni
In questa lezione abbiamo esplorato:
- Funzioni di base e l’uso di
print()
- Operazioni matematiche e logiche con esempi commentati
- Tecniche di commento per migliorare la leggibilità del codice
- Interpolazione di stringhe con template literals
- Tipizzazione statica: uso di
int
,double
,bool
,String
,var
,dynamic
,final
econst
- Metodi utili per manipolare stringhe e numeri
- Controllo del flusso e uso degli operatori (inclusi incremento/decremento)
- Null Safety e gestione delle variabili nullable
- Funzioni avanzate, closures e funzioni anonime
- Record (Tuple)
- Gestione degli errori con try-catch-finally
Questi concetti costituiscono una base solida per lo sviluppo di applicazioni Flutter. Approfondire questi argomenti aiuta a scrivere codice più sicuro, leggibile ed efficiente.
Buon studio e buon coding!