C# で生成されたコードガイド

proto3 構文を使用したプロトコル定義に対して、protocol buffer コンパイラが生成する C# コードについて正確に説明します。

このドキュメントを読む前に、proto3 言語ガイドを読むことをお勧めします。

コンパイラの呼び出し

protocol buffer コンパイラは、--csharp_out コマンドライン フラグを指定して呼び出すと、C# 出力を生成します。--csharp_out オプションのパラメータは、コンパイラが C# 出力を書き込むディレクトリですが、その他のオプションによっては、コンパイラが指定されたディレクトリのサブディレクトリを作成する場合があります。コンパイラは、入力された各 .proto ファイルに対して 1 つのソース ファイルを作成し、拡張子はデフォルトで .cs になりますが、コンパイラ オプションで構成可能です。

C# コード ジェネレータでは proto3 メッセージのみがサポートされています。各 .proto ファイルが次の宣言で始まることを確認してください。

syntax = "proto3";

C# 固有のオプション

--csharp_opt コマンドライン フラグを使用して、protocol buffer コンパイラに追加の C# オプションを指定できます。サポートされているオプションは次のとおりです。

  • file_extension: 生成されたコードのファイル拡張子を設定します。デフォルトは .cs ですが、一般的な代替案は、ファイルに生成されたコードが含まれていることを示す .g.cs です。

  • base_namespace: このオプションが指定されている場合、ジェネレータは、生成されたクラスの名前空間に対応する生成されたソース コードのディレクトリ階層を作成し、オプションの値を使用して、出力ディレクトリの「ベース」と見なされる名前空間の部分を示します。たとえば、次のコマンドラインの場合

    protoc --proto_path=bar --csharp_out=src --csharp_opt=base_namespace=Example player.proto
    

    player.protoExample.Gamecsharp_namespace オプションがある場合、protocol buffer コンパイラは、src/Game/Player.cs ファイルを生成します。このオプションは通常、Visual Studio の C# プロジェクトのデフォルト名前空間オプションに対応します。オプションが指定されているが空の値の場合、生成されたファイルで使用されている完全な C# 名前空間がディレクトリ階層に使用されます。オプションがまったく指定されていない場合、生成されたファイルは、階層構造を作成せずに、--csharp_out で指定されたディレクトリに単純に書き込まれます。

  • internal_access: このオプションが指定されている場合、ジェネレータは public アクセス修飾子ではなく、internal アクセス修飾子を持つ型を作成します。

  • serializable: このオプションが指定されている場合、ジェネレータは生成されたメッセージ クラスに [Serializable] 属性を追加します。

複数のオプションは、次の例のようにコンマで区切って指定できます。

protoc --proto_path=src --csharp_out=build/gen --csharp_opt=file_extension=.g.cs,base_namespace=Example,internal_access src/foo.proto

ファイル構造

出力ファイルの名前は、.proto ファイル名をパスカルケースに変換し、アンダースコアを単語区切り文字として扱うことによって派生します。したがって、たとえば、player_record.proto という名前のファイルは、PlayerRecord.cs という名前の出力ファイルになります (ファイル拡張子は --csharp_opt上記を使用して指定できます)。

生成された各ファイルは、パブリック メンバーに関して、次の形式を取ります (実装はここには示されていません)。

namespace [...]
{
  public static partial class [... descriptor class name ...]
  {
    public static FileDescriptor Descriptor { get; }
  }

  [... Enums ...]
  [... Message classes ...]
}

namespace は、ファイル名と同じ変換ルールを使用して、proto の package から推測されます。たとえば、proto パッケージ example.high_score は、Example.HighScore の名前空間になります。csharp_namespace ファイル オプションを使用して、特定の .proto のデフォルトで生成される名前空間をオーバーライドできます。

トップレベルの enum とメッセージはそれぞれ、名前空間のメンバーとして宣言される enum またはクラスになります。さらに、ファイル記述子に対して常に単一の静的 partial クラスが生成されます。これは、リフレクションベースの操作に使用されます。記述子クラスには、拡張子なしでファイルと同じ名前が付けられます。ただし、(非常に一般的なように) 同じ名前のメッセージがある場合、記述子クラスは、メッセージとの衝突を避けるために、ネストされた Proto 名前空間に配置されます。

これらのすべてのルールの例として、Protocol Buffers の一部として提供されている timestamp.proto ファイルについて考えてみましょう。timestamp.proto の簡略化されたバージョンは次のようになります。

syntax = "proto3";
package google.protobuf;
option csharp_namespace = "Google.Protobuf.WellKnownTypes";

message Timestamp { ... }

生成された Timestamp.cs ファイルの構造は次のとおりです。

namespace Google.Protobuf.WellKnownTypes
{
  namespace Proto
  {
    public static partial class Timestamp
    {
      public static FileDescriptor Descriptor { get; }
    }
  }

  public sealed partial class Timestamp : IMessage<Timestamp>
  {
    [...]
  }
}

メッセージ

単純なメッセージ宣言が与えられた場合

message Foo {}

protocol buffer コンパイラは、Foo という名前の sealed partial クラスを生成します。これは、メンバー宣言とともに以下に示すように、IMessage<Foo> インターフェースを実装します。詳細については、インライン コメントを参照してください。

public sealed partial class Foo : IMessage<Foo>
{
  // Static properties for parsing and reflection
  public static MessageParser<Foo> Parser { get; }
  public static MessageDescriptor Descriptor { get; }

  // Explicit implementation of IMessage.Descriptor, to avoid conflicting with
  // the static Descriptor property. Typically the static property is used when
  // referring to a type known at compile time, and the instance property is used
  // when referring to an arbitrary message, such as during JSON serialization.
  MessageDescriptor IMessage.Descriptor { get; }

  // Parameterless constructor which calls the OnConstruction partial method if provided.
  public Foo();
  // Deep-cloning constructor
  public Foo(Foo);
  // Partial method which can be implemented in manually-written code for the same class, to provide
  // a hook for code which should be run whenever an instance is constructed.
  partial void OnConstruction();

  // Implementation of IDeepCloneable<T>.Clone(); creates a deep clone of this message.
  public Foo Clone();

  // Standard equality handling; note that IMessage<T> extends IEquatable<T>
  public override bool Equals(object other);
  public bool Equals(Foo other);
  public override int GetHashCode();

  // Converts the message to a JSON representation
  public override string ToString();

  // Serializes the message to the protobuf binary format
  public void WriteTo(CodedOutputStream output);
  // Calculates the size of the message in protobuf binary format
  public int CalculateSize();

  // Merges the contents of the given message into this one. Typically
  // used by generated code and message parsers.
  public void MergeFrom(Foo other);

  // Merges the contents of the given protobuf binary format stream
  // into this message. Typically used by generated code and message parsers.
  public void MergeFrom(CodedInputStream input);
}

これらのメンバーはすべて常に存在することに注意してください。optimize_for オプションは、C# コード ジェネレータの出力には影響しません。

ネストされた型

メッセージは別のメッセージ内に宣言できます。例:

message Foo {
  message Bar {
  }
}

この場合、またはメッセージにネストされた enum が含まれている場合、コンパイラはネストされた Types クラスと、Types クラス内の Bar クラスを生成するため、生成されるコード全体は次のようになります。

namespace [...]
{
  public sealed partial class Foo : IMessage<Foo>
  {
    public static partial class Types
    {
      public sealed partial class Bar : IMessage<Bar> { ... }
    }
  }
}

中間 Types クラスは不便ですが、メッセージに対応するフィールドを持つネストされた型の一般的なシナリオに対処するために必要です。そうしないと、同じクラス内に同じ名前のプロパティと型がネストされてしまい、無効な C# になります。

フィールド

protocol buffer コンパイラは、メッセージ内で定義された各フィールドに対して C# プロパティを生成します。プロパティの正確な性質は、フィールドの性質 (型、および単数、反復、またはマップ フィールドかどうか) によって異なります。

単数フィールド

任意の単数フィールドは、読み取り/書き込みプロパティを生成します。string または bytes フィールドは、null 値が指定された場合、ArgumentNullException を生成します。明示的に設定されていないフィールドから値を取得すると、空の文字列または ByteString が返されます。メッセージ フィールドは null 値に設定できます。これは、フィールドを効果的にクリアすることになります。これは、値をメッセージ型の「空の」インスタンスに設定することとは同等ではありません。

反復フィールド

各反復フィールドは、型 Google.Protobuf.Collections.RepeatedField<T> の読み取り専用プロパティを生成します。ここで、T はフィールドの要素型です。ほとんどの場合、これは List<T> のように動作しますが、オブジェクト初期化子で反復フィールドを設定するときに便利な、アイテムのコレクションを一度に追加できる追加の Add オーバーロードがあります。さらに、RepeatedField<T> は、シリアライゼーション、デシリアライゼーション、および複製を直接サポートしていますが、これは通常、手動で記述されたアプリケーション コードではなく、生成されたコードによって使用されます。

反復フィールドには、以下で説明する nullable ラッパー型を除き、メッセージ型であっても null 値を含めることはできません。

マップ フィールド

各マップ フィールドは、型 Google.Protobuf.Collections.MapField<TKey, TValue> の読み取り専用プロパティを生成します。ここで、TKey はフィールドのキー型、TValue はフィールドの値型です。ほとんどの場合、これは Dictionary<TKey, TValue> のように動作しますが、オブジェクト初期化子で反復フィールドを設定するときに便利な、別のディクショナリを一度に追加できる追加の Add オーバーロードがあります。さらに、MapField<TKey, TValue> は、シリアライゼーション、デシリアライゼーション、および複製を直接サポートしていますが、これは通常、手動で記述されたアプリケーション コードではなく、生成されたコードによって使用されます。マップ内のキーは null にすることはできません。対応する単数フィールド型が null 値をサポートしている場合、値は null にすることができます。

Oneof フィールド

oneof 内の各フィールドには、通常の単数フィールドのように、個別のプロパティがあります。ただし、コンパイラは、enum のどのフィールドが設定されたかを判断するための追加のプロパティと、enum および oneof をクリアするメソッドも生成します。たとえば、この oneof フィールド定義の場合

oneof avatar {
  string image_url = 1;
  bytes image_data = 2;
}

コンパイラは、これらのパブリック メンバーを生成します。

enum AvatarOneofCase
{
  None = 0,
  ImageUrl = 1,
  ImageData = 2
}

public AvatarOneofCase AvatarCase { get; }
public void ClearAvatar();
public string ImageUrl { get; set; }
public ByteString ImageData { get; set; }

プロパティが現在の oneof 「ケース」である場合、そのプロパティを取得すると、そのプロパティに設定された値が返されます。それ以外の場合、プロパティを取得すると、プロパティの型のデフォルト値が返されます。oneof のメンバーは一度に 1 つしか設定できません。

oneof の構成プロパティを設定すると、oneof の報告される「ケース」が変更されます。通常の単数フィールドと同様に、string または bytes 型の oneof フィールドを null 値に設定することはできません。メッセージ型のフィールドを null に設定することは、oneof 固有の Clear メソッドを呼び出すことと同等です。

ラッパー型フィールド

proto3 の既知の型のほとんどはコード生成に影響を与えませんが、ラッパー型 (StringWrapperInt32Wrapper など) はプロパティの型と動作を変更します。

C# 値型 (Int32WrapperDoubleWrapperBoolWrapper など) に対応するすべてのラッパー型は、Nullable<T> にマップされます。ここで、T は対応する非 nullable 型です。たとえば、DoubleValue 型のフィールドは、Nullable<double> 型の C# プロパティになります。

StringWrapper または BytesWrapper 型のフィールドは、string および ByteString 型の C# プロパティを生成しますが、デフォルト値は null であり、null をプロパティ値として設定できます。

すべてのラッパー型について、反復フィールドでは null 値は許可されていませんが、マップ エントリの値としては許可されています。

列挙型

次のような列挙型定義が与えられた場合

enum Color {
  COLOR_UNSPECIFIED = 0;
  COLOR_RED = 1;
  COLOR_GREEN = 5;
  COLOR_BLUE = 1234;
}

protocol buffer コンパイラは、同じ値のセットを持つ Color という名前の C# enum 型を生成します。enum 値の名前は、C# 開発者にとってより慣用的なものになるように変換されます。

  • 元の名前が enum 名自体の大文字形式で始まる場合、それは削除されます
  • 結果はパスカルケースに変換されます

したがって、上記の Color proto enum は、次の C# コードになります。

enum Color
{
  Unspecified = 0,
  Red = 1,
  Green = 5,
  Blue = 1234
}

この名前変換は、メッセージの JSON 表現内で使用されるテキストには影響しません。

.proto 言語では、複数の enum シンボルが同じ数値を持つことができることに注意してください。同じ数値を持つシンボルは同義語です。これらは C# ではまったく同じように表現され、複数の名前が同じ数値に対応します。

ネストされていない列挙型は、新しい名前空間メンバーとして生成される C# enum につながります。ネストされた列挙型は、列挙型がネストされているメッセージに対応するクラス内のネストされた Types クラスで生成される C# enum につながります。

サービス

C# コード ジェネレータは、サービスを完全に無視します。