C# 生成コードガイド

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

このドキュメントを読む前に、proto2 言語ガイドproto3 言語ガイド、または editions 言語ガイドを読む必要があります。

コンパイラの呼び出し

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

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.proto には Example.Game という csharp_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 から推測されます。例えば、example.high_score という proto パッケージは、Example.HighScore という名前空間になります。csharp_namespace ファイルオプションを使用して、特定の .proto ファイルのデフォルトで生成される名前空間をオーバーライドできます。

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

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

edition = "2023";

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

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> はシリアライゼーション、デシリアライゼーション、クローン作成を直接サポートしていますが、これは通常、手書きのアプリケーションコードではなく、生成されたコードによって使用されます。

繰り返しフィールドには、後述する null 許容ラッパー型を除き、メッセージ型であっても null 値を含めることはできません。

マップフィールド

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

Oneof フィールド

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

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

コンパイラはこれらの public メンバーを生成します。

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 の "case" である場合、そのプロパティを取得すると、そのプロパティに設定された値が返されます。そうでない場合、プロパティを取得すると、プロパティの型のデフォルト値が返されます。oneof のメンバーは一度に 1 つしか設定できません。

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

ラッパー型のフィールド

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

C# の値型に対応するすべてのラッパー型(Int32Wrapper, DoubleWrapper, BoolWrapper など)は、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# コードジェネレータはサービスを完全に無視します。