Язык Dart: когда Java и C# недостаточно четкие
Опубликовано: 2022-03-11Еще в 2013 году официальная версия Dart 1.0 привлекла внимание прессы, как и большинство предложений Google, но не все так стремились, как внутренние команды Google, создавать критически важные для бизнеса приложения на языке Dart. Хорошо продуманной перестройкой Dart 2 пятью годами позже Google, похоже, доказала свою приверженность этому языку. Действительно, сегодня он продолжает набирать популярность среди разработчиков, особенно среди ветеранов Java и C#.
Язык программирования Dart важен по нескольким причинам:
- Он сочетает в себе лучшее из обоих миров: это скомпилированный, типобезопасный язык (например, C# и Java) и язык сценариев (например, Python и JavaScript) одновременно.
- Он преобразуется в JavaScript для использования в качестве веб-интерфейса.
- Он работает на всем и компилируется в собственные мобильные приложения, поэтому вы можете использовать его практически для чего угодно.
- Dart похож на C# и Java по синтаксису, поэтому его легко освоить.
Те из нас, кто знаком с миром C# или Java крупных корпоративных систем, уже знают, почему важна безопасность типов, ошибки времени компиляции и линтеры. Многие из нас не решаются принять «сценарный» язык, опасаясь потерять всю структуру, скорость, точность и возможность отладки, к которым мы привыкли.
Но с разработкой Dart нам не нужно отказываться ни от чего из этого. Мы можем написать мобильное приложение, веб-клиент и серверную часть на одном языке — и получить все то, что нам до сих пор нравится в Java и C#!
С этой целью давайте рассмотрим некоторые ключевые примеры языка Dart, которые будут новыми для разработчика C# или Java, которые мы суммируем в PDF-файле языка Dart в конце.
Примечание. В этой статье рассматривается только Dart 2.x. Версия 1.x не была «полностью готовой» — в частности, система типов была рекомендательной (как TypeScript), а не обязательной (как C# или Java).
1. Организация кода
Во-первых, мы рассмотрим одно из наиболее существенных отличий: как организованы файлы кода и как на них ссылаются.
Исходные файлы, область действия, пространства имен и импорт
В C# коллекция классов компилируется в сборку. У каждого класса есть пространство имен, и часто пространства имен отражают организацию исходного кода в файловой системе, но, в конце концов, сборка не сохраняет никакой информации о расположении файла исходного кода.
В Java исходные файлы являются частью пакета, а пространства имен обычно соответствуют местоположению в файловой системе, но, в конце концов, пакет — это просто набор классов.
Таким образом, у обоих языков есть способ сделать исходный код несколько независимым от файловой системы.
Напротив, в языке Dart каждый исходный файл должен импортировать все, на что он ссылается, включая другие ваши исходные файлы и сторонние пакеты. Точно так же нет пространств имен, и вы часто обращаетесь к файлам через их местоположение в файловой системе. Переменные и функции могут быть верхнего уровня, а не только классы. В этом смысле Dart больше похож на сценарий.
Поэтому вам нужно будет изменить свое мышление с «набора классов» на что-то более похожее на «последовательность включенных файлов кода».
Dart поддерживает как пакетную организацию, так и специальную организацию без пакетов. Начнем с примера без пакетов, чтобы проиллюстрировать последовательность подключаемых файлов:
// file1.dart int alice = 1; // top level variable int barry() => 2; // top level function var student = Charlie(); // top level variable; Charlie is declared below but that's OK class Charlie { ... } // top level class // alice = 2; // top level statement not allowed // file2.dart import 'file1.dart'; // causes all of file1 to be in scope main() { print(alice); // 1 }
Все, на что вы ссылаетесь в исходном файле, должно быть объявлено или импортировано в этом файле, поскольку нет уровня «проекта» и нет другого способа включить в область действия другие исходные элементы.
Единственное использование пространств имен в Dart — дать импорту имя, и это влияет на то, как вы ссылаетесь на импортированный код из этого файла.
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
Пакеты
В приведенных выше примерах код организован без пакетов. Чтобы использовать пакеты, код организуется более конкретным образом. Вот пример макета пакета для пакета с именем apples
:
-
apples/
-
pubspec.yaml
— определяет имя пакета, зависимости и некоторые другие вещи. -
lib/
-
apples.dart
импорт и экспорт; это файл, импортированный любыми потребителями пакета -
src/
-
seeds.dart
— весь остальной код здесь
-
-
-
bin/
-
runapples.dart
— содержит основную функцию, которая является точкой входа (если это исполняемый пакет или включает запускаемые инструменты).
-
-
Затем вы можете импортировать целые пакеты вместо отдельных файлов:
import 'package:apples';
Нетривиальные приложения всегда должны быть организованы в виде пакетов. Это избавляет от необходимости повторять пути файловой системы в каждом ссылающемся файле; плюс бегают быстрее. Это также позволяет легко поделиться вашим пакетом на pub.dev, где другие разработчики могут очень легко получить его для собственного использования. Пакеты, используемые вашим приложением, вызовут копирование исходного кода в вашу файловую систему, так что вы сможете отлаживать эти пакеты так глубоко, как пожелаете.
2. Типы данных
В системе типов Dart есть существенные различия, о которых следует знать в отношении нулей, числовых типов, коллекций и динамических типов.
Нули везде
Приходя из C# или Java, мы привыкли к примитивным или значимым типам в отличие от ссылочных или объектных типов. Типы значений на практике размещаются в стеке или в регистрах, а копии значения отправляются как параметры функции. Вместо этого ссылочные типы размещаются в куче, и в качестве параметров функции отправляются только указатели на объект. Поскольку типы значений всегда занимают память, переменная типа значения не может быть нулевой, а все элементы типа значения должны иметь инициализированные значения.
Дарт устраняет это различие, потому что все является объектом; все типы в конечном счете происходят от типа Object
. Итак, это законно:
int i = null;
На самом деле все примитивы неявно инициализируются значением null
. Это означает, что вы не можете предполагать, что значения целых чисел по умолчанию равны нулю, как вы привыкли в C# или Java, и вам может потребоваться добавить проверки на нуль.
Интересно, что даже Null
— это тип, а слово null
относится к экземпляру Null
:
print(null.runtimeType); // prints Null
Не так много числовых типов
В отличие от знакомого ассортимента целочисленных типов от 8 до 64 бит со знаком и без знака, основным целочисленным типом Dart является просто int
, 64-битное значение. (Есть также BigInt
для очень больших чисел.)
Поскольку в синтаксисе языка нет массива байтов, содержимое двоичного файла можно обрабатывать как списки целых чисел, т.е. List<Int>
.
Если вы думаете, что это должно быть ужасно неэффективно, дизайнеры уже подумали об этом. На практике существуют разные внутренние представления в зависимости от фактического целочисленного значения, используемого во время выполнения. Среда выполнения не выделяет память кучи для объекта int
, если она может оптимизировать ее и использовать регистр ЦП в неупакованном режиме. Кроме того, библиотека byte_data
предлагает UInt8List
и некоторые другие оптимизированные представления.
Коллекции
Коллекции и дженерики очень похожи на то, к чему мы привыкли. Главное, что следует отметить, это отсутствие массивов фиксированного размера: просто используйте тип данных List
везде, где бы вы использовали массив.
Кроме того, имеется синтаксическая поддержка для инициализации трех типов коллекций:
final a = [1, 2, 3]; // inferred type is List<int>, an array-like ordered collection final b = {1, 2, 3}; // inferred type is Set<int>, an unordered collection final c = {'a': 1, 'b': 2}; // inferred type is Map<string, int>, an unordered collection of name-value pairs
Итак, используйте Dart List
там, где вы использовали бы массив Java, ArrayList
или Vector
; или массив C# или List
. Используйте Set
там, где вы использовали бы HashSet
Java/C#. Используйте Map
там, где вы использовали бы Java HashMap
или C# Dictionary
.
3. Динамическая и статическая типизация
В динамических языках, таких как JavaScript, Ruby и Python, вы можете ссылаться на элементы, даже если они не существуют. Вот пример JavaScript:
var person = {}; // create an empty object person.name = 'alice'; // add a member to the object if (person.age < 21) { // refer to a property that is not in the object // ... }
Если вы запустите это, person.age
будет undefined
, но он все равно запустится.
Точно так же вы можете изменить тип переменной в JavaScript:
var a = 1; // a is a number a = 'one'; // a is now a string
Напротив, в Java вы не можете написать код, подобный приведенному выше, потому что компилятору необходимо знать тип, и он проверяет, что все операции допустимы, даже если вы используете ключевое слово var:
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java позволяет кодировать только статические типы. (Вы можете использовать интроспекцию для динамического поведения, но это не является прямой частью синтаксиса.) JavaScript и некоторые другие чисто динамические языки позволяют кодировать только динамические типы.
Язык Dart позволяет:
// dart dynamic a = 1; // a is an int - dynamic typing a = 'one'; // a is now a string a.foo(); // we can call a function on a dynamic object, to be resolved at run time var b = 1; // b is an int - static typing // b = 'one'; // not allowed in Dart
Dart имеет dynamic
псевдотипа, которая заставляет всю логику типов обрабатываться во время выполнения. Попытка вызвать a.foo()
не помешает статическому анализатору, и код запустится, но во время выполнения произойдет сбой, потому что такого метода нет.
C# изначально был похож на Java, а позже была добавлена динамическая поддержка, так что Dart и C# в этом отношении примерно одинаковы.

4. Функции
Синтаксис объявления функции
Синтаксис функций в Dart немного проще и веселее, чем в C# или Java. Синтаксис может быть любым из следующих:
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
Например:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
Передача параметров
Поскольку все является объектом, включая такие примитивы, как int
и String
, передача параметров может сбивать с толку. Хотя здесь нет передачи параметра ref
, как в C#, все передается по ссылке, и функция не может изменить ссылку вызывающей стороны. Поскольку объекты не клонируются при передаче в функции, функция может изменить свойства объекта. Однако это различие для таких примитивов, как int и String, фактически спорно, поскольку эти типы неизменяемы.
var id = 1; var name = 'alice'; var client = Client(); void foo(int id, String name, Client client) { id = 2; // local var points to different int instance name = 'bob'; // local var points to different String instance client.State = 'AK'; // property of caller's object is changed } foo(id, name, client); // id == 1, name == 'alice', client.State == 'AK'
Дополнительные параметры
Если вы знакомы с миром C# или Java, вы, вероятно, не раз ругались в ситуациях с перегруженными методами, вызывающими путаницу, вроде этих:
// java void foo(string arg1) {...} void foo(int arg1, string arg2) {...} void foo(string arg1, Client arg2) {...} // call site: foo(clientId, input3); // confusing! too easy to misread which overload it is calling
Или с необязательными параметрами C# возникает другая путаница:
// c# void Foo(string arg1, int arg2 = 0) {...} void Foo(string arg1, int arg3 = 0, int arg2 = 0) {...} // call site: Foo("alice", 7); // legal but confusing! too easy to misread which overload it is calling and which parameter binds to argument 7 Foo("alice", arg2: 9); // better
C# не требует именования необязательных аргументов на сайтах вызовов, поэтому методы рефакторинга с необязательными параметрами могут быть опасны. Если некоторые сайты вызовов окажутся легальными после рефакторинга, компилятор их не обнаружит.
У Dart есть более безопасный и очень гибкий способ. Во-первых, не поддерживаются перегруженные методы. Вместо этого есть два способа обработки необязательных параметров:
// positional optional parameters void foo(string arg1, [int arg2 = 0, int arg3 = 0]) {...} // call site for positional optional parameters foo('alice'); // legal foo('alice', 12); // legal foo('alice', 12, 13); // legal // named optional parameters void bar(string arg1, {int arg2 = 0, int arg3 = 0}) {...} bar('alice'); // legal bar('alice', arg3: 12); // legal bar('alice', arg3: 12, arg2: 13); // legal; sequence can vary and names are required
Вы не можете использовать оба стиля в одном объявлении функции.
async
позиция ключевого слова
C# имеет запутанную позицию для своего ключевого слова async
:
Task<int> Foo() {...} async Task<int> Foo() {...}
Это означает, что сигнатура функции является асинхронной, но на самом деле асинхронной является только реализация функции. Любая из приведенных выше сигнатур будет допустимой реализацией этого интерфейса:
interface ICanFoo { Task<int> Foo(); }
В языке Dart async
находится в более логичном месте, обозначая асинхронность реализации:
Future<int> foo() async {...}
Область действия и замыкания
Подобно C# и Java, Dart имеет лексическую область видимости. Это означает, что переменная, объявленная в блоке, выходит за пределы области видимости в конце блока. Таким образом, Dart обрабатывает замыкания таким же образом.
Синтаксис свойства
Java популяризировала шаблон получения/установки свойства, но в языке нет специального синтаксиса для него:
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C# имеет для этого синтаксис:
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart имеет немного другой синтаксис, поддерживающий свойства:
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. Конструкторы
Конструкторы Dart обладают большей гибкостью, чем в C# или Java. Одна приятная особенность — возможность называть разные конструкторы в одном классе:
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
Вы можете вызвать конструктор по умолчанию, используя только имя класса: var c = Client();
Существует два вида сокращений для инициализации членов экземпляра перед вызовом тела конструктора:
class Client { String _code; String _name; Client(String this._name) // "this" shorthand for assigning parameter to instance member : _code = _name.toUpper() { // special out-of-body place for initializing // body } }
Конструкторы могут запускать конструкторы суперкласса и перенаправлять на другие конструкторы того же класса:
Foo.constructor1(int x) : this(x); // redirect to the default ctor in same class; no body allowed Foo.constructor2(int x) : super.plain(x) {...} // call base class named ctor, then run this body Foo.constructor3(int x) : _b = x + 1 : super.plain(x) {...} // initialize _b, then call base class ctor, then run this body
Конструкторы, которые вызывают другие конструкторы в том же классе в Java и C#, могут сбивать с толку, если у них обоих есть реализации. В Dart ограничение, заключающееся в том, что конструкторы перенаправления не могут иметь тела, вынуждает программиста делать уровни конструкторов более четкими.
Также есть ключевое слово factory
, которое позволяет использовать функцию как конструктор, но реализация представляет собой обычную функцию. Вы можете использовать его для возврата кэшированного экземпляра или экземпляра производного типа:
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6. Модификаторы
В Java и C# у нас есть модификаторы доступа, такие как private
, protected
и public
. В Dart это радикально упрощено: если имя члена начинается с подчеркивания, оно видно везде внутри пакета (в том числе из других классов) и скрыто от внешних вызывающих сторон; иначе его видно отовсюду. Нет таких ключевых слов, как private
, чтобы обозначить видимость.
Другой вид модификаторов управляет изменчивостью: ключевые слова final
и const
предназначены для этой цели, но они означают разные вещи:
var a = 1; // a is variable, and can be reassigned later final b = a + 1; // b is a runtime constant, and can only be assigned once const c = 3; // c is a compile-time constant // const d = a + 2; // not allowed because a+2 cannot be resolved at compile time
7. Иерархия классов
Язык Dart поддерживает интерфейсы, классы и разновидность множественного наследования. Однако ключевого слова interface
нет; вместо этого все классы также являются интерфейсами, поэтому вы можете определить abstract
класс, а затем реализовать его:
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
Множественное наследование выполняется с основной родословной с использованием ключевого слова extends
и другими классами с использованием ключевого слова with
:
class Employee extends Person with Salaried implements HasDesk {...}
В этом объявлении класс Employee
является производным от Person
и Salaried
, но Person
является основным суперклассом, а Salaried
— миксином (вторичным суперклассом).
8. Операторы
Есть несколько забавных и полезных операторов Dart, к которым мы не привыкли.
Каскады позволяют вам использовать шаблон цепочки для чего угодно:
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
Оператор распространения позволяет обрабатывать коллекцию как список ее элементов в инициализаторе:
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. Нити
Dart не имеет потоков, что позволяет транспилировать его в JavaScript. Вместо этого у него есть «изоляты», которые больше похожи на отдельные процессы в том смысле, что они не могут совместно использовать память. Поскольку многопоточное программирование подвержено ошибкам, эта безопасность рассматривается как одно из преимуществ Dart. Для связи между изолятами вам необходимо передавать данные между ними; полученные объекты копируются в пространство памяти принимающего изолята.
Разработка с помощью языка Dart: вы можете это сделать!
Если вы разработчик C# или Java, то, что вы уже знаете, поможет вам быстро выучить язык Dart, поскольку он был разработан, чтобы быть знакомым. С этой целью мы составили для справки памятку Dart в формате PDF, в которой особое внимание уделено важным отличиям от эквивалентов C# и Java:
Различия, показанные в этой статье, в сочетании с вашими существующими знаниями помогут вам стать продуктивнее в течение первых двух дней использования Dart. Удачного кодирования!