C# 生成コードガイド

プロトコルバッファコンパイラが proto3 構文を使用してプロトコル定義用に生成する C# コードを正確に説明します。

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

コンパイラの呼び出し

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

C# 固有のオプション

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

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

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

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

    player.protocsharp_namespace オプション Example.Game を持つ場合、プロトコルバッファコンパイラは 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 ファイル名を Pascal ケースに変換し、アンダースコアを単語区切り文字として扱うことで導き出されます。したがって、たとえば、player_record.proto というファイルは PlayerRecord.cs という出力ファイルになります(ファイル拡張子は --csharp_opt を使用して指定できます。これは上記で示したとおりです)。

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

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

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

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

各トップレベルの 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 {}

プロトコルバッファコンパイラは、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# になります。

フィールド

プロトコルバッファコンパイラは、メッセージ内で定義された各フィールドに対して C# プロパティを生成します。プロパティの正確な性質は、フィールドの性質(その型、単数、繰り返し、マップフィールドのいずれか)に依存します。

単数フィールド

すべての単数フィールドは読み書き可能なプロパティを生成します。string または bytes フィールドは、null 値が指定された場合、ArgumentNullException を生成します。明示的に設定されていないフィールドから値を取得すると、空の文字列または ByteString が返されます。メッセージフィールドは null 値に設定でき、これは実質的にフィールドをクリアすることに相当します。これは、値をメッセージ型の「空」インスタンスに設定することとは異なります。

繰り返しフィールド

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

繰り返しフィールドには、メッセージ型であっても null 値を含めることはできません。ただし、以下で説明する nullable ラッパー型は例外です。

マップフィールド

各マップフィールドは、Google.Protobuf.Collections.MapField<TKey, TValue> 型の読み取り専用プロパティを生成します。ここで、TKey はフィールドのキー型、TValue はフィールドの値型です。ほとんどの場合、これは Dictionary<TKey, TValue> のように動作しますが、別の辞書を一度に追加できるようにする追加の Add オーバーロードがあります。これは、オブジェクト初期化子で繰り返しフィールドを埋めるときに便利です。さらに、MapField<TKey, TValue> はシリアル化、逆シリアル化、クローン作成を直接サポートしていますが、これは通常、手書きのアプリケーションコードではなく、生成されたコードによって使用されます。マップのキーは 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 メソッドを呼び出すことと同じです。

ラッパー型フィールド

ほとんどのよく知られた型はコード生成に影響しませんが、ラッパー型(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;
}

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

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

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

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

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

.proto 言語では、複数の enum シンボルが同じ数値を持つことができます。同じ数値を持つシンボルは同義語です。これらは C# ではまったく同じ方法で表現され、同じ数値に対応する複数の名前が付けられます。

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

サービス

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