Порхающий дротик

Fluttering Dart: функции

Как писать, использовать и злоупотреблять функциями в Dart

В проектах Flutter может использоваться как платформенно-зависимый, так и кроссплатформенный код. Последний написан на Dart, и для создания приложений Flutter требуются некоторые базовые знания Dart.

Цель Fluttering Dart - изучить фундаментальные знания и раскрыть советы и рекомендации мощного языка программирования, который воплощает в жизнь Flutter.

В первой части серии мы рассмотрели встроенные типы данных Дарта.

Во второй части мы познакомимся с функциями.

Некоторые примеры кода можно опробовать и поиграть с помощью DartPad.

Функции

функция (также может называться методом в контексте объекта) - это подмножество алгоритма, которое логически разделено и может использоваться повторно. Он может ничего не возвращать (void) или возвращать встроенный тип данных или пользовательский тип данных. Он также может не иметь параметров или иметь любое количество параметров.

В Dart, истинном объектно-ориентированном языке программирования, функции также являются объектами, и их тип, как вы уже догадались, Функция.

Это имеет некоторые последствия: мы можем назначать функции переменным или передавать их в качестве аргументов для других функций.

Ниже показан простой пример функции toggle:

bool toggle(bool value) {
  // returns the opposite 
  return !value;
}

Мы можем опустить типы, и функция будет работать так же (хотя рекомендуется использовать аннотацию типов):

toggle(value) {
  // also returns the opposite
  return !value;
}

Для функций, содержащих только одно выражение, мы можем использовать сокращенный синтаксис (также называемый синтаксисом стрелки):

toggle(value) => !value;

Функции могут иметь два типа параметров: обязательные и необязательные. Обязательные параметры занимают первую строку, за которой следуют любые необязательные параметры.

Необязательные параметры

Они могут быть либо именованными, либо позиционными:

  • Именованные параметры

При вызове функции мы можем указать именованные параметры, используя paramName: value. Например:

toggleSound(to: true, notifyUser:false);

При определении функции используйте {param1, param2, ...}, чтобы указать именованные параметры:

void toggleSound({bool to, bool notifyUser}) {
  // code that (optionally) toggles the sound
  // and (optionally) notifies the user
}

Именованные параметры по умолчанию являются необязательными. Чтобы сделать их обязательными, мы должны пометить их с помощью @required (это не работает в DartPad):

import 'package:meta/meta.dart';
void toggleSound({@required bool to, bool notifyUser}) {
  // code that toggles the sound
  // and (optionally) notifies the user
}
  • Позиционные параметры

Если мы заключим набор параметров функции в [], мы помечаем их как необязательные позиционные параметры. Приведенный выше пример выглядит следующим образом:

void toggleSound(bool to, [bool notifyUser]) {
  // code that toggles the sound
  // and (optionally) notifies the user
}

Мы можем установить константы времени компиляции как значения по умолчанию как для именованных, так и для позиционных параметров. Если значение не указано, параметры будут нулевыми.

Допустим, в приведенном выше примере мы хотим установить значение по умолчанию notifyUser на false. Это будет выглядеть так:

void toggleSound({bool to, bool notifyUser=false}) {
  // code that (optionally) toggles the sound
  // and (by default) doesn't notify the user
}

Следует отметить, что значения по умолчанию могут иметь только необязательные параметры.

главный()

Каждое создаваемое нами приложение должно иметь точку входа, и эту роль выполняет обязательная функция верхнего уровня main(). Эта функция возвращает void и имеет необязательный параметр List<String> для аргументов, как мы можем видеть ниже:

// a web app
void main() {
  // web app code goes here
}

or

// a command line app
void main(List<String> arguments) {
  print(arguments);
}

Первоклассные объекты

Как упоминалось в начале, мы можем передавать функции в качестве параметров другим функциям, и мы можем назначать их переменным:

Function _toggleSound = (SoundSource source, [bool to=false, bool notifyUser=false]) {
  // code that toggles the sound of a SoundSource object
  // by default mutes it without notification
}
List<SoundSource> soundSources = <SoundSource>[source1, source2, source3];
// we're passing the _toggleSound Function variable as
// parameter to the forEach method of the soundSources List object
soundSources.forEach(_toggleSound);

Анонимные функции

Хотя большинство функций, которые мы создадим, имеют и должны иметь имена, у нас есть возможность создавать безымянные функции. Эти функции называются анонимными, лямбда или закрывающими функциями.

В предыдущем примере кода мы определили такую ​​функцию и назначили ее переменной _toggleSound Function.

Лексическая область видимости и закрытия

Dart - это язык с лексической областью видимости, что означает, что область видимости переменных определяется статически, просто компоновкой кода. Мы можем «проследить за фигурными скобками наружу», чтобы увидеть, входит ли переменная в область видимости.

В приведенном ниже примере игры строковая переменная врага будет доступна или находится в области видимости для всех вложенных функций, если мы не решим переопределить значение, объявив переменную с идентичное имя (новая переменная будет доступна только на этом уровне области). Мы сделали это в функции firstLevel. После того, как область видимости закончится, переменная перестанет существовать, и объявленная переменная верхнего уровня снова станет доступной.

void main() {
  String enemy = '';
  void game() {
    void init() {
      enemy = 'boss';
    }
    void firstLevel() {
      String enemy = 'miniboss';
      print(enemy);
    }
    void endGame() {
      print(enemy);
    }
    init();
    firstLevel();
    endGame();
  }
  game();
}

Замыкания - это функциональные объекты, которые имеют доступ к переменным в своей области видимости, даже если они используются за пределами своей начальной области.

Функции могут закрывать переменные, определенные в окружающих областях. В следующем примере customiseGreeting перехватывает переменную приветствие. Куда бы ни отправилась возвращенная функция, она запоминает приветствие.

Function customiseGreeting(String greeting) {
  return (String name) => '$greeting, $name,';
}
void main() {
  var goodMorning = customizeGreeting('Good morning');
  var goodEvening = customizeGreeting('Good evening');
  print(goodMorning('Monica'));
  print(goodEvening('Monica'));
}

Typedefs

В Dart функции - это объекты (экземпляры типа Function). Фактический тип на самом деле является результатом его сигнатуры: тип параметров + тип возвращаемого значения.

Когда функция используется как параметр или возвращаемое значение, имеет значение фактический тип функции. typedef в Dart позволяет нам создавать псевдоним типа функции. Псевдоним типа можно использовать так же, как и другие типы.

typedef Processor<T> = T Function(T value);
typedef Printer<T> = void Function(T value);
Function customiseGreeting(String greeting) {
  return (String name) => '$greeting, $name,';
}
void greet<T>(List<T> list, Processor<T> processor, Printer<T> printer) {
  list.forEach((item) {
    printer(processor(item));
  });
}
void main() {
  Function goodMorning = customiseGreeting('Good morning'); 
  greet(['Monica', 'cats', 'Collin'], goodMorning, print);
}

В приведенном выше примере у нас есть функция greet, которая получает список имен и приветствует всех в этом списке. Он делает это с помощью typedefs процессора и принтера. Процессором является функция customiseGreeting, которую мы использовали в предыдущем примере. Принтер - это фактическая функция печати.

Исключения

Часто мы вызываем функции внутри других функций, и иногда эти функции не работают.

В Dart есть механизм исключений, аналогичный тому, что есть в Java, с той разницей, что все исключения в Dart не отмечены (функции не объявляют исключения, которые они могут генерировать). Это означает, что нам разрешено пропустить перехват исключений. Эта свобода может нанести ущерб общей производительности и надежности приложения, и надежные приложения должны правильно обрабатывать сбои.

Чтобы сообщить об ошибках, мы должны использовать ключевое слово throw. Хотя все ненулевые объекты могут быть выбрасывать n, рекомендуется выбрасывать только объекты типов Error и Exception.

Объект Ошибка представляет собой ошибку в нашем коде. Ошибки не предназначены для обнаружения и обычно они (должны) привести к завершению программы.

Объект Исключение представляет собой неудачный поворот событий в коде. По сравнению с ошибками они предназначены для выявления и устранения. Обычно они несут полезную информацию. Мы должны создать пользовательские типы данных, которые расширяются от Exception, чтобы инкапсулировать полезные данные.

После создания исключения мы можем поймать и остановить его распространение. Основная цель - справиться с этим (мы не должны ловить исключения просто для развлечения).

Исключения выявляются с помощью ключевых слов try, on и catch. catch используется, когда мы хотим получить доступ к объекту исключения и трассировке стека, и on, когда нам все это наплевать.

Чтобы повторно вызвать исключение, мы должны использовать ключевое слово rethrow. Это может пригодиться, когда мы хотим частично обработать исключение (что не является хорошей практикой) или нам нужно, чтобы оно было выше по стеку.

Наконец, у нас есть ключевое слово finally, которое позволяет нам запускать код в конце, независимо от возникающих исключений.

Применяя все это к предыдущему примеру и убедившись, что мы вызываем исключение, мы получим:

void main() {
  Function goodMorning;
  try {
    goodMorning('test');
  } on NoSuchMethodError catch(e) {
    print('our attempt to greet failed: ${e.runtimeType}'); 
    goodMorning = customiseGreeting('Good morning'); 
  } finally {
    greet(['Monica', 'cats', 'Collin'], goodMorning, print);
  }
}

Функции Dart гибкие и очень мощные.

Вы можете взглянуть на этот красивый пример использования функций верхнего уровня:



Когда дело доходит до функций, мы только что рассмотрели основы. Чтобы разобраться во всем, нужно время и практика.

В следующей части серии Fluttering Dart мы углубимся в операторы, еще один фундаментальный принцип Dart, необходимый для создания надежных приложений Flutter.



Это все!