Сгенерированный код для Dart

Описание того, какой код на Dart генерирует компилятор protocol buffer для любого заданного определения протокола.

Различия между сгенерированным кодом для proto2, proto3 и editions выделены - обратите внимание, что эти различия находятся в сгенерированном коде, как описано в этом документе, а не в базовом API, который одинаков в обеих версиях. Вам следует прочитать руководство по языку proto2, руководство по языку proto3 или руководство по языку editions перед чтением этого документа.

Вызов компилятора

Компилятору protocol buffer требуется плагин для генерации Dart кода. Его установка в соответствии с инструкциями предоставляет бинарный файл protoc-gen-dart, который protoc использует при вызове с флагом командной строки --dart_out. Флаг --dart_out указывает компилятору, куда записывать исходные файлы Dart. Для входного файла .proto компилятор создает среди прочих файл .pb.dart.

Имя файла .pb.dart вычисляется путем взятия имени файла .proto и внесения двух изменений:

  • Расширение (.proto) заменяется на .pb.dart. Например, файл с именем foo.proto приводит к созданию выходного файла с именем foo.pb.dart.
  • Путь proto (указанный с помощью флага командной строки --proto_path или -I) заменяется на выходной путь (указанный с помощью флага --dart_out).

Например, при вызове компилятора следующим образом:

protoc --proto_path=src --dart_out=build/gen src/foo.proto src/bar/baz.proto

компилятор прочитает файлы src/foo.proto и src/bar/baz.proto. Он создаст: build/gen/foo.pb.dart и build/gen/bar/baz.pb.dart. Компилятор автоматически создаст директорию build/gen/bar, если это необходимо, но он не создаст build или build/gen; они должны уже существовать.

Сообщения

Для простого объявления сообщения:

message Foo {}

Компилятор protocol buffer генерирует класс с именем Foo, который расширяет класс GeneratedMessage.

Класс GeneratedMessage определяет методы, которые позволяют проверять, манипулировать, читать или записывать все сообщение. В дополнение к этим методам, класс Foo определяет следующие методы и конструкторы:

  • Foo(): Конструктор по умолчанию. Создает экземпляр, в котором все единичные поля не установлены, а повторяющиеся поля пусты.
  • Foo.fromBuffer(...): Создает Foo из сериализованных данных protocol buffer, представляющих сообщение.
  • Foo.fromJson(...): Создает Foo из строки JSON, кодирующей сообщение.
  • Foo clone(): Создает глубокую копию полей в сообщении.
  • Foo copyWith(void Function(Foo) updates): Создает записываемую копию этого сообщения, применяет к ней updates и помечает копию доступной только для чтения перед возвратом.
  • static Foo create(): Фабричная функция для создания единичного Foo.
  • static PbList<Foo> createRepeated(): Фабричная функция для создания List, реализующего изменяемое повторяющееся поле элементов Foo.
  • static Foo getDefault(): Возвращает singleton-экземпляр Foo, который идентичен вновь созданному экземпляру Foo (так что все единичные поля не установлены, а все повторяющиеся поля пусты).

Вложенные типы

Сообщение может быть объявлено внутри другого сообщения. Например:

message Foo {
  message Bar {
  }
}

В этом случае компилятор генерирует два класса: Foo и Foo_Bar.

Поля

В дополнение к методам, описанным в предыдущем разделе, компилятор protocol buffer генерирует методы доступа для каждого поля, определенного в сообщении в файле .proto.

Обратите внимание, что сгенерированные имена всегда используют верблюжий регистр (camelCase), даже если имя поля в файле .proto использует нижний регистр с подчеркиваниями (как и должно быть). Преобразование регистра работает следующим образом:

  1. Для каждого подчеркивания в имени подчеркивание удаляется, а следующая буква capitalizes.
  2. Если к имени будет присоединен префикс (например, "has"), первая буква capitalizes. В противном случае она приводится к нижнему регистру.

Таким образом, для поля foo_bar_baz геттер становится get fooBarBaz, а метод с префиксом has будет hasFooBarBaz.

Единичные примитивные поля

Все поля имеют явное присутствие в реализации на Dart.

Для следующего определения поля:

int32 foo = 1;

Компилятор сгенерирует следующие методы доступа в классе сообщения:

  • int get foo: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.

  • bool hasFoo(): Возвращает true, если поле установлено.

  • set foo(int value): Устанавливает значение поля. После вызова hasFoo() будет возвращать true, а get foo будет возвращать value.

  • void clearFoo(): Очищает значение поля. После вызова hasFoo() будет возвращать false, а get foo будет возвращать значение по умолчанию.

    {{% alert title="Примечание" color="note" %}} Из-за особенности в реализации Dart proto3 следующие методы генерируются даже если настроено неявное присутствие.{{% /alert %}}

  • bool hasFoo(): Возвращает true, если поле установлено.

    {{% alert title="Примечание" color="note" %}} Этому значению нельзя действительно доверять, если proto был сериализован на другом языке, который поддерживает неявное присутствие (например, Java). Несмотря на то, что Dart отслеживает присутствие, другие языки этого не делают, и циклическая обработка поля с неявным присутствием и нулевым значением приведет к его "исчезновению" с точки зрения Dart. {{% /alert %}}

  • void clearFoo(): Очищает значение поля. После вызова hasFoo() будет возвращать false, а get foo будет возвращать значение по умолчанию.

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

Единичные поля сообщений

Для данного типа сообщения:

message Bar {}

Для сообщения с полем Bar:

// proto2
message Baz {
  optional Bar bar = 1;
  // Сгенерированный код будет тем же результатом, если используется required вместо optional.
}

// proto3 и editions
message Baz {
  Bar bar = 1;
}

Компилятор сгенерирует следующие методы доступа в классе сообщения:

  • Bar get bar: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.
  • set bar(Bar value): Устанавливает значение поля. После вызова hasBar() будет возвращать true, а get bar будет возвращать value.
  • bool hasBar(): Возвращает true, если поле установлено.
  • void clearBar(): Очищает значение поля. После вызова hasBar() будет возвращать false, а get bar будет возвращать значение по умолчанию.
  • Bar ensureBar(): Устанавливает bar в пустой экземпляр, если hasBar() возвращает false, а затем возвращает значение bar. После вызова hasBar() будет возвращать true.

Повторяющиеся поля

Для этого определения поля:

repeated int32 foo = 1;

Компилятор сгенерирует:

  • List<int> get foo: Возвращает список, лежащий в основе поля. Если поле не установлено, возвращает пустой список. Изменения в списке отражаются в поле.

Поля Int64

Для этого определения поля:

int64 bar = 1;

Компилятор сгенерирует:

  • Int64 get bar: Возвращает объект Int64, содержащий значение поля.

Обратите внимание, что Int64 не встроен в основные библиотеки Dart. Для работы с этими объектами вам может потребоваться импортировать библиотеку Dart fixnum:

import 'package:fixnum/fixnum.dart';

Поля Map

Для определения поля map like this:

map<int32, int32> map_field = 1;

Компилятор сгенерирует следующий геттер:

  • Map<int, int> get mapField: Возвращает Dart map, лежащий в основе поля. Если поле не установлено, возвращает пустой map. Изменения в map отражаются в поле.

Any

Для поля Any like this:

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  google.protobuf.Any details = 2;
}

В нашем сгенерированном коде геттер для поля details возвращает экземпляр com.google.protobuf.Any. Это предоставляет следующие специальные методы для упаковки и распаковки значений Any:

    /// Распаковывает сообщение в [value] в [instance].
    ///
    /// Выбрасывает [InvalidProtocolBufferException], если [typeUrl] не соответствует
    /// типу [instance].
    ///
    /// Типичное использование: `any.unpackInto(new Message())`.
    ///
    /// Возвращает [instance].
    T unpackInto<T extends GeneratedMessage>(T instance,
        {ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY});

    /// Возвращает `true`, если закодированное сообщение соответствует типу [instance].
    ///
    /// Может использоваться с экземпляром по умолчанию:
    /// `any.canUnpackInto(Message.getDefault())`
    bool canUnpackInto(GeneratedMessage instance);

    /// Создает новый [Any], кодирующий [message].
    ///
    /// [typeUrl] будет [typeUrlPrefix]/`fullName`, где `fullName` - это
    /// полное квалифицированное имя типа [message].
    static Any pack(GeneratedMessage message,
        {String typeUrlPrefix = 'type.googleapis.com'});

Oneof

Для определения oneof like this:

message Foo {
  oneof test {
    string name = 1;
    SubMessage sub_message = 2;
  }
}

Компилятор сгенерирует следующий тип перечисления Dart:

 enum Foo_Test { name, subMessage, notSet }

Кроме того, он сгенерирует эти методы:

  • Foo_Test whichTest(): Возвращает перечисление, указывающее, какое поле установлено. Возвращает Foo_Test.notSet, если ни одно из них не установлено.
  • void clearTest(): Очищает значение поля oneof, которое в настоящее время установлено (если есть), и устанавливает случай oneof в Foo_Test.notSet.

Для каждого поля внутри определения oneof генерируются обычные методы доступа к полям. Например, для name:

  • String get name: Возвращает текущее значение поля, если случай oneof - Foo_Test.name. В противном случае возвращает значение по умолчанию.
  • set name(String value): Устанавливает значение поля и устанавливает случай oneof в Foo_Test.name. После вызова get name будет возвращать value, а whichTest() будет возвращать Foo_Test.name.
  • void clearName(): Ничего не изменится, если случай oneof не Foo_Test.name. В противном случае очищает значение поля. После вызова get name будет возвращать значение по умолчанию, а whichTest() будет возвращать Foo_Test.notSet.

Перечисления

Для определения перечисления like:

enum Color {
  COLOR_UNSPECIFIED = 0;
  COLOR_RED = 1;
  COLOR_GREEN = 2;
  COLOR_BLUE = 3;
}

Компилятор protocol buffer сгенерирует класс с именем Color, который расширяет класс ProtobufEnum. Класс будет включать static const Color для каждого из четырех значений, а также static const List<Color>, который содержит значения.

static const List<Color> values = <Color> [
  COLOR_UNSPECIFIED,
  COLOR_RED,
  COLOR_GREEN,
  COLOR_BLUE,
];

Он также будет включать следующий метод:

  • static Color? valueOf(int value): Возвращает Color, соответствующий заданному числовому значению.

Каждое значение будет иметь следующие свойства:

  • name: Имя перечисления, как указано в файле .proto.
  • value: Целочисленное значение перечисления, как указано в файле .proto.

Обратите внимание, что язык .proto позволяет нескольким символам перечисления иметь одно и то же числовое значение. Символы с одинаковым числовым значением являются синонимами. Например:

enum Foo {
  BAR = 0;
  BAZ = 0;
}

В этом случае BAZ является синонимом для BAR и будет определен следующим образом:

static const Foo BAZ = BAR;

Перечисление может быть определено вложенным в тип сообщения. Например, при определении перечисления like:

message Bar {
  enum Color {
    COLOR_UNSPECIFIED = 0;
    COLOR_RED = 1;
    COLOR_GREEN = 2;
    COLOR_BLUE = 3;
  }
}

Компилятор protocol buffer сгенерирует класс с именем Bar, который расширяет GeneratedMessage, и класс с именем Bar_Color, который расширяет ProtobufEnum.

Расширения (недоступно в proto3)

Для файла foo_test.proto, включающего сообщение с диапазоном расширений и определением расширения верхнего уровня:

message Foo {
  extensions 100 to 199;
}

extend Foo {
  optional int32 bar = 101;
}

Компилятор protocol buffer сгенерирует, в дополнение к классу Foo, класс Foo_test, который будет содержать static Extension для каждого поля расширения в файле вместе с методом для регистрации всех расширений в ExtensionRegistry:

  • static final Extension bar
  • static void registerAllExtensions(ExtensionRegistry registry): Регистрирует все определенные расширения в данном реестре.

Методы доступа расширений Foo могут использоваться следующим образом:

Foo foo = Foo();
foo.setExtension(Foo_test.bar, 1);
assert(foo.hasExtension(Foo_test.bar));
assert(foo.getExtension(Foo_test.bar)) == 1);

Расширения также могут быть объявлены вложенными внутри другого сообщения:

message Baz {
  extend Foo {
    int32 bar = 124;
  }
}

В этом случае расширение bar вместо этого объявляется как статический член класса Baz.

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

ExtensionRegistry registry = ExtensionRegistry();
registry.add(Baz.bar);
Foo foo = Foo.fromBuffer(input, registry);

Если у вас уже есть разобранное сообщение с неизвестными полями, вы можете использовать reparseMessage в ExtensionRegistry для повторного разбора сообщения. Если набор неизвестных полей содержит расширения, присутствующие в реестре, эти расширения разбираются и удаляются из набора неизвестных полей. Расширения, уже присутствующие в сообщении, сохраняются.

Foo foo = Foo.fromBuffer(input);
ExtensionRegistry registry = ExtensionRegistry();
registry.add(Baz.bar);
Foo reparsed = registry.reparseMessage(foo);

Имейте в виду, что этот метод извлечения расширений в целом более затратен. По возможности мы рекомендуем использовать ExtensionRegistry со всеми необходимыми расширениями при выполнении GeneratedMessage.fromBuffer.

Сервисы

Для определения сервиса:

service Foo {
  rpc Bar(FooRequest) returns(FooResponse);
}

Компилятор protocol buffer может быть вызван с опцией `grpc` (например, --dart_out=grpc:output_folder), в этом случае он сгенерирует код для поддержки gRPC. Смотрите руководство по быстрому старту gRPC Dart для более подробной информации.