プロジェクトのコーディング規約において、クラスメンバーの順序については頭を悩ませる点の一つです。
厳格な規約は整然としたコードを保つのに役立ちますが、一方でコーディングやレビューの際の負担が増加する恐れがあります。
理想としては、規約に応じてクラスメンバーを自動で順序付けしてくれる仕組みを設けたいところです。
この課題を解決するための一つのアプローチとして、Riderの File Layout 機能を紹介します。
どんなことができるようになるのか
たとえば、次のような乱雑なクラスがあるとします。
public class Example
{
private int _privateField;
private readonly int _privateReadonlyField;
private static int _privateStaticField;
public Example() {}
~Example() {}
private class NestedClass {}
private delegate void NestedDelegate();
private enum NestedEnum {}
private event NestedDelegate NestedEvent;
private const int PrivateConstField = 0;
private void PrivateMethod() { }
private int PrivateProperty { get; set; }
protected int ProtectedField;
protected void ProtectedMethod() { }
protected int ProtectedProperty { get; set; }
public int PublicField;
public void PublicMethod() { }
public int PublicProperty { get; set; }
public static void PublicStaticMethod() { }
}
あるルールに従って、例えばC# at Google Style Guideに則った並び替えを行う場合、RiderのFileLayoutを適用すると、以下のように整理されます。
public class Example
{
private class NestedClass {}
private delegate void NestedDelegate();
private enum NestedEnum {}
private event NestedDelegate NestedEvent;
private readonly int _privateReadonlyField;
private static int _privateStaticField;
private const int PrivateConstField = 0;
public int PublicField;
public int PublicProperty { get; set; }
protected int ProtectedField;
protected int ProtectedProperty { get; set; }
private int _privateField;
private int PrivateProperty { get; set; }
public Example() {}
~Example() {}
public void PublicMethod() { }
public static void PublicStaticMethod() { }
protected void ProtectedMethod() { }
private void PrivateMethod() { }
}
C# at Google Style Guide の並び替えルール (クリックで開く)
- クラスメンバーの順序は以下のとおりです:
- クラスメンバーは以下の順序でグループ化します:
- ネストされたクラス、列挙型、デリゲートとイベント
- static、const、readonly フィールド
- フィールドとプロパティ
- コンストラクターとファイナライザー
- メソッド
- 各グループ内での要素の順序は以下のとおりです:
- Public.
- Internal.
- Protected internal.
- Protected.
- Private.
並び替えルールを定義したXAML (クリックで開く)
<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
<TypePattern DisplayName="C# at Google Style Guide" RemoveRegions="All">
<Entry DisplayName="Nested classes, enums, delegates and events.">
<Entry.Match>
<Or>
<Kind Is="Class" />
<Kind Is="Enum" />
<Kind Is="Delegate" />
<Kind Is="Event" />
</Or>
</Entry.Match>
<Entry.SortBy>
<Access Order="Public Internal ProtectedInternal Protected Private" />
</Entry.SortBy>
</Entry>
<Entry DisplayName="Static, const and readonly fields.">
<Entry.Match>
<Or>
<And>
<Kind Is="Field" />
<Static />
</And>
<Kind Is="Constant" />
<And>
<Kind Is="Field" />
<Readonly />
</And>
</Or>
</Entry.Match>
<Entry.SortBy>
<Access Order="Public Internal ProtectedInternal Protected Private" />
</Entry.SortBy>
</Entry>
<Entry DisplayName="Fields and properties.">
<Entry.Match>
<Or>
<Kind Is="Field" />
<Kind Is="Property" />
</Or>
</Entry.Match>
<Entry.SortBy>
<Access Order="Public Internal ProtectedInternal Protected Private" />
</Entry.SortBy>
</Entry>
<Entry DisplayName="Constructors and finalizers.">
<Entry.Match>
<Or>
<Kind Is="Constructor" />
<Kind Is="Destructor" />
</Or>
</Entry.Match>
<Entry.SortBy>
<Static />
<Kind Order="Constructor Destructor" />
<Access Order="Public Internal ProtectedInternal Protected Private" />
</Entry.SortBy>
</Entry>
<Entry DisplayName="Methods.">
<Entry.Match>
<And>
<Kind Is="Method" />
</And>
</Entry.Match>
<Entry.SortBy>
<Access Order="Public Internal ProtectedInternal Protected Private" />
</Entry.SortBy>
</Entry>
</TypePattern>
</Patterns>
File Layoutとは
File Layoutは、クラスメンバーをC#ファイル内で特定の規則に従って自動的に並び替えてくれるRiderの便利機能です。
この機能を使用するには、Code Cleanupのプロファイル内で「Rearrange code -> C# -> Apply file layout」を有効にし、その後Code Cleanupを実行します。
Code Cleanupの使用方法や設定については、以下の記事を参照してください。
File Layoutの並び替えルールの確認/編集
ルールの確認や編集は、「Settings -> Editor -> Code Style -> C# -> File Layout」からアクセスできます。
Unity関連のクラスはUnityタブで、その他のクラスはGeneralタブで各自のルールを設定します。
File Layoutの並び替えルールのXAMLの書き方
ルールを定義するXAMLは、「Patterns -> TypePattern -> Region(Option) -> Entry」という階層で構成されます。
まずTypePattern.Matchを用いて対象タイプを特定し、その後にRegionやEntryによってメンバーの並び順を定義します。
Entry内ではEntry.SortByを使用して並び替えのキーを指定することができます。
以下に、その記述方法の例を示します。
<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
<TypePattern DisplayName="Example Pattern" RemoveRegions="All">
<TypePattern.Match>
<!-- TypePatternに含める条件 -->
<And>
<Kind Is="Class" />
<Name Is=".*Example.*" />
</And>
</TypePattern.Match>
<Region Name="Foo Region">
<Entry DisplayName="Foo Entry">
<Entry.Match>
<!-- Entryに含める条件 -->
<Name Is=".*[Ff]oo.*" />
</Entry.Match>
<Entry.SortBy>
<!-- Entryの並び替え条件 -->
<!-- 第一キー: アクセスレベル, 第二キー: staticかどうか, 第三キー: 名前 -->
<Access Order="Public Internal ProtectedInternal Protected Private" />
<Static />
<Name />
</Entry.SortBy>
</Entry>
</Region>
<Entry DisplayName="Bar Entry">
<Entry.Match>
<!-- Entryに含める条件 -->
<Name Is=".*[Bb]ar.*" />
</Entry.Match>
<!-- Entry.SortByが未指定のため並び替えは行われず、並び順が維持される -->
</Entry>
</TypePattern>
</Patterns>
-
<Patterns>
: このタグで囲われた中でTypePatternを定義します。 -
<TypePattern>
: 対象となる型、およびその型におけるメンバの並び替えルールをここで指定します。-
<TypePattern.Match>
: TypePatternが適応される型を指定する条件です。
-
-
<Entry>
: メンバをグループ化するために使用します。-
<Entry.Match>
: Entryの対象とするメンバの条件を指定します。 -
<Entry.SortBy>
: Entry内での並び替えキーを指定します。
-
-
<Region>
: このタグで囲まれたEntryは、#region
~#endregion
で囲まれます。Entryに属するメンバが一つもなければ、regionも追加されません。
Match で使えるタグ
-
<And>
~</And>
: 要素をAND条件で結びつけるためのタグ。 -
<Or>
~</Or>
: 要素をOR条件で結びつけるためのタグ。 -
<Not>
~</Not>
: 条件の否定を表すためのタグ。 -
<HasMember>
~</HasMember>
: 特定の条件に該当するメンバが存在するかどうかを判定するためのタグ。 -
<Kind Is="..." />
: 型の種類を指定するためのタグ。-
<Kind Is="Class" />
: クラス。 -
<Kind Is="Interface" />
: インターフェイス。 -
<Kind Is="Struct" />
: 構造体。 -
<Kind Is="Enum" />
: 列挙体。 -
<Kind Is="Delegate" />
: デリゲート。 -
<Kind Is="Constant" />
: 定数。 -
<Kind Is="Property" />
: プロパティ。 -
<Kind Is="Indexer" />
: インデクサ。 -
<Kind Is="Field" />
: フィールド。 -
<Kind Is="Constructor" />
: コンストラクタ。 -
<Kind Is="Destructor" />
: デストラクタ。 -
<Kind Is="Operator" />
: オペレータ。 -
<Kind Is="Method" />
: メソッド。オペレータを含む。 -
<Kind Is="Member" />
: デリゲート, 定数, プロパティ, インデクサ, フィールド, コンストラクタ, デストラクタ, メソッド, オペレータのいずれか。 -
<Kind Is="Type" />
: クラス, インターフェイス, 構造体, 列挙体, デリゲートのいずれか。
-
-
<Access Is="..." />
: アクセス修飾子を指定するためのタグ。-
<Access Is="Public" />
: public修飾子を持つ。 -
<Access Is="Internal" />
: internal修飾子を持つ。 -
<Access Is="ProtectedInternal" />
: protected internal修飾子を持つ。 -
<Access Is="Protected" />
: protected修飾子を持つ。 -
<Access Is="Private" />
: private修飾子を持つ。
-
-
<Name Is="..." />
: 名前のパターンを指定するためのタグ。正規表現も利用可能。- [例]
<Name Is="^On" />
: "On"から始まる名前。
- [例]
-
<Readonly />
: readonly修飾子を持つ。 -
<Static />
: static修飾子を持つ。 -
<Virtual />
: virtual修飾子を持つ。 -
<Override />
: override修飾子を持つ。 -
<HandlesEvent />
: イベントハンドラーメソッド。 -
<ImplementsInterface />
: インターフェイスの実装。-
Name="..."
属性: インターフェイス名を指定するための属性。正規表現も利用可能。
-
-
<HasAttribute />
: 属性を持つことをを指定するためのタグ。-
Name="..."
: 属性名を指定するための属性。正規表現も利用可能。
-
SortBy で使えるタグ
-
<Kind Order="..." />
: 型の並び順を指定するタグ。複数の型をスペース区切りで設定可能。-
<Kind Order="Constructor Destructor" />
: コンストラクタ->デストラクタ->その他の順で並び替える。(一例)
-
-
<Access Order="..." />
: アクセス修飾子に基づきメンバの並び順を指定するタグ。複数のアクセス修飾子をスペース区切りで設定可能。-
<Access Order="Public Protected Private" />
: public -> protected -> private -> その他の順で並び替える。
-
-
<Name />
: 名前に基づきメンバをJISコード順に並び替える。 - Matchで使えるタグ: 一致しているメンバの優先度を上げる。
複数のTypePattern, Entryの条件に一致する場合の優先度
以下の順で優先度が評価されます。
-
Priority
属性の値が高い方。 - 制約が強い方。
TypePattern, EntryタグではPriority
属性を指定することができ、デフォルトは50
になっています。
指定できる値は 0, 25, 50, 100, 150のいずれかの値に制限されています。
(それ以外の値を指定するとエラー)
「制約の強さ」とは、おそらくMatchで指定した条件の具体度のことで、ざっくり次のような感じです。
- 条件指定あり > 条件指定なし
- public static > public
これらはわかりやすいケースですが、詳細はあまり言及されていないためよくわかっていません。
もし優先度を調整する必要がある場合は、Priority
属性を設定するのがベターです。
特定の型でFile Layoutによる並び替えを無効化する方法
特定の型でFile Layoutの並び替え機能を無効にしたい場合は、TypePattern.Match
に条件を指定し、TypePattern.SortBy
を省略することで実現できます。
たとえば、デフォルトのXAML設定には以下のようなTypePatternが含まれています。
<TypePattern DisplayName="Non-reorderable types">
<TypePattern.Match>
<Or>
<And>
<Kind Is="Interface" />
<Or>
<HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
<HasAttribute Name="System.Runtime.InteropServices.ComImport" />
</Or>
</And>
<Kind Is="Struct" />
<HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" />
<HasAttribute Name="JetBrains.Annotations.NoReorder" />
</Or>
</TypePattern.Match>
</TypePattern>
これは、以下の型に対しては並び替えを行わない、というルールを定義しています。
- COMオブジェクトのInterface
- 構造体
-
NoReorderAttribute
を持つ型(class, interface)
NoReorderAttributeはFile Layout用に用意された属性ですが、機能を有効にするためには上記のTypePatternのような定義が必要です。
Regionがある場合の並び替えについて
既存のRegion内ではメンバーの並び替えは実行されません。
TypePatternにRemoveRegions="All"
の属性をつけると、既存のRegionを全て除去してから並び替えとRegionの追加が行われるので、必ずつけるようにしましょう。
File Layoutを使用する際は、XAMLに定義していないRegionは切らないようにするのがいいと思います。
部分的な名前一致を用いたRegion内の項目のグループ化
Region.GroupByタグを使用することで、部分的な名前の一致に基づいてクラスメンバをグループ化し、それぞれに対応するRegionを自動的に作成することができます。
こちらは、ドキュメント内で紹介されているXAMLの実装例と、File Layoutを適用した後のC#クラスのコードです。
<TypePattern>
<Region Name="${Name}" Priority="100">
<Region.GroupBy>
<Name Is="(test\w+?)_.*" />
</Region.GroupBy>
<Entry>
<Entry.Match>
<Name Is="test\w+?_.*" />
</Entry.Match>
</Entry>
</Region>
</TypePattern>
#region test_groupOne
public void test_groupOne_nameOne(){ /*...*/ }
public void test_groupOne_nameTwo(){ /*...*/ }
#endregion
#region test_groupTwo
public void test_groupTwo_nameOne(){ /*...*/ }
public void test_groupTwo_nameTwo(){ /*...*/ }
#endregion
Region.GroupByタグ内に記述されたは、正規表現のキャプチャグループ1を使用しています。
これにより、キャプチャとそれに一致したメンバごとにRegionが作成されます。
また、<Region Name="${Name}">
という形でRegionの名前指定時にキャプチャの参照もできます。
Unity専用クラスの並び替えルールについて
File LayoutにてUnity専用クラスの配置ルールを定義する際は、Patterns
タグに次の名前空間 xmlns:unity="urn:schemas-jetbrains-com:member-reordering-patterns-unity" を指定することで、Unity関連の拡張タグを活用することができます。
以下に代表的なUnity専用タグを紹介します。
-
<unity:SerializableClass />
: Unityでシリアライズ可能なクラスを指定するためのタグ。 -
<unity:SerializedField />
:シリアライズ可能なフィールドを指定するためのタグ。 -
<unity:AutoPropertyWithSerializedBackingField />
: SerializeField属性をバッキングフィールドに付けた自動実装プロパティを指定するためのタグ。例:[field: SerializeField] public int PublicProperty { get; set; }
-
<unity:EventFunction />
: Unityのイベント関数を指定するためのタグ。 -
<unity:EventFunctionName />
: Unityのイベント関数名を指定するためのタグ。
以下に、これらのタグを使用したXAMLの例を示し、MonoBehaviour
継承クラスに適用した例も併せて掲載します。
XAML例 (クリックで開く)
<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns" xmlns:unity="urn:schemas-jetbrains-com:member-reordering-patterns-unity">
<TypePattern DisplayName="Unity classes" Priority="100">
<TypePattern.Match>
<unity:SerializableClass />
</TypePattern.Match>
<Region Name="SerializedField">
<Entry>
<Entry.Match>
<unity:SerializedField />
</Entry.Match>
</Entry>
</Region>
<Region Name="AutoPropertyWithSerializedBackingField">
<Entry>
<Entry.Match>
<unity:AutoPropertyWithSerializedBackingField />
</Entry.Match>
</Entry>
</Region>
<Region Name="EventFunction">
<Entry>
<Entry.Match>
<unity:EventFunction />
</Entry.Match>
<Entry.SortBy>
<unity:EventFunctionName />
</Entry.SortBy>
</Entry>
</Region>
<Region Name="Others">
<Entry />
</Region>
</TypePattern>
</Patterns>
MonoBehaviour継承クラス適用例 (クリックで展開)
public class ExampleMonoBehaviour : MonoBehaviour
{
#region SerializedField
public int PublicField;
[SerializeField] private int _privateField;
#endregion
#region AutoPropertyWithSerializedBackingField
[field: SerializeField] public int PublicProperty { get; set; }
#endregion
#region EventFunction
private void Awake() {}
private void Reset() {}
private void OnEnable() {}
private void OnDisable() {}
private void OnDestroy() {}
#endregion
#region Other Region
[NonSerialized] public int NonSerializedPublicField;
private int _nonSerializedPrivateField;
private void Start() {}
private void Update() {}
private void FixedUpdate() {}
private void LateUpdate() {}
private void OnApplicationFocus() {}
private void OnApplicationPause() {}
private void OnApplicationQuit() {}
private void OnCollisionEnter() {}
private void OnCollisionExit() {}
private void OnCollisionStay() {}
private void OnTriggerEnter() {}
private void OnTriggerExit() {}
private void OnTriggerStay() {}
#endregion
}
デフォルトのXAMLでは、次のようにシリアライズ可能なフィールドはInspectorウィンドウでの順序を維持するため、その並び替えが行われないようになっています。
<Entry DisplayName="Serialized Fields">
<Entry.Match>
<!--
Keep serialised fields and auto-properties with serialised backing
fields together, unsorted, to maintain order in the Inspector
window
-->
<Or>
<unity:SerializedField />
<unity:AutoPropertyWithSerializedBackingField />
</Or>
</Entry.Match>
<!-- No sorting -->
</Entry>
このような設定は、File LayoutがInspector上での表示順に影響を及ぼさないようにするためのもので、通常は変更せずに使用するのが望ましいでしょう。
File Layoutの並び替えルールを保存してチームに共有する
並び替えルールを保存する際は、右下の "Save" ボタン右のプルダウンから "Solution {プロジェクト名} team-shared" 選択することで、プロジェクトのルートに {プロジェクト名}.sln.DotSettings として保存されます。
このファイルをGitにコミットすることで、並び替えルールの設定を共有することが可能です。
なお、Riderは保存時のプルダウンの選択によって保存先レイヤーが変わります。
- "Solution {プロジェクト名} personal" : 'Solution personal' layer
- "Solution {プロジェクト名} team-shared" : 'Solution team-shared' layer
- "Save" もしくは "This computer" : 'This computer' layer
設定項目の優先度は Solution personal > Solution team-shared > This computer となっており、各設定項目で最も優先度の高いレイヤーが適用されます。
保存先のレイヤーを間違えると、想定外の挙動が起こる原因になるので注意しましょう。
理想的なコードの整形を実現するためのコードスタイル設定の調整
理想的な形へ整形するには、コードスタイルの設定を適切に調整する必要があります。
たとえば、以下のように余分な空白行があるクラスがあったとします。
public class ExampleClass
{
private int _privateField;
protected int ProtectedField;
public int PublicField;
}
この例で、次のようなメンバー並び替えルールを設定したとします。
<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
<TypePattern RemoveRegions="All">
<Entry>
<Entry.Match>
<Kind Is="Field" />
</Entry.Match>
<Entry.SortBy>
<Access Order="Public Protected Private" />
</Entry.SortBy>
</Entry>
</TypePattern>
</Patterns>
デフォルトのコードスタイル設定でFile Layoutを適用すると、以下のような結果になります。
public class ExampleClass
{
public int PublicField;
protected int ProtectedField;
private int _privateField;
}
不自然な空白行が残ってしまっています。
もともと3行と1行の空白行があったコードが整形された結果、2行の空白行が残るのはなぜなのか。
主な要因は、次のコードスタイル設定にあります。
- "Keep max blank lines in declarations" (宣言内の最大空白行を維持する): 2
- "Remove blank lines after '{' and before '}' in declarations" (宣言内の'{' の後と '}' の前にある空白業を除去): ON
次のようなにコード整形が行われ、先ほどの結果になっていると思われます。
- File Layoutによる並び替え実行。
-
ProtectedField
の前にある空白行数が3から2行に。 -
PublicField
の前の1行の空白は、'{'の直後に位置するため除去。
常に同じ整形結果を得るためには、"Keep max blank lines in declarations"は0にする必要があります。
あとは、プロジェクトのルールや個人の好みに応じて、他の設定項目も適宜調整してください。
常にFile Layoutが適用されるようにする
チームプロジェクトにおいてFile Layoutを取り入れる場合、Code Cleanupをかけていないコードをコミットすると、他のメンバーによるCode Cleanup時に不要な差分が発生する可能性があります。
そのため、コミット前に必ずCode Cleanupがされるような仕組みを用意したいところです。
一つの簡単な解決策として、「Settings -> Tools -> Actions on Save -> Reformat and Cleanup Code(保存時のコード再フォーマットとクリーンアップ)」を有効化する方法があります。
右側のメニューからは、「Apply file layout」がオンになっているプロファイルを選択し、「Whole file(ファイル全体)」オプションを選ぶようにします。
この設定は手動でファイルを保存する場合にのみ適応されるため、次の自動保存関連の設定を無効化することで、より確実にこの機能を利用できます。
Settings -> Appearance & Behaviour -> System Settings -> Autosave の画面の
- Save files if the IDE is idle for N seconds. (IDEがN秒間アイドル状態の場合にファイルを保存する)
- Save files when switching to different application or a built-in terminal. (別のアプリケーションや組み込みのターミナルに切り替える際にファイルを保存する)
ただし、この方法では保存の度にファイルの並び替えが行われるため、その点がイマイチだと感じています。
(もしこの方法よりも良い手法をご存じでしたら、情報共有をお願いします)
最後に
RiderのFile Layout機能を利用して、効率的かつ簡単にクラスメンバを理想的な順序に整理することができました。
私が所属するチームでは、チームの合意をとった上でFile Layoutをプロジェクトに導入し、それがクラスメンバの並び替えルールに関する議論を促す一因となっています。
これからも適宜ルールの適正化に励んでいこうと思っています。
みなさんのプロジェクトでもFile Layoutを活用されてみてはいかがでしょうか。
以上、最後まで読んで頂き、ありがとうございました。
参考したもの
https://gist.github.com/VacuumBreather/672ca1c8005deb0de94d77cfc944cb17
https://gist.github.com/VacuumBreather/013139478373189cff3ed8ebbee556cb
この記事のタイトルはChatGPTに考えてもらいました。
記事全体の添削もしてもらいました。ありがとうChatGPT。
-
正規表現では
(
~)
で囲った部分をキャプチャグループ、キャプチャグループに一致した文字列をキャプチャと呼びます。 ↩