diegoRodicio

View Categories

🧩 Funcións, callbacks e closures

🧩 Dividir o programa en tarefas

Unha función agrupa instrucións para poder usalas varias veces. Pode recibir datos (parámetros) e devolver un resultado con return.

// A función recibe dúas notas e devolve a media.
double calcularMedia(double primeira, double segunda) {
  return (primeira + segunda) / 2;
}

void main() {
  double media = calcularMedia(7, 8); // Chamamos á función.
  print(media);
}

double antes do nome indica o tipo do resultado. Se non precisamos devolver un resultado, usamos void, como en main().

return remata a función. Nunha función void podemos escribir return; para saír antes de chegar ao final.

📥 Parámetros posicionais e nomeados

🔢 Posicionais obrigatorios

Os valores asígnanse segundo a posición. En calcularMedia(7, 8), 7 é primeira e 8 é segunda. Hai que pasar os dous.

🏷️ Nomeados: moi habituais en Flutter

Defínense entre {}. Ao chamar á función indicamos o nome de cada argumento. Podemos marcar un como obrigatorio con required ou darlle un valor predeterminado.

void presentar({required String nome, String grupo = '2º DAM'}) {
  print('$nome pertence a $grupo');
}

void main() {
  presentar(nome: 'Iria'); // Usa o grupo predeterminado.
  presentar(grupo: '2º DAM', nome: 'Roi'); // A orde non importa.
}

Un parámetro nomeado sen required é opcional. Se non ten valor predeterminado, debe admitir null, por exemplo {String? telefono}. Os valores predeterminados deben ser constantes.

required obriga a pasar o argumento; ? permite que o valor sexa nulo. Son cousas distintas.

📦 Posicionais opcionais

Defínense entre [] e pásanse sen nome. Verémolos menos ca os nomeados.

String saudar(String nome, [String saudo = 'Ola']) {
  return '$saudo, $nome';
}

void main() {
  print(saudar('Diego')); // Ola, Diego
  print(saudar('Rosa', 'Bo día'));
}

Podemos combinar posicionais obrigatorios con nomeados ou con posicionais opcionais; non usamos [] e {} na mesma lista de parámetros.

📦 Devolver varios datos cun rexistro

Unha función devolve un único resultado, pero ese resultado pode ser un rexistro con varios campos. Así podemos devolver, por exemplo, a media e se o alumno aprobou:

({double media, bool aprobado}) avaliar(double primeira, double segunda) {
  double media = (primeira + segunda) / 2;
  return (media: media, aprobado: media >= 5); // Dous datos nun rexistro.
}

void main() {
  final resultado = avaliar(7, 9);
  print('Media: ${resultado.media}'); // 8.0
  print('Aprobado: ${resultado.aprobado}'); // true
}

({double media, bool aprobado}) indica o tipo do resultado. Despois accedemos a cada campo polo seu nome. Tamén poderiamos devolver un rexistro posicional, pero os nomes axudan a entender que representa cada valor.

➡️ Sintaxe de frecha

Se a función contén unha única expresión, podemos usar =>. Non depende do número de liñas que ocupe.

// Devolve o resultado da comparación.
bool estaAprobado(int nota) => nota >= 5;

Se precisamos varias instrucións, usamos chaves e, cando corresponda, return.

📌 Funcións como obxectos

En Dart, as funcións son tamén obxectos. Isto significa que podemos traballar con elas como facemos con outros valores:

  • gardalas nunha variable;
  • pasalas como argumento a outra función;
  • devolvelas como resultado dunha función.

📦 Gardar unha función nunha variable

Podemos gardar unha función xa existente nunha variable:

int dobre(int numero) {
  return numero * 2;
}

void main() {
  int Function(int) operacion = dobre;

  print(operacion(5)); // 10
}

Neste exemplo, operacion contén unha referencia á función dobre.

É importante distinguir entre:

ExpresiónQue fai?
dobreRefírese á función, sen executala.
dobre(5)Executa a función co argumento 5.

Ao escribir o nome sen parénteses, estamos a referirnos á función. Ao engadir os parénteses, executámola.

🏷️ Tipos de función

O tipo dunha función indica os parámetros que recibe e o resultado que devolve. Estas son notacións de tipos, non programas para executar:

TipoQue describe?
void Function()Unha función sen parámetros nin resultado.
int Function(int)Recibe un enteiro e devolve outro.
String Function(String)Recibe un texto e devolve outro.

En int Function(int) operacion = dobre;, o tipo indica que función podemos gardar en operacion.

✍️ Gardar unha función anónima

Unha función anónima non ten nome. Podemos gardala nunha variable e chamala a través desa variable:

void main() {
  String Function(String) saudar = (String nome) {
    return 'Ola, $nome!'; // Devolvemos o texto do saúdo.
  };
  print(saudar('Iria')); // Ola, Iria!
}

A mesma función cunha única expresión pode escribirse con frecha:

void main() {
  String Function(String) saudar = (String nome) => 'Ola, $nome!';
  print(saudar('Iria')); // O mesmo resultado que coa versión con bloque.
}

Nos primeiros exemplos indicamos os tipos para facilitar a lectura. Cando Dart poida deducilos, poderemos escribir (nome) en vez de (String nome).

🔔 Callbacks: pasar unha función como argumento

Un callback é unha función que se pasa como argumento a outra función para que esta a execute.

A función que recibe o callback decide cando e con que argumentos debe chamalo.

🔔 Callback sen parámetros

Creamos unha función que recibe outra función como argumento:

void executarAccion(void Function() accion) {
  print('Comeza a acción');
  accion();
  print('Remata a acción');
}

Conserva a función executarAccion() anterior e engade este código para pasarlle unha función con nome:

void avisar() {
  print('Acción realizada');
}

void main() {
  executarAccion(avisar);
}

A saída será:

Comeza a acción
Acción realizada
Remata a acción

Fixémonos novamente na diferenza:

executarAccion(avisar);   // Pasamos a función como argumento.
// executarAccion(avisar()); // Incorrecto: executa avisar e non entrega unha función.

O parámetro void Function() accion recibe a función que imos executar.

Isto indica que accion debe ser unha función que:

  • non recibe argumentos;
  • non devolve ningún valor.

Conserva executarAccion() e substitúe só main() para pasar unha función anónima:

void main() {
  executarAccion(() {
    print('Rosa revisou o aviso'); // Callback escrito directamente aquí.
  });
}

Conservando tamén executarAccion(), podemos substituír main() por esta versión con frecha:

void main() {
  executarAccion(() => print('Julia compartiu un exercicio'));
}

📥 Callback con parámetros e valor de retorno

Os callbacks tamén poden recibir parámetros e devolver resultados:

int aplicarOperacion(
  int numero,
  int Function(int) operacion,
) {
  return operacion(numero);
}

int dobre(int numero) {
  return numero * 2;
}

void main() {
  print(aplicarOperacion(5, dobre)); // 10

  print(
    aplicarOperacion(5, (int numero) => numero * numero),
  ); // 25
}

Neste caso, aplicarOperacion() recibe:

  1. un número;
  2. unha función que recibe un int e devolve outro int.

A función aplicarOperacion() chama o callback e devolve o resultado obtido.

📱 Unha primeira ollada a Flutter

📌 Este fragmento require Flutter e vai dentro dunha interface. Non é un programa completo de consola. No Tema 2.2 faremos unha ponte a Flutter; na UD 03 veremos os widgets e os eventos con detalle.

Os callbacks empréganse con frecuencia en Flutter para indicar o que debe suceder cando se produce un evento:

ElevatedButton(
  onPressed: () {
    print('Premiches o botón');
  },
  child: const Text('Premer'),
)

A función asignada a onPressed é un callback que Flutter executará cando a persoa usuaria prema o botón.

🔒 Closures: conservar o acceso ás variables do contorno

Unha closure é unha función que conserva o acceso ás variables do ámbito no que foi creada, mesmo cando se executa noutro lugar ou noutro momento.

Unha closure pode ser unha función con nome ou unha función anónima. Polo tanto, non debemos confundir estes conceptos:

  • Unha función anónima é unha función que non ten nome.
  • Un callback é unha función que se pasa como argumento a outra función.
  • Unha closure é unha función que captura variables do seu contorno.

🔒 Capturar unha variable exterior

Observa o seguinte exemplo:

void main() {
  String alumno = 'Xurxo';
  final saudar = () {
    print('Ola, $alumno'); // Usa a variable do contorno.
  };

  saudar(); // Ola, Xurxo
  alumno = 'Iria';
  saudar(); // Ola, Iria
}

A función saudar() utiliza a variable alumno, aínda que esta non foi declarada dentro da función nin se recibiu como parámetro.

Dicimos que saudar() captura a variable alumno.

Ademais, a closure non garda necesariamente unha copia do valor que tiña a variable cando se creou. Conserva o acceso á propia variable, polo que pode observar os seus cambios.

🔢 Conservar e modificar un estado

Unha función pode devolver outra función. Esa función devolta pode conservar unha variable e modificala entre chamadas:

int Function() crearContador() {
  int conta = 0;

  return () {
    conta++;
    return conta;
  };
}

void main() {
  final contador = crearContador();

  print(contador()); // 1
  print(contador()); // 2
  print(contador()); // 3
}

A variable conta créase dentro de crearContador(). Porén, a función devolta continúa tendo acceso a ela despois de que remate a execución de crearContador().

int Function() antes de crearContador indica que devolve unha función sen parámetros que produce un enteiro. Por iso crearContador() devolve unha función e contador() devolve un número. A variable conta consérvase entre esas chamadas.

Cada contador mantén o seu propio estado. Conserva crearContador() e substitúe só main() por este código:

void main() {
  final contadorA = crearContador();
  final contadorB = crearContador();

  print(contadorA()); // 1
  print(contadorA()); // 2

  print(contadorB()); // 1

  print(contadorA()); // 3
  print(contadorB()); // 2
}

contadorA e contadorB son funcións diferentes. Cada unha conserva a súa propia variable conta.

🧩 Un callback que tamén é closure

void executarAccion(void Function() accion) {
  accion(); // Executamos a función recibida.
}

void main() {
  String alumno = 'Iria';
  executarAccion(() {
    print('Gardouse a nota de $alumno'); // Accedemos á variable exterior.
  });
}

É unha función anónima porque non ten nome, un callback porque se pasa a executarAccion() e unha closure porque captura alumno.

🧭 Diferenza entre os conceptos

ConceptoQue significa?
Función como obxectoPode gardarse, pasarse como argumento ou devolverse.
Función anónimaÉ unha función que non ten nome.
CallbackÉ unha función que se pasa a outra función para que esta a execute.
ClosureÉ unha función que conserva o acceso ás variables do contorno no que foi creada.

Unha mesma función pode pertencer a varias destas categorías ao mesmo tempo.

✏️ Exercicio de comprobación

Analiza o seguinte código antes de executalo:

int Function(int) crearSumador(int cantidade) {
  return (int numero) => numero + cantidade;
}

void main() {
  final sumarCinco = crearSumador(5);
  final sumarDez = crearSumador(10);

  print(sumarCinco(3));
  print(sumarDez(3));
}

Preguntas:

Analiza o código e responde as seguintes preguntas:

  1. Que tipo de valor devolve crearSumador()?
  2. Que variable captura a función devolta?
  3. Cal será a saída do programa?
  4. A función devolta é anónima? É un callback? É unha closure?
  5. Como crearías unha función chamada sumarVinte?

Respostas:

  1. Devolve unha función de tipo int Function(int): recibe un enteiro e devolve outro.
  2. Captura o parámetro cantidade; cada chamada a crearSumador() crea un contorno independente.
  3. Mostra 8 e 13, resultado de sumar 5 e 10 ao número 3.
  4. É anónima e closure. Neste exemplo non é callback, porque non se pasa como argumento a outra función.
  5. Dentro do mesmo main(), podemos engadir:
final sumarVinte = crearSumador(20);
print(sumarVinte(3)); // 23

📚 Consulta: closures — https://dart.dev/language/functions#lexical-closures.

🧪 Exemplo completo

🔗 Ver o código en DartPad — https://dartpad.dev/?id=be0f458633ba3c80b0344ff3bf672fe6

O comentario /// documenta a función: o editor pode mostralo cando a usamos.

📚 Consulta: funcións en Dart – https://dart.dev/language/functions.