Порхающий дротик
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.
Это все!