Присутствие полей

Объясняет различные дисциплины отслеживания присутствия для полей protobuf. Также объясняется поведение явного отслеживания присутствия для сингулярных полей proto3 с базовыми типами.

Предпосылки

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

{{% alert title="Примечание" color="note" %}} Мы рекомендуем всегда добавлять метку optional для базовых типов proto3. Это обеспечивает более плавный переход к редакциям (editions), которые по умолчанию используют явное присутствие.{{% /alert %}}

Дисциплины присутствия

Дисциплины присутствия определяют семантику преобразования между представлением API и сериализованным представлением. Дисциплина неявного присутствия полагается на само значение поля для принятия решений во время (де)сериализации, в то время как дисциплина явного присутствия полагается на состояние явного отслеживания.

Присутствие в Потоке "тег-значение" (Бинарный формат) сериализации

Бинарный формат (wire format) — это поток тегированных, саморазделяющихся значений. По определению, бинарный формат представляет последовательность присутствующих значений. Другими словами, каждое значение, найденное в сериализации, представляет присутствующее поле; более того, сериализация не содержит информации об отсутствующих значениях.

Сгенерированный API для proto-сообщения включает определения (де)сериализации, которые преобразуют между типами API и потоком по определению присутствующих пар (тег, значение). Это преобразование разработано для обеспечения прямой и обратной совместимости при изменениях в определении сообщения; однако эта совместимость вносит некоторые (возможно, удивительные) особенности при десериализации сообщений в бинарном формате:

  • При сериализации поля с неявным присутствием не сериализуются, если они содержат значение по умолчанию.
    • Для числовых типов значением по умолчанию является 0.
    • Для перечислений (enum) значением по умолчанию является элемент с нулевым значением.
    • Для строк, байтов и повторяющихся полей значением по умолчанию является значение нулевой длины.
  • "Пустые" значения с длинной-разделителем (например, пустые строки) могут быть корректно представлены в сериализованных значениях: поле "присутствует" в том смысле, что оно появляется в бинарном формате. Однако, если сгенерированный API не отслеживает присутствие, то эти значения могут не быть повторно сериализованы; т.е. пустое поле может стать "отсутствующим" после цикла сериализации-десериализации.
  • При десериализации повторяющиеся значения полей могут обрабатываться по-разному в зависимости от определения поля.
    • Дубликаты repeated полей обычно добавляются к представлению поля в API. (Обратите внимание, что сериализация упакованного повторяющегося поля производит только одно значение с длинной-разделителем в потоке тегов.)
    • Дубликаты optional полей следуют правилу "последнее wins".
  • Поля oneof обеспечивают инвариант на уровне API, что только одно поле установлено в один момент времени. Однако бинарный формат может включать несколько пар (тег, значение), которые концептуально принадлежат oneof. Аналогично optional полям, сгенерированный API следует правилу "последнее wins".
  • Значения вне допустимого диапазона не возвращаются для полей-перечислений в сгенерированных API proto2. Однако значения вне диапазона могут быть сохранены как неизвестные поля в API, даже если тег в бинарном формате был распознан.

Присутствие в форматах Сопоставления по имени поля

Protobuf может быть представлен в удобочитаемых, текстовых форматах. Два известных формата — это TextFormat (формат вывода, производимый сгенерированными методами DebugString сообщений) и JSON.

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

JSON — гораздо более строгий формат, и он не может корректно представить некоторые семантики бинарного формата или TextFormat.

  • В частности, элементы JSON семантически неупорядочены, и каждый элемент должен иметь уникальное имя. Это отличается от правил TextFormat для повторяющихся полей.
  • JSON может включать поля, которые "отсутствуют", в отличие от дисциплины неявного присутствия для других форматов:
    • JSON определяет значение null, которое может использоваться для представления определенного, но отсутствующего поля.
    • Значения повторяющихся полей могут быть включены в форматированный вывод, даже если они равны значению по умолчанию (пустой список).
  • Поскольку элементы JSON неупорядочены, нет способа однозначно интерпретировать правило "последнее wins".
    • В большинстве случаев это нормально: элементы JSON должны иметь уникальные имена: повторяющиеся значения полей не являются допустимым JSON, поэтому их не нужно разрешать, как в TextFormat.
    • Однако это означает, что может быть невозможно однозначно интерпретировать поля oneof: если присутствуют несколько вариантов, они неупорядочены.

Теоретически JSON может представлять присутствие семантически сохраняющим образом. На практике, однако, корректность представления присутствия может варьироваться в зависимости от выбора реализации, особенно если JSON был выбран как средство взаимодействия с клиентами, не использующими protobuf.

Присутствие в API Proto2

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

Тип поляЯвное присутствие
Сингулярное числовое (целое или с плавающей точкой)✔️
Сингулярное перечисление (enum)✔️
Сингулярная строка или bytes✔️
Сингулярное сообщение (message)✔️
Повторяющееся (repeated)
Oneofs✔️
Карты (maps)

Сингулярные поля (всех типов) явно отслеживают присутствие в сгенерированном API. Сгенерированный интерфейс сообщения включает методы для запроса присутствия полей. Например, для поля foo существует соответствующий метод has_foo. (Конкретное имя следует тому же языково-специфичному соглашению об именовании, что и методы доступа к полям.) Эти методы иногда называются "hazzers" внутри реализации protobuf.

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

oneof foo {
  int32 a = 1;
  float b = 2;
}

В зависимости от целевого языка, сгенерированный API обычно включал бы несколько методов:

  • Hazzer для oneof: has_foo
  • Метод варианта oneof: foo_case (или подобный)
  • Hazzers для членов: has_a, has_b
  • Геттеры для членов: a, b

Повторяющиеся поля и карты не отслеживают присутствие: нет различия между пустым и отсутствующим повторяющимся полем.

Присутствие в API Proto3

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

Тип поляoptionalЯвное присутствие
Сингулярное числовое (целое или с плавающей точкой)Нет
Сингулярное числовое (целое или с плавающей точкой)Да✔️
Сингулярное перечисление (enum)Нет
Сингулярное перечисление (enum)Да✔️
Сингулярная строка или bytesНет
Сингулярная строка или bytesДа✔️
Сингулярное сообщение (message)Нет✔️
Сингулярное сообщение (message)Да✔️
Повторяющееся (repeated)Н/П
OneofsН/П✔️
Карты (maps)Н/П

Аналогично API proto2, API proto3 не отслеживает присутствие явно для повторяющихся полей. Без метки optional API proto3 также не отслеживает присутствие для базовых типов (числовые, строки, байты и перечисления). Поля Oneof явно раскрывают присутствие, хотя тот же набор методов hazzer может не генерироваться, как в API proto2.

Это поведение по умолчанию (отсутствие отслеживания присутствия без метки optional) отличается от поведения proto2. Мы рекомендуем использовать метку optional в proto3, если у вас нет конкретной причины не делать этого.

В рамках дисциплины неявного присутствия значение по умолчанию является синонимом "отсутствует" для целей сериализации. Чтобы "очистить" поле (чтобы оно не сериализовалось), пользователь API устанавливает для него значение по умолчанию.

Значение по умолчанию для полей типа enum в рамках неявного присутствия — это соответствующий элемент перечисления со значением 0. Согласно правилам синтаксиса proto3, все типы перечислений обязаны иметь элемент перечисления, который соответствует 0. По соглашению, это UNKNOWN или элемент с похожим именем. Если нулевое значение концептуально находится вне области допустимых значений для приложения, это поведение можно рассматривать как эквивалентное явному присутствию.

Присутствие в API Редакций (Editions)

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

Тип поляЯвное присутствие
Сингулярное числовое (целое или с плавающей точкой)✔️
Сингулярное перечисление (enum)✔️
Сингулярная строка или bytes✔️
Сингулярное сообщение† (message)✔️
Повторяющееся (repeated)
Oneofs†✔️
Карты (maps)

† Сообщения и oneof никогда не имели неявного присутствия, и редакции не позволяют установить field_presence = IMPLICIT.

API на основе редакций отслеживают присутствие полей явно, аналогично proto2, если только для features.field_presence не установлено значение IMPLICIT. Аналогично API proto2, API на основе редакций не отслеживают присутствие явно для повторяющихся полей.

Семантические различия

Дисциплина сериализации неявного присутствия приводит к видимым различиям от дисциплины явного присутствия, когда установлено значение по умолчанию. Для сингулярного поля с числовым, enum или строковым типом:

  • Дисциплина неявного присутствия:
    • Значения по умолчанию не сериализуются.
    • Значения по умолчанию не объединяются-from (при слиянии).
    • Чтобы "очистить" поле, ему устанавливается значение по умолчанию.
    • Значение по умолчанию может означать:
      • поле было явно установлено в значение по умолчанию, которое является допустимым в предметной области значений приложения;
      • поле было концептуально "очищено" установкой значения по умолчанию; или
      • поле никогда не устанавливалось.
    • Методы has_ не генерируются (но см. примечание после этого списка)
  • Дисциплина явного присутствия:
    • Явно установленные значения всегда сериализуются, включая значения по умолчанию.
    • Неустановленные поля никогда не объединяются-from.
    • Явно установленные поля — включая значения по умолчанию — объединяются-from.
    • Сгенерированный метод has_foo указывает, было ли поле foo установлено (и не очищено).
    • Для очистки (т.е. сброса) значения должен использоваться сгенерированный метод clear_foo.

{{% alert title="Примечание" color="note" %}} Методы Has_ в большинстве случаев не генерируются для полей с неявным присутствием. Исключением из этого поведения является Dart, который генерирует методы has_ для файлов схемы proto3.{{% /alert %}}

Соображения по слиянию

В соответствии с правилами неявного присутствия практически невозможно для целевого поля объединить-from его значение по умолчанию (с использованием функций слияния API protobuf). Это происходит потому, что значения по умолчанию пропускаются, аналогично дисциплине сериализации неявного присутствия. Слияние обновляет только целевое (merged-to) сообщение, используя не пропущенные значения из сообщения-обновления (merged-from).

Разница в поведении при слиянии имеет дальнейшие последствия для протоколов, которые полагаются на частичные обновления "patch". Если присутствие поля не отслеживается, то сам по себе патч обновления не может представить обновление до значения по умолчанию, потому что только не-значения по умолчанию объединяются-from.

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

Соображения по совместимости изменений

Изменение поля между явным присутствием и неявным присутствием является бинарно-совместимым изменением для сериализованных значений в бинарном формате. Однако, сериализованное представление сообщения может отличаться в зависимости от того, какая версия определения сообщения использовалась для сериализации. В частности, когда "отправитель" явно устанавливает поле в его значение по умолчанию:

  • Сериализованное значение, следующее дисциплине неявного присутствия, не содержит значения по умолчанию, даже если оно было явно установлено.
  • Сериализованное значение, следующее дисциплине явного присутствия, содержит каждое "присутствующее" поле, даже если оно содержит значение по умолчанию.

Это изменение может быть безопасным или нет, в зависимости от семантики приложения. Например, рассмотрим двух клиентов с разными версиями определения сообщения.

Клиент A использует это определение сообщения, которое следует дисциплине явного присутствия для поля foo:

syntax = "proto3";
message Msg {
  optional int32 foo = 1;
}

Клиент B использует определение того же сообщения, за исключением того, что оно следует дисциплине отсутствия присутствия:

syntax = "proto3";
message Msg {
  int32 foo = 1;
}

Теперь рассмотрим сценарий, в котором клиент A наблюдает за присутствием foo, когда клиенты многократно обмениваются "одним и тем же" сообщением путем десериализации и повторной сериализации:

// Клиент A:
Msg m_a;
m_a.set_foo(1);                  // не-значение по умолчанию
assert(m_a.has_foo());           // OK
Send(m_a.SerializeAsString());   // клиенту B

// Клиент B:
Msg m_b;
m_b.ParseFromString(Receive());  // от клиента A
assert(m_b.foo() == 1);          // OK
Send(m_b.SerializeAsString());   // клиенту A

// Клиент A:
m_a.ParseFromString(Receive());  // от клиента B
assert(m_a.foo() == 1);          // OK
assert(m_a.has_foo());           // OK
m_a.set_foo(0);                  // значение по умолчанию
Send(m_a.SerializeAsString());   // клиенту B

// Клиент B:
Msg m_b;
m_b.ParseFromString(Receive());  // от клиента A
assert(m_b.foo() == 0);          // OK
Send(m_b.SerializeAsString());   // клиенту A

// Клиент A:
m_a.ParseFromString(Receive());  // от клиента B
assert(m_a.foo() == 0);          // OK
assert(m_a.has_foo());           // FAIL (СБОЙ)

Если клиент A зависит от явного присутствия для foo, то "цикл обхода" через клиента B будет потерянным с точки зрения клиента A. В примере это небезопасное изменение: клиент A требует (через assert), чтобы поле присутствовало; даже без каких-либо изменений через API это требование терпит неудачу в зависящем от значения и пира случае.

Как включить Явное присутствие в Proto3

Это общие шаги для использования поддержки отслеживания присутствия полей в proto3:

  1. Добавьте поле optional в файл .proto.
  2. Запустите protoc (как минимум v3.15, или v3.12 с использованием флага --experimental_allow_proto3_optional).
  3. Используйте сгенерированные методы "hazzer" и "clear" в коде приложения, вместо сравнения или установки значений по умолчанию.

Изменения в файле .proto

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

syntax = "proto3";
package example;

message MyMessage {
  // неявное присутствие:
  int32 not_tracked = 1;

  // Явное присутствие:
  optional int32 tracked = 2;
}

Вызов protoc

Отслеживание присутствия для сообщений proto3 включено по умолчанию начиная с v3.15.0, ранее, до v3.12.0, требовался флаг --experimental_allow_proto3_optional при использовании отслеживания присутствия с protoc.

Использование сгенерированного кода

Сгенерированный код для полей proto3 с явным присутствием (метка optional) будет таким же, как если бы он был в файле proto2.

Это определение, используемое в примерах "неявного присутствия" ниже:

syntax = "proto3";
package example;
message Msg {
  int32 foo = 1;
}

Это определение, используемое в примерах "явного присутствия" ниже:

syntax = "proto3";
package example;
message Msg {
  optional int32 foo = 1;
}

В примерах функция GetProto создает и возвращает сообщение типа Msg с неуказанным содержимым.

Пример на C++

Неявное присутствие:

Msg m = GetProto();
if (m.foo() != 0) {
  // "Очистить" поле:
  m.set_foo(0);
} else {
  // Значение по умолчанию: поле могло отсутствовать.
  m.set_foo(1);
}

Явное присутствие:

Msg m = GetProto();
if (m.has_foo()) {
  // Очистить поле:
  m.clear_foo();
} else {
  // Поле отсутствует, поэтому установить его.
  m.set_foo(1);
}

Пример на C#

Неявное присутствие:

var m = GetProto();
if (m.Foo != 0) {
  // "Очистить" поле:
  m.Foo = 0;
} else {
  // Значение по умолчанию: поле могло отсутствовать.
  m.Foo = 1;
}

Явное присутствие:

var m = GetProto();
if (m.HasFoo) {
  // Очистить поле:
  m.ClearFoo();
} else {
  // Поле отсутствует, поэтому установить его.
  m.Foo = 1;
}

Пример на Go

Неявное присутствие:

m := GetProto()
if m.Foo != 0 {
  // "Очистить" поле:
  m.Foo = 0
} else {
  // Значение по умолчанию: поле могло отсутствовать.
  m.Foo = 1
}

Явное присутствие:

m := GetProto()
if m.Foo != nil {
  // Очистить поле:
  m.Foo = nil
} else {
  // Поле отсутствует, поэтому установить его.
  m.Foo = proto.Int32(1)
}

Пример на Java

Эти примеры используют Builder для демонстрации очистки. Простая проверка присутствия и получение значений из Builder следует тому же API, что и тип сообщения.

Неявное присутствие:

Msg.Builder m = GetProto().toBuilder();
if (m.getFoo() != 0) {
  // "Очистить" поле:
  m.setFoo(0);
} else {
  // Значение по умолчанию: поле могло отсутствовать.
  m.setFoo(1);
}

Явное присутствие:

Msg.Builder m = GetProto().toBuilder();
if (m.hasFoo()) {
  // Очистить поле:
  m.clearFoo()
} else {
  // Поле отсутствует, поэтому установить его.
  m.setFoo(1);
}

Пример на Python

Неявное присутствие:

m = example.Msg()
if m.foo != 0:
  # "Очистить" поле:
  m.foo = 0
else:
  # Значение по умолчанию: поле могло отсутствовать.
  m.foo = 1

Явное присутствие:

m = example.Msg()
if m.HasField('foo'):
  # Очистить поле:
  m.ClearField('foo')
else:
  # Поле отсутствует, поэтому установить его.
  m.foo = 1

Пример на Ruby

Неявное присутствие:

m = Msg.new
if m.foo != 0
  # "Очистить" поле:
  m.foo = 0
else
  # Значение по умолчанию: поле могло отсутствовать.
  m.foo = 1
end

Явное присутствие:

m = Msg.new
if m.has_foo?
  # Очистить поле:
  m.clear_foo
else
  # Поле отсутствует, поэтому установить его.
  m.foo = 1
end

Пример на Javascript

Неявное присутствие:

var m = new Msg();
if (m.getFoo() != 0) {
  // "Очистить" поле:
  m.setFoo(0);
} else {
  // Значение по умолчанию: поле могло отсутствовать.
  m.setFoo(1);
}

Явное присутствие:

var m = new Msg();
if (m.hasFoo()) {
  // Очистить поле:
  m.clearFoo()
} else {
  // Поле отсутствует, поэтому установить его.
  m.setFoo(1);
}

Пример на Objective-C

Неявное присутствие:

Msg *m = [Msg message];
if (m.foo != 0) {
  // "Очистить" поле:
  m.foo = 0;
} else {
  // Значение по умолчанию: поле могло отсутствовать.
  m.foo = 1;
}

Явное присутствие:

Msg *m = [Msg message];
if ([m hasFoo]) {
  // Очистить поле:
  [m clearFoo];
} else {
  // Поле отсутствует, поэтому установить его.
  m.foo = 1;
}

Шпаргалка

Proto2:

Отслеживается ли присутствие поля?

Тип поляОтслеживается?
Сингулярное поледа
Сингулярное поле сообщенияда
Поле в oneofда
Повторяющееся поле & картанет

Proto3:

Отслеживается ли присутствие поля?

Тип поляОтслеживается?
Другое сингулярное полеесли определено как optional
Сингулярное поле сообщенияда
Поле в oneofда
Повторяющееся поле & картанет

Редакция 2023:

Отслеживается ли присутствие поля?

Тип поля (в порядке убывания приоритета)Отслеживается?
Повторяющееся поле & картанет
Поля сообщений и Oneofда
Другие сингулярные поля, если features.field_presence установлено в IMPLICITнет
Все остальные поляда