Pull to refresh

Характерные особенности языка Dart

Reading time 8 min
Views 19K
Original author: Bob Nystrom
Dart был разработан так, чтобы выглядеть знакомо для программистов на таких языках, как Java и JavaScript. Если постараться, можно писать на Dart практически так же, как на одном из них. Если очень постараться — можно даже превратить его в Фортран, но при этом вы упустите множество неповторимых и классных особенностей Dart.

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

Конструкторы


Мы начнем эту статью так же, как начинают свою жизнь объекты — с конструкторов. Каждому объекту предстоит быть созданным в конструкторе, и его определение — важный момент в создании качественного класса. У Dart есть несколько интересных особенностей.

Автоматическая инициализация полей

Для начала избавимся от занудных повторений. Конструкторы часто берут аргументы и просто присваивают их значения полям класса:

class Point {
  num x, y;
  Point(num x, num y) {
    this.x = x;
    this.y = y;
  }
}

Нам пришлось набрать x четыре раза просто чтобы инициализировать поле. Полный отстой! Лучше сделать так:

class Point {
  num x, y;
  Point(this.x, this.y);
}

Если в списке аргументов конструктора перед именем аргумента идет this., поле с этим именем будет автоматически инициализировано значением аргумента. В нашем примере использована ещё одна маленькая хитрость — если тело конструктора пустое, можно использовать ; вместо {}.

Именованные конструкторы

Как большинство динамических языков, Dart не поддерживает перегрузку. В случае методов это не так страшно, потому что мы просто можем придумать другое имя. Конструкторам повезло меньше. Чтобы облегчить их участь, Dart позволяет использовать именованные конструкторы:

class Point {
  num x, y;
  Point(this.x, this.y);
  Point.zero() : x = 0, y = 0;
  Point.polar(num theta, num radius) {
    x = Math.cos(theta) * radius;
    y = Math.sin(theta) * radius;
  }
}

У класса Point есть три конструктора — обычный и два именованных. Вот как их можно использовать:

var a = new Point(1, 2);
var b = new Point.zero();
var c = new Point.polar(Math.PI, 4.0);

Обратите внимание, что мы используем new с именованными конструкторами, это не обычные статические методы.

Фабричные конструкторы

Иногда бывает полезно использовать шаблон проектирования “фабрика”. Например, вам нужно создать экземпляр класса, но необходима некоторая гибкость, просто захардкодить вызов конструктора определенного типа недостаточно. Возможно вы хотите вернуть кешированный экземпляр, если таковой имеется, или объект другого типа.

Dart позволяет сделать это, не изменяя код в месте создания объекта. Вы можете создать фабричный конструктор, который вызывается так же, как и обычный. Например:

class Symbol {
  final String name;
  static Map<String, Symbol> _cache;

  factory Symbol(String name) {
    if (_cache == null) {
      _cache = {};
    }

    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final symbol = new Symbol._internal(name);
      _cache[name] = symbol;
      return symbol;
    }
  }

  Symbol._internal(this.name);
}

Мы определили класс Symbol. Символ — это примерно то же, что и строка, но мы хотим гарантировать, что в любой момент времени существует только один символ с данным именем. Это дает возможность безопасно проверять символы на равенство, просто убедившись, что они указывают на один и тот же объект.

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

Клёво, что всё это прозрачно для вызывающего кода:

var a = new Symbol('something');
var b = new Symbol('something');

Второй вызов на самом деле не создаст новый символ, а вернет уже имеющийся. Это удобно, так как если сначала нам не был нужен фабричный конструктор, а потом оказалось, что таки был, нам не придется где-то в коде менять new на вызов статического фабричного метода.

Функции


Как в большинстве современных языков, функции Dart — объекты первого класса, с замыканиями и облегченным вариантом синтаксиса. Любая функция — это объект, и вы можете без стеснения делать с ней что угодно. Мы широко используем функции для обработчиков событий.

В Dart есть три способа создания функций. Первый — именованные функции:

void sayGreeting(String salutation, String name) {
  final greeting = '$salutation $name';
  print(greeting);
}

Это выглядит, как обычное объявление функции в C или метода в Java или JavaScript. В отличие от C и C++, объявления функций могут быть вложенными. Второй способ — анонимные функции:

window.on.click.add((event) {
  print('You clicked the window.');
})

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

var items = [1, 2, 3, 4, 5];
var odd = items.filter((i) => i % 2 == 1);
print(odd); // [1, 3, 5]

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

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

В Dart есть ещё один фокус (это одна из моих любимых фишек языка) — вы можете использовать => для определения членов класса. Конечно, можно делать это так:

class Rectangle {
  num width, height;

  bool contains(num x, num y) {
    return (x < width) && (y < height);
  }

  num area() {
    return width * height;
  }
}

Но зачем, если можно так:

class Rectangle {
  num width, height;
  bool contains(num x, num y) => (x < width) && (y < height);
  num area() => width * height;
}

Мы находим стрелочную нотацию великолепной для определения простых геттеров/сеттеров и других однострочных функций.

Поля, геттеры и сеттеры


Для работы со свойствами Dart использует стандартный синтаксис вида object.someProperty. В Dart вы можете определить методы, которые будут выглядеть, как обращение к полю класса, но при этом выполнять произвольный код. Так же, как и в других языках, такие методы называются геттерами и сеттерами:

class Rectangle {
  num left, top, width, height;

  num get right()           => left + width;
      set right(num value)  => left = value - width;
  num get bottom()          => top + height;
      set bottom(num value) => top = value - height;

  Rectangle(this.left, this.top, this.width, this.height);
}


У нас есть класс Rectangle с четырьмя «настоящими» свойствами — left, top, width, и height и двумя логическими свойствами в виде геттеров и сеттеров — right и bottom. При использовании класса нет никакой видимой разницы между натуральными полями и геттерами и сеттерами:

var rect = new Rectangle(3, 4, 20, 15);
print(rect.left);
print(rect.bottom);
rect.top = 6;
rect.right = 12;

Стирание границы между полями и геттерами/сеттерами — одно из фундаментальных свойств языка. Лучше всего думать о полях именно как о наборе “магических” геттеров и сеттеров. Из этого следует, что вы вполне можете переопределить унаследованный геттер натуральным полем и наоборот. Если в интерфейсе требуется геттер, в реализации вы можете просто задать поле с таким же именем и типом. Если поле изменяемое (не final), можно написать требуемый интерфейсом сеттер.

На практике это означает, что нет никакой необходимости тщательно изолировать поля класса кучей геттеров и сеттеров, как в Java или C#. Смело объявляйте публичные свойства. Если вы хотите предотвратить их модификацию, используёте ключевое слово final.

Позже, если появится необходимость делать валидацию или что-то ещё в этом роде, вы всегда сможете заменить это поле геттером и сеттером. Например, мы хотим, чтобы наш класс Rectangle всегда имел неотрицательный размер:

class Rectangle {
  num left, top;
  num _width, _height;

  num get width() => _width;
  set width(num value) {
    if (value < 0) throw 'Width cannot be negative.';
    _width = value;
  }

  num get height() => _height;
  set height(num value) {
    if (value < 0) throw 'Height cannot be negative.';
    _height = value;
  }

  num get right()           => left + width;
      set right(num value)  => left = value - width;
  num get bottom()          => top + height;
      set bottom(num value) => top = value - height;

  Rectangle(this.left, this.top, this._width, this._height);
}

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

Определения верхнего уровня


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

num abs(num value) => value < 0 ? -value : value;

final TWO_PI = Math.PI * 2.0;

int get today() {
  final date = new DateTime.now();
  return date.day;
}

Даже в языках, которые не требуют помещать всё внурь классов или объектов, вроде JavaScript, принято делать так чтобы избежать конфликтов имен: глобальные определения в разных местах могут вступать в коллизии. Чтобы справиться с этим, в Dart есть система библиотек, которая позволяет импортировать определения из других файлов, добавляя к ним префиксы, чтобы избежать неоднозначности. Так что нет необходимости прятать определения внутрь классов.

Мы всё ещё исследуем, как эта особенность может повлиять на способ написания библиотек. Большая часть нашего кода помещает определения внутрь классов, например Math. Трудно сказать что это — укоренившаяся привычка из других языков, или полезная и для Dart практика программирования. В этой области нам очень нужна обратная связь от других разработчиков.

У нас есть несколько примеров использования определений верхнего уровня. Прежде всего — это main(). При работе с DOM, “переменные” document и window — это геттеры, определенные на верхнем уровне.

Строки и интерполяция


В Dart есть несколько видов строковых литералов. Вы можете использовать двойные и одинарные кавычки, а также тройные кавычки для многострочных литералов:

'I am a "string"'
"I'm one too"

'''I'm
on multiple lines
'''

"""
As
am
I
"""

Чтобы объединить несколько строк в одну, вы можете использовать конкатенацию:

var name = 'Fred';
var salutation = 'Hi';
var greeting = salutation + ', ' + name;

Но интерполяция будет чище и быстрее:

var name = 'Fred';
var salutation = 'Hi';
var greeting = '$salutation, $name';

На место знака доллара ($), после которого идет имя переменной, будет подставлено значение переменной (если это не строка, будет вызван метод toString()). Внутрь фигурных скобок можно помещать выражения:

var r = 2;
print('The area of a circle with radius $r is ${Math.PI * r * r}');

Операторы


Dart использует те же операторы, с теми же приоритетами, что и C, Java и другие подобные языки. Они будут вести себя так, как вы ожидаете. Тем не менее, внутренняя реализация имеет свои особенности. В Dart выражение с оператором вида 1 + 2 — просто синтаксический сахар для вызова метода. С точки зрения языка этот пример выглядит, как 1.+(2).

Это значит, что вы можете перегружать большинство операторов, создавая свои типы. Вот например, класс Vector:

class Vector {
  num x, y;
  Vector(this.x, this.y);
  operator +(Vector other) => new Vector(x + other.x, y + other.y);
}


Теперь мы можем складывать векторы, используя привычный синтаксис:

var position = new Vector(3, 4);
var velocity = new Vector(1, 2);
var newPosition = position + velocity;


Тем не менее, не стоит этой возможностью злоупотреблять. Мы даём вам ключи от машины в надежде, что вы не будете таранить столбы.

На практике, если типы, которые вы определяете, используют операторы в реальном мире, это хороший признак того, что стоит переопределять для них стандартные операторы: комплексные числа, векторы, матрицы и т.д. Во всех остальных случаях перегружать операторы, скорее всего, не стоит. Типы с перегружаемыми операторами обычно должны быть неизменяемыми.

Стоит отметить, что, так как операторы — это вызовы методов, им изначально присуща асимметрия. Поиск метода всегда делается для левого аргумента. Так что, когда вы пишете a + b, смысл операции зависит от типа a.

Равенство


Этому набору операторов стоит уделить особое внимание. В Dart есть две пары операторов равенства: == и != и === и !==. Выглядит знакомо для программистов JavaScript, но здесь они работают немного по-другому.

== и != служат для проверки на эквивалентность. 99% времени вы будете использовать именно их. В отличие от JavaScript, они не делают никаких неявных преобразований, так что они будут вести себя более предсказуемо. Не бойтесь использовать их! В отличие от Java, они работают с любыми типами, для которых определено отношение эквивалентности. Никаких больше someString.equals("something").

Вы можете перегружать == для своих типов, если это будет иметь смысл. При этом нет необходимости перегружать !=, Dart автоматически выведет его из ==.

Вторая пара операторов, === и !==, служит для проверки на идентичность. a === b вернет true только если a и b — один и тот же объект в памяти. Вам редко придется использовать их на практике. По-умолчанию, == полагается на ===, если для типа не определено отношение эквивалентности, так что === будет необходим в единственном случае — чтобы обойти переопределенный пользователем ==.
Tags:
Hubs:
+61
Comments 100
Comments Comments 100

Articles