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

Подтип в Аде — это «тип вместе со связанным ограничением», т. е. подмножество значений существующего типа. Пример -

subtype Small_Int is Integer range -10 .. 10;

Это объявляет тип с именем Small_Int, который является подтипом Integer с теми же операциями, что и Integer, но ограничен диапазоном значений от -10 до 10. Любая попытка присвоить этому типу значение, выходящее за пределы диапазона ограничения, вызовет ошибку Ada. время выполнения, чтобы вызвать ошибку ограничения (форма исключения), которую нельзя игнорировать, а только обрабатывать. Тип используется для наилучшего отражения сохраняемых значений. Итак, какие преимущества это дает нам -

  • Любой человек, читающий код, будет лучше понимать назначение переменной.
  • Улучшенные возможности проверки ошибок
  • Легче доказать свойства программ с более ограниченными типами

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

Итак, можем ли мы сделать это в Dart? Ну да, мы не можем применить это во время выполнения так же, как это делает Ada, но Dart достаточно гибок, чтобы с помощью комбинации расширения класса и перегрузки операторов мы могли смоделировать работающую структуру подтипов. Для целей этой статьи рассматриваются только целые числа, однако код, представленный ниже, легко сделать универсальным.
Полный код для этой статьи и набор модульных тестов можно найти здесь.

Во-первых, создайте абстрактный класс подтипа, из которого могут быть получены более конкретные типы:

abstract class Subtype {  
  Subtype.set(this.start, this.end, {int initialValue = 0}) {  
    _value = initialValue;  
  }  
  final int end;  
  final int start;  
  int _value = 0;  
  int get value => _value;  
  void call(int val) => _value = _boundsCheck(val);  
  
  @override  
  String toString() => _value.toString();
  ...........

Это позволяет нам устанавливать ограничения для наших подтипов и содержит приватный метод boundsCheck для проверки этих ограничений. Если проверка не пройдена, возникает ошибка ArgumentError. Теперь мы можем добавить оператор равенства и оператор + -

@override  
bool operator ==(Object other) {  
  if (other is int) {  
    if (_value == other) {  
      return true;  
    }  
  }  
  if (other is Subtype) {  
    if (_value == other.value) {  
      return true;  
    }  
  }  
  return false;  
}  
  
dynamic operator +(Object other) {  
  if (other is int) {  
    _boundsCheck(_value += other);  
  } else {  
    try {  
      _boundsCheck(_value += (other as Subtype).value);  
    } on ArgumentError {  
      rethrow;  
    } catch (e) {  
      throw ArgumentError(  
          'AdaTypes:Subtype - the type supplied is not derived from Subtype - $e',  
          'other');  
    }  
  }  
  return this;  
}

Для равенства мы разрешаем сравнение с целыми числами, а также с другими подтипами, аналогично для добавления мы разрешаем добавление целых чисел и других типов, производных от подтипа (это может быть дополнительно ограничено, см. ниже). Обратите внимание, что тип возвращаемого значения оператора сложения является динамическим, что позволяет использовать операции по умолчанию для производных типов. Итак, как мы можем это использовать? Один из способов — создать подтипы общего назначения и использовать псевдонимы типов для удобства чтения.

class ADozen extends Subtype {  
  ADozen() : super.set(0, 12);  
}  
  
class AScore extends Subtype {  
  AScore() : super.set(0, 20);  
}  
  
typedef EggsInBox = ADozen;  
typedef PeasInPod = AScore;

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

final numEggs1 = EggsInBox();  
numEggs1(3);  
final numEggs2 = EggsInBox();  
numEggs2(4);  
final numPeas = PeasInPod();  
numPeas(15);

Дополнение теперь просто делается -

numEggs1 += numEggs2;

зная, что ограничения подтипа не могут быть нарушены. Аналогично можно добавить все остальные необходимые операторы.

Пока все хорошо, но реализация до сих пор также позволяет это -

numEggs += numPeas;

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

Subtype.set(this.start, this.end,  
    {int initialValue = 0, strictTyping = false}) {  
  _value = initialValue;  
  _strictTyping = strictTyping;  
}  
  
final int end;  
final int start;  
int _value = 0;  
int get value => _value;  
bool _strictTyping = false;  
bool _initialised = false;

поэтому оператор сложения теперь становится -

dynamic operator +(Object other) {  
  if (other is int) {  
    _operationValid(_value += other);  
  } else if (_strictTyping) {  
    final tThis = runtimeType;  
    final tOther = other.runtimeType;  
    if (tThis != tOther) {  
      throw ArgumentError(  
          'AdaTypes:Subtype - strict typing - the type supplied is different from the subject type',  
          'other');  
    } else {  
      _operationValid(_value += (other as Subtype).value);  
    }  
  } else {  
    _tryAssignment(other);  
  }  
  return this;  
}

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

Как видно, создание полностью готового к производству механизма подтипирования с соответствующим набором модульных тестов нетривиально и подойдет не всем. Однако после реализации это можно повторно использовать во многих ваших пакетах и ​​приложениях.

Настоящим преимуществом здесь является не только код, но и привитие сомневающегося мышления в глазах читателя. Если теперь вы посмотрите на свой код по-новому и зададитесь вопросами вроде «Действительно ли этой переменной нужен полный диапазон значений int?» Я знаю, что в этом приложении это может быть только 0…10, что произойдет, если ему будет присвоено значение 20 или отрицательное значение?» Тогда вы на пути к созданию еще более безопасного кода, чем позволяет стандартный Dart.

Повышение уровня кодирования

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

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу