Skip to content

Instantly share code, notes, and snippets.

@hideyukisaito
Last active April 16, 2024 06:46
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hideyukisaito/d298075c63b97f56825b0d413d8e4dc5 to your computer and use it in GitHub Desktop.
Save hideyukisaito/d298075c63b97f56825b0d413d8e4dc5 to your computer and use it in GitHub Desktop.
C# at Google Style Guide 日本語訳

C# at Google Style Guide を日本語訳したものです。一部こなれていない箇所もあるので、お気づきの点があれば気軽にコメントでご指摘頂ければ幸いです。

C# at Google Style Guide 日本語訳

このスタイルガイドは Google 社内で開発された C# コード用であり、Google の C# コードのデフォルトのスタイルです。 Google C++ スタイルや Google Java スタイルなど、Google の他の言語に準拠した文体の選択を行います。

フォーマットについてのガイドライン

命名規則

命名規則は Microsoft の C# 命名ガイドラインに従います。Microsoft の命名ガイドラインが指定されていない場合(プライベート変数やローカル変数など)、ルールは CoreFX C# コーディングガイドラインから取得されます。

ルールの概要:

コード

  • クラス、メソッド、列挙型、パブリックフィールド、パブリックプロパティ、名前空間の名前:PascalCase
  • ローカル変数の名前、パラメーター:camelCase
  • private、protected、internal および protected な internal フィールドとプロパティの名前:_camelCase。
  • 命名規則は、const、static、readonlyなどの修飾子の影響を受けません。
  • 頭文字を含め、スペースがないひとまとまりの文字列をひとつの「単語」として扱います。これによってケーシングの行い方が決定されます。たとえば "My RPC" は MyRPC ではなく MyRpc です。
  • インターフェースの名前は I で始まります。例:IInterface。

ファイル

  • ファイル名とディレクトリ名はPascalCaseです。例:MyFile.cs
  • 可能な場合、ファイル名はファイル内のメインクラスの名前と同じにする必要があります。例:MyClass.cs
  • 一般に、ファイルごとに1つのコアクラスを優先します。

グループと整理

  • 修飾子は次の順序で発生します:public protected internal private new abstract override sealed static readonly extern unsafe volatile async
  • 名前空間の using 宣言は、これから定義する名前空間の宣言より前の上部に配置します。System 系のインポートは常に最初に配置し、その他の using 宣言の順番はアルファベット順です。
  • クラスメンバーの順序は以下のとおりです:
    • クラスメンバーは以下の順序でグループ化します:
      • ネストされたクラス、列挙型、デリゲートとイベント
      • static、const、readonly フィールド
      • フィールドとプロパティ
      • コンストラクターとファイナライザー
      • メソッド
    • 各グループ内での要素の順序は以下のとおりです:
      • Public.
      • Internal.
      • Protected internal.
      • Protected.
      • Private.
    • 可能な場合は、インターフェースの実装をグループ化します。

スペース(空白文字、改行、インデント)についてのルール

Google Java スタイルにもとづきます。

  • 1行あたり最大1つのステートメント。
  • ステートメントごとに最大1つの割り当て。
  • 2つのスペースのインデント、タブなし。
  • 1行の最大文字数は100文字。
  • 波括弧の開始前に改行を入れない。
  • 波括弧閉じと else の間に改行を入れない。
  • オプションにも波括弧を使用します。
  • if/for/while 等やカンマのあとにはスペースを挿入します。
  • 括弧開きの直後、または括弧閉じの直前にはスペースを入れません。
  • 単項演算子とそのオペランドの間にスペースは入れません。演算子と他のすべての演算子の各オペランドの間の1つのスペースを入れます。
  • 行の折り返しには Microsoft の C# 整形ツール向けの互換性のため若干の変更が加えられた Google C++ スタイルガイドを使用します。
    • 一般に、行の継続は4スペースインデントされます。
    • 波括弧(リストイニシャライザー、ラムダ、オブジェクトイニシャライザーなど)での改行は継続としてカウントされません。
    • メソッドの定義と呼び出しをおこなう際、引数がすべて1行に収まらない場合は複数の行に分割し、後続の各行を最初の引数に揃える必要があります。これに十分なスペースがない場合は、代わりに4つのスペースインデントを使用して後続の行に引数を配置できます。以下のコード例はこれを示しています。

コード例

using System;                                       // `using` は名前空間よりも前に書きます。

namespace MyNamespace {                             // 名前空間は PascalCase です。
                                                    // 名前空間のあとはインデントします。
  public interface IMyInterface {                   // インターフェース名は 'I' で開始します。
    public int Calculate(float value, float exp);   // メソッド名は PascalCase です。
                                                    // カンマのあとにはスペースを入れます。
  }

  public enum MyEnum {                              // 列挙型名は PascalCase です。
    Yes,                                            // 列挙子名も PascalCase です。
    No,
  }

  public class MyClass {                            // クラス名は PascalCase です。
    public int Foo = 0;                             // パブリックメンバー名は PascalCase です。
                                                    
    public bool NoCounting = false;                 // フィールドの初期化を奨励します。
    private class Results {
      public int NumNegativeResults = 0;
      public int NumPositiveResults = 0;
    }
    private Results _results;                       // プライベートメンバー名は _camelCase です。
                                                    
    public static int NumTimesCalled = 0;
    private const int _bar = 100;                   // const 修飾子は命名規則に影響しません。
                                                    
    private int[] _someTable = {                    // コンテナ初期化子には2スペースのインデントを使用します。
      2, 3, 4,                                      
    }

    public MyClass() {
      _results = new Results {
        NumNegativeResults = 1,                     // オブジェクト初期化子には2スペースのインデントを使用します。
        NumPositiveResults = 1,                     
      };
    }

    public int CalculateValue(int mulNumber) {      // 括弧開き前には改行しません。
      var resultValue = Foo * mulNumber;            // ローカル変数名は camelCase です。
      NumTimesCalled++;
      Foo += _bar;

      if (!NoCounting) {                            // 'if' のあとにはスペースを入れます。
                                                    // 単項演算子のあとにはスペースを入れません。
        if (resultValue < 0) {                      // 比較演算子の前後にスペースを入れます。
                                                    
          _results.NumNegativeResults++;
        } else if (resultValue > 0) {               // 波括弧と else の間は改行しません。
          _results.NumPositiveResults++;
        }
      }

      return resultValue;
    }

    public void ExpressionBodies() {
      // 単純なラムダ式は可能であれば1行におさめます。括弧類は必要ありません。
      Func<int, int> increment = x => x + 1;

      // 閉じの波括弧は、括弧開きが含まれる行の先頭に揃えます。
      Func<int, int, long> difference1 = (x, y) => {
        long diff = (long)x - y;
        return diff >= 0 ? diff : -diff;
      };

      // 改行のあとに定義をおこなう場合は、その定義全体をインデントします。
      Func<int, int, long> difference2 =
          (x, y) => {
            long diff = (long)x - y;
            return diff >= 0 ? diff : -diff;
          };

      // インラインのラムダ式をメソッドの引数とする場合も上記のルールに従います。
      // ラムダ式が含まれている場合、引数のグループの前に先頭の改行を入れることを優先します。
      CallWithDelegate(
          (x, y) => {
            long diff = (long)x - y;
            return diff >= 0 ? diff : -diff;
          });
    }

    void DoNothing() {}                             // 空のブロックは簡潔に記述します。

    // メソッド名が長くなる場合、可能であれば2つ目以降の引数を改行し、最初の引数に揃えます。
    void AVeryLongFunctionNameThatCausesLineWrappingProblems(int longArgumentName,
                                                             int p1, int p2) {}

    // 1つ目の引数に揃えるとフィットしなかったり、または読みづらくなったりする場合
    // すべての引数を新しい行に記述します。その場合は4スペースのインデントをおこないます。
    void AnotherLongFunctionNameThatCausesLineWrappingProblems(
        int longArgumentName, int longArgumentName2, int longArgumentName3) {}

    void CallingLongFunctionName() {
      int veryLongArgumentName = 1234;
      int shortArg = 1;
      // 可能であれば、新しい行を最初の引数の先頭に揃えます。
      AnotherLongFunctionNameThatCausesLineWrappingProblems(shortArg, shortArg,
                                                            veryLongArgumentName);
      // 1つ目の引数に揃えるとフィットしなかったり、または読みづらくなったりする場合
      // すべての引数を新しい行に記述します。その場合は4スペースのインデントをおこないます。
      AnotherLongFunctionNameThatCausesLineWrappingProblems(
          veryLongArgumentName, veryLongArgumentName, veryLongArgumentName);
    }
  }
}

C# コーディングガイドライン

定数

  • const にすることができる変数とフィールドは、常に const にする必要があります。
  • const が不可能な場合は readonly が適切な代替手段になります。
  • マジックナンバーよりも名前付き定数を優先します。

IEnumerable vs IList vs IReadOnlyList

  • メソッドへの入力を不変にする必要がある場合は、可能な限り最も制限の厳しいコレクションタイプ(IReadOnlyCollection / IReadOnlyList / IEnumerable など)を使用します。
  • 出力の場合、返されたコンテナーの所有権を所有者に渡す場合は IEnumerable よりも IList を優先します。所有権を譲渡しない場合は、最も制限の厳しいオプションを選択してください。

ジェネレーター vs コンテナ

  • 以下のことを念頭に置いて、最善の判断をしてください。
    • 多くの場合、ジェネレーターを使用するコードはコンテナを使用するものよりも読みにくくなります。
    • 結果が遅延処理される場合、ジェネレータを使用するコードのパフォーマンスが向上する可能性があります。例えば、すべての結果が必要なわけではない場合などがそれにあたります。
    • ToList() を介して直接コンテナに変換されるジェネレーターコードは、コンテナに直接入力するよりもパフォーマンスが低下します。
    • 複数回呼び出されるジェネレーターコードは、コンテナを複数回イテレートするよりもかなり遅くなります。

プロパティのスタイル

  • 単一行の読み取り専用プロパティの場合、可能な場合は式形式 (=>) のプロパティを優先します。
  • それ以外の場合は昔ながらの { get; set; } 記法を使用します。

式形式構文

以下のような式があるとします。

int SomeProperty => _someProperty
  • ラムダ式とプロパティにおいて、式形式構文は慎重に使用しましょう。
  • メソッドの定義には使用しないでください。このルールは、この構文が多用されている C# 7 が稼働しているときにレビューされます。
  • メソッドやその他のスコープブロックと同様に、中括弧開きを含む行の最初の文字に終了を揃えます。例についてはサンプルコードを参照してください。

構造体とクラス

  • 構造体はクラスとは大きく異なります。
    • 構造体は常に値型としてやり取りされます。
    • 返された構造体のメンバーに値を割り当てても、もとの構造体は変更されません。例えば transform.position.x = 10 という式は transformposition.x を10に設定しません。ここでの positionVector3 を値で返すプロパティであるため、これは元のコピーの x を設定するだけです。
  • ほぼ常にクラスを使用してください。
  • ある型を他の値型と同じように扱うことができる場合には構造体を検討してください。例えばその型のインスタンスが使用するメモリサイズが小さく、たいていはすぐに廃棄される場合、または他のオブジェクトに埋め込まれる形で使用される場合などです。
  • このガイダンスはチームごとに異なる可能性があることに注意してください。たとえば、パフォーマンスの問題により構造体の使用が強制される場合があります。

ラムダ式 vs 名前付きメソッド

  • そのラムダ式が自明でない場合 (例えば、宣言を除いて2つ以上のステートメントを含む場合など)、またはいくつかの場所で繰り返し使用される場合は名前付きのメソッドを使用するべきです。

フィールドの初期化

  • 通常、フィールドの初期化は奨励されます。

拡張メソッド

  • 元のクラスのソースが利用できない場合、またはソースを変更できない場合にのみ拡張メソッドを使用してください。
  • 追加される機能が元のクラスのソースに追加するのに適切な一般的な「コア」機能である場合にのみ、拡張メソッドを使用してください。
    • 注意:すでに拡張されているクラスのソースがあり、元のクラスのメンテナが機能を追加することを望まない場合は、拡張メソッドを使用しないことをお勧めします。
  • どこでも利用できるコアライブラリにのみ拡張メソッドを配置します。一部のコードでのみ利用できる拡張は可読性の問題を引き起こします。
  • 拡張メソッドを使用すると常にコードが難読化されるため、それをおこなわないことを第一の選択肢としてください。

refout

  • その値がメソッドの入力値でない場合に out を使用します。
  • そのメソッドのすべての引数のあとに out を使用したパラメータを記述します。
  • ref の使用はまれで、入力値を変更することが必須の場合に使用します。
  • 構造体の受け渡しを最適化するために ref を使用しないでください。
  • 変更可能なコンテナをメソッドに渡すために ref を使用しないでください。ref は提供されたコンテナをまったく異なるコンテナのインスタンスに置き換える必要がある場合にのみ必要です。

LINQ

  • 一般に、LINQ の長いチェーンではなく単一行の LINQ 呼び出しと命令型のコードを優先します。命令型のコードと高度にチェーンされた LINQ を混在させると可読性が落ちます。
  • SQL スタイルの LINQ キーワードよりもメンバー拡張メソッドを優先します。例えば myList where x 形式よりも myList.Where(x) 形式の使用を優先します。
  • Container.ForEach(...) が単一のステートメントよりも長くなる場合は使用を避けてください。

配列 vs リスト

  • 通常、public な変数やプロパティ、戻り値の型には配列よりも List<> の使用を優先します (先述の IList / IEnumerable / IReadOnlyList についてのガイドを心に留めておいてください)。
  • コンテナのサイズが変更される可能性がある場合は List<> の使用を優先します。
  • コンテナのサイズが固定されており、構築時にわかっている場合は配列を優先します。
  • 多次元配列には配列を優先します。
  • 注意事項
    • array と List<> は、どちらも線形で連続したコンテナを表現します。
    • C++ における配列と std::vector と同様に、配列は固定長で、List<> は追加が可能です。
    • 配列のパフォーマンスの方が高い場合もありますが、一般的には List<> の方が柔軟性があります。

フォルダとファイルの配置

  • プロジェクト内で一貫性を保ちましょう。
  • 可能な場合はフラットな構造をお勧めします。

戻り値におけるタプルの使用

  • 一般に、特に複雑な型を返す場合は Tuple<> よりも名前付きクラス型の使用を優先します。

文字列補間 vs String.Format() vs String.Concat vs + 演算子

  • 一般に、特にメッセージのロギングとアサーションには最も読みやすいものを使用してください。
  • 文字列連結における + 演算子の連続使用は重大なメモリチャーンを引き起こす可能性があることに注意してください。
  • パフォーマンスが懸念される場合、StringBuilder は複数の文字列連結に対してより高速になります。

using 宣言

  • 通常、using を使用して長いタイプ名のエイリアスを作成しないでください。多くの場合、これは Tuple<> をクラスに変換する必要があることを示しています。
    • 例えば using RecordList = List<Tuple<int, float>> という表現はタプルではなく名前付きクラスにするべきです。
  • using ステートメントはファイルスコープであるため、使用が制限されていることに注意してください。タイプエイリアスはファイル外のユーザーには使用できません。

オブジェクト初期化構文

以下のような式があるとします。

var x = new SomeClass {
  Property1 = value1,
  Property2 = value2,
};
  • オブジェクト初期化構文は「プレーンな古いデータ」タイプに適しています。
  • コンストラクターを持つクラスまたは構造体にこの構文を使用することは避けてください。
  • 複数の行に分割する場合は1つのブロックレベルをインデントします。

名前空間のネーミング

  • 一般に、名前空間の深さは2レベル以下にする必要があります。
  • ファイル/フォルダのレイアウトを名前空間と一致させないでください。
  • 共有ライブラリ/モジュールには名前空間を使用します。 unity_app のような「アプリケーション」コードの場合、名前空間は必要ありません。
  • 新しいトップレベルの名前空間名は、グローバルに一意かつ識別可能である必要があります。

構造体を戻り値とする場合のデフォルト値と null 戻り値

  • ブール値で「成功」を返し、構造体は out で返すことを優先します。
  • パフォーマンス上の懸念がなく、結果としてコードの可読性が大幅に向上する場合 (例えば null 比較演算子のチェーン vs 深くネストされた if 文など)、 null 許容値型 (Nullable<T>) 構造体を使用することができます。
  • 注意事項
    • null 許容値型 (Nullable<T>) 構造体は便利ですが、Google が避ける「null なら失敗」パターンを強化することにもなります。十分な需要がある場合は、将来 StatusOr と同等のものを調査します。

コンテナ反復中のアイテムの削除

C# は他の多くの言語と同様に、反復中にコンテナからアイテムを削除するための明確なメカニズムを提供しません。これにはいくつかのオプションがあります。

  • ある条件を満たすアイテムを削除するだけでよい場合は someList.RemoveAll(somePredicate) をお勧めします。
  • 反復で他の作業を行う必要がある場合は RemoveAll では不十分な場合があります。一般的な代替パターンは、ループの外側に新しいコンテナを作成し、その新しいコンテナに保持するアイテムを挿入し、反復の最後に元のコンテナを新しいコンテナと入れ替えるというアプローチです。

デリゲートの呼び出し

  • デリゲートを呼び出すときは Invoke() を使用し null 条件演算子を使用します (例: SomeDelegate?.Invoke())。これにより、呼び出しサイトでの呼び出しを「呼ばれているデリゲート」として明確にマークします。ヌルチェックは簡潔で、スレッドの競合状態に対して堅牢です。

var キーワード

  • ノイジーであったり、明白であったり、またはそれほど重要でない型名の記述を回避することで可読性を向上させられる場合は var の使用をお勧めします。
  • 以下のような例では var の使用が推奨されます。
    • var apple = new Apple();var request = Factory.Create<HttpRequest>(); のように型が明白なとき。
  • また、以下のような例では非推奨です。
    • 基本的な型を使用するとき。例: var success = true;
    • コンパイラーによって解決される組み込みの数値型を使用するとき。例: var number = 12 * ReturnsFloat();
    • ユーザーが型を知ることで明らかにメリットが得られる場合。例: var listOfItems = GetList();

属性

  • 属性は関連付けられているフィールド、プロパティ、またはメソッドの上の行に、改行で区切られて表示される必要があります。
  • 複数の属性は改行で区切る必要があります。これにより属性の追加と削除が簡単になり、各属性を簡単に検索できるようになります。

引数のネーミング

Google C++ スタイルガイドから派生したものです。

関数の引数の意味が自明でない場合は、次のいずれかの解決策を検討してください

  • 引数がリテラル定数であり、同じ定数が暗黙のうちに同じであると想定する方法で複数の関数呼び出しで使用される場合は、名前付き定数を使用してその制約を明示し、それが保持されることを保証します。
  • 関数のシグネチャを変更して bool 型の引数を enum に置き換えることを検討してください。これにより引数の値が自己記述的になります。
  • 大きい、または複雑なネストされた式を名前付き変数に置き換えます。
  • 名前付き引数を使用して、呼び出しサイトでの引数の意味を明確にすることを検討してください。
  • 複数の構成オプションがある関数の場合、すべてのオプションを保持し、そのインスタンスを渡す単一のクラスまたは構造体を定義することを検討してください。このアプローチにはいくつかの利点があります。オプションは呼び出しサイトで名前で参照され、その意味が明確になります。また、関数の引数数が減り、関数呼び出しの読み取りと書き込みが容易になります。追加のメリットとして、別のオプションが追加されたときに呼び出しサイトを変更する必要がありません。

次の例を考えてみましょう。

// Bad - これらの引数は何?
DecimalNumber product = CalculateProduct(values, 7, false, null);

これに対する良い例は次のとおりです。

// Good
ProductOptions options = new ProductOptions();
options.PrecisionDecimals = 7;
options.UseCache = CacheUsage.DontUseCache;
DecimalNumber product = CalculateProduct(values, options, completionDelegate: null);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment