Saltar al contenido principal

Funciones

Todo sobre las funciones en Dart.

Dart es un lenguaje verdaderamente orientado a objetos, por lo que incluso las funciones son objetos y tienen un tipo, Function. Esto significa que las funciones se pueden asignar a variables o pasarse como argumentos a otras funciones. También puedes llamar a una instancia de una clase Dart como si fuera una función. Para más detalles, consulta Objetos invocables (Callable objects).

Aquí tienes un ejemplo de cómo implementar una función:

dart
bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

Aunque Effective Dart recomienda anotaciones de tipo para las APIs públicas, la función sigue funcionando si omites los tipos:

dart
isNoble(atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

Para las funciones que contienen solo una expresión, puedes usar una sintaxis abreviada:

dart
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

La sintaxis => expr es una abreviatura de { return expr; }. La notación => a veces se denomina sintaxis de flecha (arrow).

Parámetros

#

Una función puede tener cualquier número de parámetros posicionales obligatorios. Estos pueden ir seguidos por parámetros nombrados o por parámetros posicionales opcionales (pero no ambos).

Puedes usar comas finales al pasar argumentos a una función o al definir parámetros de función.

Parámetros nombrados

#

Los parámetros nombrados son opcionales a menos que estén marcados explícitamente como required.

Al definir a una función, usa {param1, param2, …} para especificar parámetros nombrados. Si no proporcionas un valor por defecto o marcas un parámetro nombrado como required, sus tipos deben ser anulables ya que su valor por defecto será null:

dart
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool? bold, bool? hidden}) {
  ...
}

Al llamar a una función, puedes especificar argumentos nombrados usando paramName: value. Por ejemplo:

dart
enableFlags(bold: true, hidden: false);

Para definir un valor por defecto para un parámetro nombrado diferente de null, usa = para especificar un valor por defecto. El valor especificado debe ser una constante en tiempo de compilación. Por ejemplo:

dart
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {
  ...
}

// bold will be true; hidden will be false.
enableFlags(bold: true);

Si en cambio quieres que un parámetro nombrado sea obligatorio, requiriendo que los llamadores proporcionen un valor para el parámetro, anótalos con required:

dart
const Scrollbar({super.key, required Widget child});

Si alguien intenta crear un Scrollbar sin especificar el argumento child, entonces el analyzer informa de un problema.

Es posible que prefieras colocar los argumentos posicionales primero, pero Dart no lo requiere. Dart permite colocar argumentos nombrados en cualquier lugar de la lista de argumentos cuando convenga a tu API:

dart
repeat(times: 2, () {
  ...
});

Parámetros posicionales opcionales

#

Envolver un conjunto de parámetros de función entre [] los marca como parámetros posicionales opcionales. Si no proporcionas un valor por defecto, sus tipos deben ser anulables ya que su valor por defecto será null:

dart
String say(String from, String msg, [String? device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

Aquí tienes un ejemplo de cómo llamar a esta función sin el parámetro opcional:

dart
assert(say('Bob', 'Howdy') == 'Bob says Howdy');

Y aquí tienes un ejemplo de cómo llamar a esta función con el tercer parámetro:

dart
assert(
  say('Bob', 'Howdy', 'smoke signal') ==
      'Bob says Howdy with a smoke signal',
);

Para definir un valor por defecto para un parámetro posicional opcional diferente de null, usa = para especificar un valor por defecto. El valor especificado debe ser una constante en tiempo de compilación. Por ejemplo:

dart
String say(String from, String msg, [String device = 'carrier pigeon']) {
  var result = '$from says $msg with a $device';
  return result;
}

assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');

Modificadores de parámetros

#

En Dart 3.13 y versiones posteriores, no puedes usar modificadores como final o var para parámetros de función normales. Estas palabras clave ahora están reservadas exclusivamente para que los constructores primarios (primary constructors) declaren campos de instancia.

Si deseas imponer la inmutabilidad para los parámetros de función, usa reglas del linter como parameter_assignments en lugar de la palabra clave final en la firma.

La función main()

#

Cada aplicación debe tener una función main() de nivel superior, que sirve como el punto de entrada de la aplicación. La función main() devuelve void y tiene un parámetro opcional List<String> para los argumentos.

Aquí tienes una función main() simple:

dart
void main() {
  print('Hello, World!');
}

Aquí tienes un ejemplo de la función main() para una aplicación de línea de comandos que toma argumentos:

args.dart
dart
// Run the app like this: dart run args.dart 1 test
void main(List<String> arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}

Puedes usar la biblioteca args para definir y analizar argumentos de línea de comandos.

Las funciones como objetos de primera clase

#

Puedes pasar una función como parámetro a otra función. Por ejemplo:

dart
void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// Pass printElement as a parameter.
list.forEach(printElement);

También puedes asignar una función a una variable, como:

dart
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

Este ejemplo utiliza una función anónima. Más sobre ellas en la siguiente sección.

Tipos de funciones

#

Puedes especificar el tipo de una función, lo que se conoce como un tipo de función (function type). Un tipo de función se obtiene del encabezado de la declaración de una función reemplazando el nombre de la función por la palabra clave Function. Además, se te permite omitir los nombres de los parámetros posicionales, pero los nombres de los parámetros nombrados no se pueden omitir. Por ejemplo:

dart
void greet(String name, {String greeting = 'Hello'}) =>
    print('$greeting $name!');

// Store `greet` in a variable and call it.
void Function(String, {String greeting}) g = greet;
g('Dash', greeting: 'Howdy');

Funciones anónimas

#

Aunque le pones nombre a la mayoría de las funciones, como main() o printElement(), también puedes crear funciones sin nombres. Estas funciones se llaman funciones anónimas, lambdas o closures.

Una función anónima se asemeja a una función nombrada ya que tiene:

  • Cero o más parámetros, separados por comas
  • Anotaciones de tipo opcionales entre paréntesis.

El siguiente bloque de código contiene el cuerpo de la función:

dart
([[Type] param1[, ...]]) {
  codeBlock;
}

El siguiente ejemplo define una función anónima con un parámetro sin tipo, item. La función anónima lo pasa a la función map. La función map, invocada para cada elemento de la lista, convierte cada cadena a mayúsculas. Luego, la función anónima pasada a forEach, imprime cada cadena convertida junto con su longitud.

dart
const list = ['apples', 'bananas', 'oranges'];

var uppercaseList = list.map((item) {
  return item.toUpperCase();
}).toList();
// Convert to list after mapping

for (var item in uppercaseList) {
  print('$item: ${item.length}');
}

Haz clic en Run para ejecutar el código.

Si la función contiene solo una expresión o sentencia de retorno, puedes acortarla usando la notación de flecha. Pega la siguiente línea en DartPad y haz clic en Run para verificar que es funcionalmente equivalente.

dart
var uppercaseList = list.map((item) => item.toUpperCase()).toList();
uppercaseList.forEach((item) => print('$item: ${item.length}'));

Ámbito léxico

#

Dart determina el alcance de las variables basándose en la disposición de su código. Un lenguaje de programación con esta característica se denomina lenguaje de ámbito léxico (lexical scope). Puedes "seguir las llaves hacia afuera" para ver si una variable está al alcance.

Ejemplo: Una serie de funciones anidadas con variables en cada nivel de alcance:

dart
bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}

El método nestedFunction() puede usar variables de cada nivel, hasta llegar al nivel superior.

Closures léxicos

#

Un objeto de función que puede acceder a variables en su ámbito léxico cuando la función se encuentra fuera de ese ámbito se denomina closure.

Las funciones pueden capturar variables definidas en los ámbitos circundantes. En el siguiente ejemplo, makeAdder() captura la variable addBy. Dondequiera que vaya la función devuelta, recuerda addBy.

dart
/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  // Create a function that adds 2.
  var add2 = makeAdder(2);

  // Create a function that adds 4.
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}

Tear-offs

#

Cuando te refieres a una función, método o constructor nombrado sin paréntesis, Dart crea una referencia a función (tear-off). Esto es un closure que recibe los mismos parámetros que la función e invoca la función subyacente cuando lo llamas. Si tu código necesita un closure que invoque una función nombrada con los mismos parámetros que acepta el closure, no envuelvas la llamada en una lambda. Usa una referencia a función (tear-off).

dart
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
bien dart
// Function tear-off
charCodes.forEach(print);

// Method tear-off
charCodes.forEach(buffer.write);
mal dart
// Function lambda
charCodes.forEach((code) {
  print(code);
});

// Method lambda
charCodes.forEach((code) {
  buffer.write(code);
});

Comprobar la igualdad de las funciones

#

Aquí tienes un ejemplo de cómo comprobar la igualdad de funciones de nivel superior, métodos estáticos y métodos de instancia:

dart
void foo() {} // A top-level function

class A {
  static void bar() {} // A static method
  void baz() {} // An instance method
}

void main() {
  Function x;

  // Comparing top-level functions.
  x = foo;
  assert(foo == x);

  // Comparing static methods.
  x = A.bar;
  assert(A.bar == x);

  // Comparing instance methods.
  var v = A(); // Instance #1 of A
  var w = A(); // Instance #2 of A
  var y = w;
  x = w.baz;

  // These closures refer to the same instance (#2),
  // so they're equal.
  assert(y.baz == x);

  // These closures refer to different instances,
  // so they're unequal.
  assert(v.baz != w.baz);
}

Valores de retorno

#

Todas las funciones devuelven un valor. Si no se especifica un valor de retorno, la sentencia return null; se añade implícitamente al cuerpo de la función.

dart
foo() {}

assert(foo() == null);

Para devolver múltiples valores en una función, agrupa los valores en un record.

dart
(String, int) foo() {
  return ('something', 42);
}

Getters y setters

#

Cada acceso a propiedad (de nivel superior, estático o de instancia) es una invocación de un getter o un setter. Una variable crea implícitamente un getter y, si es mutable, un setter. Es por esto que cuando accedes a una propiedad, en realidad estás llamando a una pequeña función en segundo plano. Leer una propiedad llama a una función getter, y escribir en una llama a una función setter, incluso en los casos en que la propiedad se declara como una variable.

Sin embargo, también puedes declarar getters y setters explícitamente con las palabras clave get y set respectivamente. Esto permite calcular el valor de una propiedad cuando se lee o se escribe.

El propósito de usar getters y setters es crear una separación clara entre el cliente (el código que usa la propiedad) y el proveedor (la clase o biblioteca que la define). El cliente solicita o establece un valor sin necesidad de saber si ese valor está almacenado en una variable simple o si se calcula en el momento. Esto le da al proveedor la libertad de cambiar cómo funciona la propiedad.

Por ejemplo, debido a que el valor de la propiedad podría no estar almacenado en ningún lugar, podría calcularse cada vez que se llama al getter. Otro ejemplo es cuando un valor se almacena en una variable privada y el acceso público solo se permite llamando a un getter o un setter.

El siguiente ejemplo ilustra esto, con el getter y setter secret proporcionando acceso indirecto a la variable privada _secret con sus propias manipulaciones sobre los valores asignados y recuperados.

dart
// Defines a variable `_secret` that is private to the library since
// its identifier starts with an underscore (`_`).
String _secret = 'Hello';

// A public top-level getter that
// provides read access to [_secret].
String get secret {
  print('Getter was used!');
  return _secret.toUpperCase();
}

// A public top-level setter that
// provides write access to [_secret].
set secret(String newMessage) {
  print('Setter was used!');
  if (newMessage.isNotEmpty) {
    _secret = newMessage;
    print('New secret: "$newMessage"');
  }
}

void main() {
  // Reading the value calls the getter.
  print('Current message: $secret');

  /*
  Output:
  Getter was used!
  Current message: HELLO
  */

  // Assigning a value calls the setter.
  secret = 'Dart is fun';

  // Reading it again calls the getter to show the new computed value
  print('New message: $secret');

  /*
  Output:
  Setter was used! New secret: "Dart is fun"
  Getter was used!
  New message: DART IS FUN
  */
}

Generadores

#

Cuando necesites producir de forma perezosa (lazy) una secuencia de valores, considera usar una función generadora (generator function). Dart tiene soporte integrado para dos tipos de funciones generadoras:

  • Generador síncrono: Devuelve un objeto Iterable.
  • Generador asíncrono: Devuelve un objeto Stream.

Para implementar una función generadora síncrona, marca el cuerpo de la función como sync*, y usa sentencias yield para entregar valores:

dart
Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

Para implementar una función generadora asíncrona, marca el cuerpo de la función como async*, y usa sentencias yield para entregar valores:

dart
Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

Si tu generador es recursivo, puedes mejorar su rendimiento utilizando yield*:

dart
Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

Funciones externas

#

Una función externa es una función cuyo cuerpo se implementa por separado de su declaración. Incluye la palabra clave external antes de la declaración de una función, de esta manera:

dart
external void someFunc(int i);

La implementación de una función externa puede provenir de otra biblioteca Dart o, más comúnmente, de otro lenguaje. En contextos de interoperabilidad (interop), external introduce información de tipo para funciones o valores externos, haciéndolos utilizables en Dart. La implementación y el uso dependen en gran medida de la plataforma, así que consulta la documentación de interoperabilidad sobre, por ejemplo, C o JavaScript para obtener más información.

Las funciones externas pueden ser funciones de nivel superior, métodos de instancia, getters o setters, o constructores no redireccionados. Una variable de instancia también puede ser external , lo cual es equivalente a un getter externo y (si la variable no es final) a un setter externo.