Protocol Buffer の基本: C#
このチュートリアルは、proto3 バージョンの Protocol Buffers 言語を使用して、C# プログラマー向けの Protocol Buffers の基本的な使用方法を紹介します。シンプルなサンプルアプリケーションの作成を通して、以下の方法を示します。
.proto
ファイルでメッセージ形式を定義する。- Protocol Buffer コンパイラを使用する。
- C# Protocol Buffer API を使用してメッセージを書き込み、読み取る。
これは、C# で Protocol Buffers を使用するための包括的なガイドではありません。より詳細なリファレンス情報については、Protocol Buffer 言語ガイド、C# API リファレンス、C# 生成コードガイド、およびエンコーディング リファレンスを参照してください。
問題領域
この例で使用するのは、人々の連絡先情報をファイルに読み書きできる、非常にシンプルな「アドレス帳」アプリケーションです。アドレス帳の各人物には、名前、ID、メールアドレス、連絡先電話番号が含まれます。
このような構造化データをシリアル化して取得するにはどうすればよいでしょうか?この問題を解決するにはいくつかの方法があります。
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
および関連クラスを使用する .NET バイナリシリアル化を使用します。これは変更に対して非常に脆弱であり、場合によってはデータサイズが大きくなるためコストがかかります。また、他のプラットフォーム用に書かれたアプリケーションとデータを共有する必要がある場合にも、あまりうまく機能しません。- データ項目を単一の文字列にエンコードするアドホックな方法を考案できます。例えば、4つの整数を「12:3:-23:67」のようにエンコードするなどです。これはシンプルで柔軟なアプローチですが、一度限りのエンコードとパースのコードを書く必要があり、パースにはわずかな実行時コストがかかります。これは非常にシンプルなデータをエンコードする場合に最適です。
- データをXMLにシリアル化します。このアプローチは、XMLが(ある程度)人間が読める形式であり、多くの言語に対応するバインディングライブラリが存在するため、非常に魅力的です。他のアプリケーション/プロジェクトとデータを共有したい場合には良い選択肢となるでしょう。しかし、XMLはスペースを大量に消費することで知られており、そのエンコード/デコードはアプリケーションに大きなパフォーマンス上のペナルティを課す可能性があります。また、XML DOMツリーをナビゲートすることは、クラス内の単純なフィールドを通常ナビゲートするよりもかなり複雑です。
Protocol Buffers は、まさにこの問題を解決するための柔軟で効率的、自動化されたソリューションです。Protocol Buffers を使用すると、格納したいデータ構造の .proto
記述を作成します。そこから、Protocol Buffer コンパイラは、効率的なバイナリ形式で Protocol Buffer データの自動エンコードと解析を実装するクラスを生成します。生成されたクラスは、Protocol Buffer を構成するフィールドのゲッターとセッターを提供し、Protocol Buffer を単一の単位として読み書きする詳細を処理します。重要なのは、Protocol Buffer 形式が、古い形式でエンコードされたデータをコードがまだ読み取れるような方法で、時間をかけて形式を拡張するという考え方をサポートしていることです。
サンプルコードの場所
この例は、Protocol Buffers を使用してエンコードされたアドレス帳データファイルを管理するためのコマンドラインアプリケーションです。コマンド AddressBook
(参照: Program.cs) は、データファイルに新しいエントリを追加したり、データファイルを解析してデータをコンソールに出力したりできます。
完全なサンプルは、GitHub リポジトリの examples ディレクトリ および csharp/src/AddressBook
ディレクトリにあります。
プロトコル形式の定義
アドレス帳アプリケーションを作成するには、まず .proto
ファイルから始める必要があります。.proto
ファイル内の定義はシンプルです。シリアル化したい各データ構造に対して メッセージ を追加し、そのメッセージ内の各フィールドに名前と型を指定します。この例では、メッセージを定義する .proto
ファイルは addressbook.proto
です。
.proto
ファイルはパッケージ宣言から始まり、異なるプロジェクト間での名前の衝突を防ぐのに役立ちます。
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
C# では、csharp_namespace
が指定されていない場合、生成されたクラスは package
名に一致する名前空間に配置されます。この例では、デフォルトをオーバーライドするために csharp_namespace
オプションが指定されているため、生成されたコードは Tutorial
の代わりに Google.Protobuf.Examples.AddressBook
という名前空間を使用します。
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
次に、メッセージ定義です。メッセージは、型付きフィールドのセットを含む単なる集合体です。bool
、int32
、float
、double
、string
など、多くの標準的な単純データ型がフィールド型として利用できます。他のメッセージ型をフィールド型として使用することで、メッセージにさらなる構造を追加することもできます。
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
上記の例では、Person
メッセージには PhoneNumber
メッセージが含まれ、AddressBook
メッセージには Person
メッセージが含まれています。他のメッセージ内にネストされたメッセージ型を定義することもできます。ご覧のとおり、PhoneNumber
型は Person
内で定義されています。フィールドの1つに定義済みの値リストのいずれかを持たせたい場合は、enum
型を定義することもできます。ここでは、電話番号が PHONE_TYPE_MOBILE
、PHONE_TYPE_HOME
、または PHONE_TYPE_WORK
のいずれかであることを指定したいとします。
各要素の「 = 1」、「 = 2」というマーカーは、バイナリエンコーディングでそのフィールドが使用する一意の「タグ」を識別します。タグ番号1~15は、より高い番号よりもエンコードに1バイト少なく済みます。そのため、最適化として、これらのタグを頻繁に使用される要素や繰り返し要素に使い、タグ16以降をあまり使用されないオプション要素に残すことができます。繰り返しフィールドの各要素はタグ番号の再エンコードを必要とするため、繰り返しフィールドはこの最適化に特に適しています。
フィールド値が設定されていない場合、デフォルト値が使用されます。数値型の場合はゼロ、文字列の場合は空文字列、ブール型の場合は false です。埋め込みメッセージの場合、デフォルト値は常にメッセージの「デフォルトインスタンス」または「プロトタイプ」であり、そのフィールドは設定されていません。明示的に設定されていないフィールドの値を取得するためにアクセサーを呼び出すと、常にそのフィールドのデフォルト値が返されます。
フィールドが repeated
の場合、そのフィールドは任意の回数(ゼロ回を含む)繰り返すことができます。繰り返し値の順序は Protocol Buffer で保持されます。繰り返しフィールドは動的にサイズ変更される配列と考えることができます。
すべての可能なフィールド型を含む、.proto
ファイルの書き方に関する完全なガイドは、Protocol Buffer 言語ガイドにあります。ただし、クラス継承のような機能を探さないでください。Protocol Buffers はそれを提供しません。
Protocol Buffers のコンパイル
.proto
ファイルができたので、次に AddressBook
(ひいては Person
と PhoneNumber
)メッセージの読み書きに必要なクラスを生成する必要があります。これを行うには、.proto
ファイルに対して Protocol Buffer コンパイラ protoc
を実行する必要があります。
コンパイラをインストールしていない場合は、パッケージをダウンロードし、README の指示に従ってください。
次に、コンパイラを実行します。ソースディレクトリ(アプリケーションのソースコードが置かれている場所。値を指定しない場合は現在のディレクトリが使用されます)、出力先ディレクトリ(生成されたコードの保存先。
$SRC_DIR
と同じである場合が多い)、および.proto
へのパスを指定します。この場合、次のように呼び出します。protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto
C# コードが必要なので、
--csharp_out
オプションを使用します。他のサポートされている言語にも同様のオプションが提供されています。
これにより、指定された出力先ディレクトリに Addressbook.cs
が生成されます。このコードをコンパイルするには、Google.Protobuf
アセンブリへの参照を持つプロジェクトが必要です。
Addressbook クラス
Addressbook.cs
を生成すると、以下の5つの便利な型が得られます。
- Protocol Buffer メッセージに関するメタデータを含む静的
Addressbook
クラス。 - 読み取り専用の
People
プロパティを持つAddressBook
クラス。 Name
、Id
、Email
、およびPhones
のプロパティを持つPerson
クラス。- 静的
Person.Types
クラス内にネストされたPhoneNumber
クラス。 Person.Types
内にネストされたPhoneType
enum。
生成される内容の詳細はC# 生成コードガイドで詳しく読むことができますが、ほとんどの場合、これらはごく普通のC#型として扱うことができます。注目すべき点として、繰り返しフィールドに対応するプロパティはすべて読み取り専用である点が挙げられます。コレクションに項目を追加したり、コレクションから項目を削除したりすることはできますが、完全に別のコレクションに置き換えることはできません。繰り返しフィールドのコレクション型は常に RepeatedField<T>
です。この型は List<T>
に似ていますが、コレクション初期化子で使用するための項目コレクションを受け入れる Add
オーバーロードなど、いくつかの便利な追加メソッドがあります。
Person のインスタンスを作成する例を次に示します。
Person john = new Person
{
Id = 1234,
Name = "John Doe",
Email = "jdoe@example.com",
Phones = { new Person.Types.PhoneNumber { Number = "555-4321", Type = Person.Types.PhoneType.Home } }
};
C# 6 では、using static
を使用して Person.Types
の煩わしさを解消できることに注意してください。
// Add this to the other using directives
using static Google.Protobuf.Examples.AddressBook.Person.Types;
...
// The earlier Phones assignment can now be simplified to:
Phones = { new PhoneNumber { Number = "555-4321", Type = PhoneType.HOME } }
解析とシリアル化
Protocol Buffers を使用する主な目的は、データをシリアル化して他の場所で解析できるようにすることです。生成されたすべてのクラスには WriteTo(CodedOutputStream)
メソッドがあり、CodedOutputStream
は Protocol Buffer ランタイムライブラリのクラスです。ただし、通常は拡張メソッドのいずれかを使用して通常の System.IO.Stream
に書き込んだり、メッセージをバイト配列または ByteString
に変換したりします。これらの拡張メッセージは Google.Protobuf.MessageExtensions
クラスにあるため、シリアル化したい場合は通常、Google.Protobuf
名前空間の using
ディレクティブが必要です。例えば、
using Google.Protobuf;
...
Person john = ...; // Code as before
using (var output = File.Create("john.dat"))
{
john.WriteTo(output);
}
解析もシンプルです。生成された各クラスには、その型に対応する MessageParser<T>
を返す静的な Parser
プロパティがあります。このプロパティには、ストリーム、バイト配列、および ByteString
を解析するメソッドがあります。したがって、先ほど作成したファイルを解析するには、次のように使用できます。
Person john;
using (var input = File.OpenRead("john.dat"))
{
john = Person.Parser.ParseFrom(input);
}
これらのメッセージを使用してアドレス帳を管理する(新しいエントリの追加と既存のエントリの一覧表示)完全なサンプルプログラムは、GitHub リポジトリで入手できます。
Protocol Buffer の拡張
Protocol Buffer を使用するコードをリリースした後、遅かれ早かれ Protocol Buffer の定義を「改善」したいと思うでしょう。新しいバッファが後方互換性があり、古いバッファが前方互換性があることを望む場合(そして、ほとんどの場合そう望むでしょう)、従うべきいくつかのルールがあります。Protocol Buffer の新しいバージョンでは、
- 既存のフィールドのタグ番号を変更してはなりません。
- フィールドを削除できます。
- 新しいフィールドを追加できますが、その場合は新しいタグ番号(つまり、この Protocol Buffer で一度も使用されていないタグ番号、削除されたフィールドであっても)を使用する必要があります。
(これらのルールにはいくつかの例外がありますが、それらはめったに使用されません。)
これらのルールに従えば、古いコードは新しいメッセージを問題なく読み取り、新しいフィールドは単純に無視します。古いコードにとっては、削除された単一のフィールドは単にデフォルト値を持つことになり、削除された繰り返しフィールドは空になります。新しいコードも古いメッセージを透過的に読み取ります。
ただし、新しいフィールドは古いメッセージには存在しないため、デフォルト値を適切に処理する必要があることに留意してください。型固有のデフォルト値が使用されます。文字列の場合、デフォルト値は空の文字列です。ブール型の場合、デフォルト値は false です。数値型の場合、デフォルト値はゼロです。
リフレクション
メッセージ記述子(.proto
ファイル内の情報)とメッセージのインスタンスは、リフレクション API を使用してプログラムで検査できます。これは、異なるテキスト形式やスマートな差分ツールなどの汎用コードを記述する際に役立ちます。生成された各クラスには静的な Descriptor
プロパティがあり、任意のインスタンスの記述子は IMessage.Descriptor
プロパティを使用して取得できます。これらの使用方法の簡単な例として、任意のメッセージのトップレベルフィールドを出力する短いメソッドを次に示します。
public void PrintMessage(IMessage message)
{
var descriptor = message.Descriptor;
foreach (var field in descriptor.Fields.InDeclarationOrder())
{
Console.WriteLine(
"Field {0} ({1}): {2}",
field.FieldNumber,
field.Name,
field.Accessor.GetValue(message);
}
}