sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# コーディングガイドライン&プラクティス 2021

f:id:shikaku_sh:20210823173353p:plain:w300

この記事は、Microsoft MVP のひとり Vincent Maverick Sanchez Durano のブログ記事「C# Coding Guidelines and Practices - 2021」を個人的に雑訳したものです。

この記事では、一般的なソフトウェアエンジニアリング ガイドラインを紹介します。これらのガイドラインのほとんどは、業界で一般的に使用されているものであり、これらを使用することで、あなたのコードが他の人にも読みやすくなります。

コーディングの標準は 任意のもの (arbitrary) であることは事実です。しかし、メンテナンス制の高いプロジェクトを成功させるためにどのようなコーディングスタイルに従うのかではなくて、一貫性を保つことが重要です。

ここでは、コードをどのようにインデント(タブ、スペース、{} の位置)するべきかということではなくて、管理しやすいコードを書くためのガイドラインです。

とはいえ、もしあなたがチームを率いているなら、あるいはひとりの開発者であってもより良いものを目指したいと思っているなら、コーディングガイドラインを用意することは、より良いものを実現する優れたスタートとなります。

それでは早速、本題に入っていきます。

Tips

#1

つぎのような if-else 文は避けること:

bool result;
if (condition)
{
   result = true;
}
else
{
   result = false;
}

代わりに三項演算子 (ternary conditional operator) を使います:

bool result = condition ? true: false;

コードはよりすっきりして、読みやすく理解しやすくなりました。それらに加えて、より簡潔です。

#2

つぎのように null チェックのために if 文を使うことを避けること:

if (something != null)
{
    if (other != null)
    {
        return whatever;
    }
}

代わりに null 条件演算子 (null conditional) を使います:

return something?.other?.whatever;

コードは、よりクリーンで簡潔なものになります。

#3

null チェックのためにややこしい if-else 文を使うことを避けること:

if (something != null)
{
    if (other != null)
    {
        return whatever;
    }
    else 
    {
        return string.empty;
    }
}
else
{
    return string.empty;
}

代わりに ?? 演算子 (null coalescing) を使います:

return something?.other?.whatever ?? string.empty;

#4

オブジェクトのデフォルト値が null のとき、つぎのようなコードを使わないこと:

int? number = null;
var n = number.HasValue ? number : 0;

代わりに ?? 演算子 (null coalescing) か、GetValueOrDefault メソッドを使うこと:

var n = number ?? 0;
or
var n = number.GetValueOrDefault();

#5

nullable な変数のチェックは等値演算子 == や HasValue を使わないこと:

int? number = null;

if (number == null)
{
    //do something
}

if (!number.HasValue)
{
    //do something
}

is を使うことで、さらに改善することができます:

int? number = null;

if (number is null)
{
    //do something
}

意図がはっきりするので、とても読みやすくなります。

#6

単純な if, for, foreach 文でも、{} のないコードを避けること:

if(conditioin) action;

中括弧がないと、2行目を誤って追加してしまうことで、if 文に含まれていないのに含まれていると勘違いするコードを書きます。

必ず中括弧を使用しましょう:

if (condition) { action; }

//or better
if (condition) 
{ 
    action; 
}

#7

次のような if-else 文の連続を使わないこと:

if (condition)
{
   //do something
}
else if(condition)
{
   //do something
}
else if(condition)
{
   //do something
}
else(condition)
{
   //do something else
}

代わりに switch を使うこと:

switch(condition)
{
   case 1:
      //do something
      break;
   case 2:
      //do something
      break;
   case 3:
      //do something
      break;
   default:
      //do something else
     break;
}

ただし、可能なら従来の switch よりも switch の式 (switch statements) のほうが好ましい:

condition switch
{
    1 => //do something;
    2 => //do something;
    3 => //do something;
    _ => //do something else;
}

より簡潔で、読みやすくて理解しやすくなりました。(注:C# 8.0 からの機能です)

例外:switch よりも if-else 文のほうが意味を持つことがあります。例えば、条件の異なるオブジェクトや複雑な条件が含まれている場合などです。どちらがよいかは判断に任せます。

#8

リソースを使うオブジェクト、IDisposable インターフェースを実装したオブジェクトを扱うときは、必ず using ステートメントを使用すること:

using (MemoryStream stream = new MemoryStream()) 
{
    // do something
}

C# 8.0 で導入された新しい using を使っても良い:

using var stream = new MemoryStream();
// do something

新しい using は、メソッドの中で中括弧の数を減らし、リソースを破棄する箇所を簡単に見出すことができます。詳細は「Microsoft Docs - "pattern-based using" and "using declarations"」を参照します。

#9

テキストを + で連結しないこと:

string name = "Vianne";
string greetings = "Hello " + name + "!";

代わりに string.Format() 、または、文字列補間 $ (tring interpolation) を使用すること:

string name = "Vynn";
string greetings = string.Format("Hello {0}!", name);
or
string name = "Vjor";
string greeting = $"Hello, {name}!;

簡潔で読みやすいコードになります。

どっちもコンパイルすると同じコードの意味になっていたから。ただし、C# 10.0 からは文字列補間の関係から後者の $ を使ったほうがパフォーマンスに優れる場合がある。

#10

単純なオブジェクトのテキストをフォーマットするときは、なるべく string.Format() を避けること:

var date = DateTime.Now;
string greetings = string.Format("Today is {0}, the time is {1:HH:mm} now.", date.DayOfWeek, date);

代わりに文字列補間を使うこと:

var date = DateTime.Now;
string greetings = $"Today is {date.DayOfWeek}, the time is {date:HH:mm} now.");

簡潔で理解しやすいコードになります。しかし、string.Format() を使ったほうがよい場合もあります。例えば、複雑な書式設定やデータ操作をする場合などです。適用するべきだとあなたが判断するときに string.Formart() を使用してください。

#11

変数を定義するとき、(特に複雑なオブジェクトに対して)型を指定することを避けること:

List<Repository.DataAccessLayer.Whatever> listOfBlah = _repo.DataAccessLayer.GetWhatever();

代わりに var を使う。他のローカル変数も同じ:

var listOfBlah = _repo.DataAccessLayer.GetWhatever();
and
var students = new List<Students>(); 
var memoryStream = new MemoryStream();
var dateUntilProgramExpiry = DateTime.Now; 

#12

中括弧を使った1行のメソッド実装は避けること:

public string Greeter(string name)
{
    return $"Hello {name}!";
}

代わりに => (Expression-bodied) を使った実装にする:

public string Greeter(string name) => $"Hello {name}!";

読みやすさを維持しつつ、より簡潔になります。

#13

つぎのようなオブジェクトの初期化は避けること:

Person person = new Person();
person.FirstName = "Vianne";
person.LastName = "Durano";

代わりにオブジェクトやコレクションの初期化子を使用します。

var person = new Person { 
    FirstName = "Vianne",
    LastName = "Durano"
};

プロパティは中括弧の中で定義されているので、より自然に読むことができるうえ、意図もはっきりする。

#14

単純な2つのプロパティの値を持つだけの結果セットのクラスを作るのを避けること:

public Person GetName()
{
    var person = new Person
    {
        FirstName = "Vincent",
        LastName = "Durano"
    };
    
    return person;
}

代わりに Tuple を使うこと:

public (string FirstName, string LastName) GetName()
{
    return ("Vincent", "Durano");
}

#15

変換(conversion, transformation)・検証・フォーマット・解析などの一般的なタスクを実行する Extension Methods を作成してみてください。

つぎのようにはしません:

string dateString = "40/1001/2021";
var isDateValid = DateTime.TryParse(dateString, our var date);

このコードでも問題なく変換をすることができます。しかし、基本的な変換をするだけにしては、少々(コードが)長くなっています。

プロジェクトの様々な箇所で同じ変換のコードが散らかっている様を想像してみてください。これでは、コードが乱雑になって開発にかかる時間が長くなってしまう恐れがあります。

こうしたことを防ぐために、プロジェクト間で再利用可能な共通タスク(変換など)を実行するヘルパー/ユーティリティの関数を作成することを検討するべきです。例えば、さっきのコードはつぎのような拡張機能です:

public static class DateExtensions
{
     public static DateTime ToDateTime(this string value)
         => DateTime.TryParse(value, out var result) ? result : default;
}

これを用意することで、拡張メソッドをどこのコードからでも呼び出すことができます:

var date = "40/1001/2021".ToDateTime();

このコードは、コードを簡潔でわかりやすくして、利便性を高めています。このような一般的なタスクをする NuGet パッケージを作成しました。興味のある方はパッケージを入手できます。

#16

.NET の定義済のデータ型 Int32, String, Boolean の使用を避けること:

String firstName; 
Int32 orderCount; 
Boolean isCompleted; 

代わりに組み込み型 (built-in primitive data types) を使います:

string firstName; 
int orderCount; 
bool isCompleted; 

.NET Framework に準拠、コードをより自然に読みことができます。

#17

識別子の略語 (identifier abbreviations) としてイニシャルを使用しないこと。その主な理由は、同じような名称のクラスが存在すると、混乱や矛盾を引き起こす恐れがあるためです。

private readonly PersonManager _pm;
and
private readonly ProductManager _pm;

つぎのように、明解で簡潔な名前にします:

private readonly PersonManager _personManager;
private readonly ProductManager _productManager;

オブジェクトがなになのか明解に示したことで、よりわかりやすくなります。

#18

名前空間は明解に定義された構造で整理すること。一般的に名前空間は、プロジェクトのフォルダーの階層を反映させるべきです:

namespace ProjectName.App.Web;
namespace ProjectName.Services.Common;
namespace ProjectName.Services.Api.Payment;
namespace ProjectName.Services.Api.Ordering;
namespace ProjectName.Services.Worker.Ordering;

プロジェクトのコードをよく整理して、レイヤー間を簡単に移動できるようにします。

#19

クラスの名前には単数形、名詞、名詞句を使うこと:

public class Person
{
    //some code
}

public class BusinessLocation
{
    //some code
}

public class DocumentCollection
{
    //some code
}

こうすることで、あるオブジェクトが単一のアイテムの値を保持しているのか、コレクションを保持しているのかを簡単に判断することができます。

例えば、List<People> vs List<Person> を想像してみてください。リストやコレクションに複数形の名前を入れると、奇妙なことになります。

わかるとおもうけど、People が複数形です。

#20

名詞や形容詞句をプロパティの名前に使用すること。bool 型のプロパティや変数には "can, is, has" のような接頭辞 (prefix) をつけることができる:

public bool IsActive { get; set; }
public bool CanDelete { get; set; }

//variables
bool hasActiveSessions = false;
bool doesItemExist = true;

接頭辞を追加することで、呼び出す側での価値をより良くすることができます。

#21

クラス・メソッド・プロパティ・定数の変数名には Pascal Casing を使用すること:

public class ClassName 
{ 
    const int MaxPageSize = 100;
    
    public string PropertyName { get; set; } 
    
    public void MethodName() 
    { 
        //do something
    } 
} 

コードを Microsoft .NET Framework と一貫性のあるようにするためです。

Unity は注意が必要だと思います。

#22

メソッドの引数やローカル変数には Camel Casing を使用すること:

public void MethodName(CreatePersonRequestDto requestDto) 
{ 
       var firstName = requestDto.FirstName; 
} 

コードを Microsoft .NET Framework と一貫性のあるようにするためです。

#23

クラス・メソッド・プロパティは、意味のあるわかりやすい (self-explanatory) 名前をつけること:

int daysUntilProgramExpiry;

public List<Person> GetPersonProfileById(long personId)
{
       //do something
}

#24

非同期のメソッドは接尾辞 (suffix) に Async をつけること:

public async Task<List<Person>> GetPersonProfileByIdAsync(long personId)
{
     //do something
}

メソッドを見ただけで、同期 vs 非同期を見分けることができます。

#25

インターフェースの名前は接頭辞に I をつけること:

public interface IPersonManager 
{ 
   //...
} 

インターフェースとクラスを簡単に区別するためです。実際、インターフェースを定義する標準(的な手法)として知られます。

#26

グローバル変数やクラスのメンバー変数には _(アンダースコア)を接頭辞につけること:

private readonly ILogger<ClassName> _logger;
private long _rowsAffected;
private IEnumerable<Persons> _people;

ローカルまたはグローバルな変数/識別子を簡単に区別できるようにします。

#27

(コーディングとして)クラスの一番はじめにすべてのメンバー変数とフィールドを宣言すること:

private static string _externalIdType;
private readonly ILogger<PersonManager> _logger;
private int _age;

一般的に受け入れられている手法です。変数宣言を探す手間を省くことができます。

#28

すべての private メソッドを public メソッドの後に定義すること:

public class SomeClass
{
    private void SomePublicMethodA()
    {

    }

    // rest of your public methods here
    // ...

    private void SomePrivateMethodA()
    {

    }

    private void SomePrivateMethodB()
    {

    }
}

28(おそらく 27 のこと)と同じ理由です。メソッドの宣言を探す手間を省くことができます。

#29

コードを region でまとめないこと:

#region Private Members
    private void SomePrivateMethodA()
    {

    }

    private void SomePrivateMethodB()
    {

    }
#endregion

このやり方は、気がつかないうちにコードを肥大化させる可能性があるコード(の臭い)です。

確かに、私もクラスの中のコードをまとめるために region を何度も使ったことがあります。region の機能は、隠したコード行を知覚的に最大化(再表示)すること以外に、特別な機能や価値を持たないことも気づいています。

複数の開発者でプロジェクトを進めると、他の開発者が自分のコードに region を追加して、時間が経つにつれてコードが(知らず知らずのうちに)どんどんと大きくなっていることがあります。

よい習慣としてクラスはできるだけ小さくすることが推奨されます。

クラスの中にかなりの数のプライベートメソッドがあるときは、それらを別クラスに分割することができます。

#30

一般的によく知られている場合にのみ、短縮した名前 (short-hand names) を使うこと:

private readonly CreateQuestionDefinitionRequestDto _requestDto;

変数/パラメーターがリクエストオブジェクトであることがわかっているときに「createQuestionDefinitionRequestDto」という変数名をつけることは、長すぎるでしょう。

FTP, UI, IO も同じことが言えます。一般的に知られている範囲であれば略語を使ってもよいですが、そうではないなら逆効果(悪影響)になります。

#31

識別子の名前の間に _(アンダースコア)を入れるのを避けること:

public PersonManager person_Manager;
private long rows_Affected;
private DateTime row_updated_date_time;

C# は postgres ではないからです……冗談です。

Microsost .NET Frameworkの規約との一貫性と、コードをより自然に読めるようにするためです。また、下線が見えない「下線ストレス (underline stress)」を避けることができる。

#32

定数や readonly の変数に SCREAMING CAPS を使用しないこと:

public static const string EXTERNALIDTYPE = "ABC"; 
public static const string ENVIRONMENT_VARIABLE_NAME = "TEST"; 

あまりにもたくさん注目を集めてしまうためです。

#33

(インターフェースを除いた)識別子にはハンガリアン記法やその他の識別記法を使用しないこと:

int iCounter; 
string strName;
string spCreateUsers; 
OrderingService svcOrdering;

Visual Studio のコードエディターは、オブジェクトの型を判断するための便利な tooltips がすでに提供されています。一般的に、識別子の(名前の)中に型の指標となるものを含めることは避けたほうが良いです。

#34

enum 型の名前には "Enum" のような接尾辞を使用しないこと。複数形の名称をしないこと:

public enum BeerType
{
    Lager,
    Ale,
    Ipa,
    Porter,
    Pilsner
} 

繰り返しですが、Microsoft .NET Framework との一貫性を保つためで、識別子の中に型の指標となるものを入れることを避けています。

#35

不変のオブジェクトには、record 型を使うこと。record は C# 9.0 で導入された新機能で、コードをシンプルにします。例えば、つぎのようなコード(これは record ではなくクラス)です:

public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

(このクラスは)record 型を使って、つぎのように書くことができる。

public record Person(string FirstName, string LastName);

record 型を使用することで、定型的なコードが自動的に生成することが出来る。コードを簡潔に保つことができます。record 型は DTO, Commands といった不変のデータを定義する際にとても役立ちます。

この機能についての詳細は、つぎを参照します。

感想

一貫性については、リーダブルコードだと「個人的な好みと一貫性」で、ベタープログラマだと「見かけのよい状態を維持する」でそれぞれ説明があります。

32 は個人的に慣例的なところがあるので難しい気がしました。

C# 8.0 や 9.0 の書き方がガイドラインに載ってくる感じが新しいですね。C# 9.0 は 2020 年 11 月ごろのリリースだったけど、元の記事は2月に書かれているため、すぐに 9.0 のスタイルも取り入れていることがわかります。

そもそも、C# の新機能はオープンソース化してからフィードバックが早くなり、言語として変化の流れも昔より早い。コーダーも進化を早める必要があるのと、リリースされたなら基本的には使ったほうが便利なはず。(じゃないと、フィードバックのやりとりの意味がわからない)

ただし、Visual Studio をさっさと新しいやつに入れ替えてね、ってなる。

2021 年も終わりが近づく今日だと .NET6 も出て、C# 10.0 も新機能としてフォローし始めるころ。

  • record は record struct の値型レコードが追加
  • 文字列補間が改善
  • 名前空間を宣言するようにできる
  • global using が便利

など、コーディングガイドラインはまたちょっと考えたほうがよいところがありそうです。

全体に影響がありそうで、わかりやすいのは:

  • 名前空間の宣言の仕方
  • global using
  • (文字列補間)

個人的に、名前空間の宣言の仕方は好感が持てる。インデントの問題で書き換えたい。(自動生成コードが邪魔だ)

補足

2, 3 は以下のミスかも。

class Somthing
{
    public Other other;
}
class Other
{
    public object whatever;
}

// 上の定義なら以下のように書ける
var something = new Something { other = new Other() };

if (something != null)
{
    if (something.other != null)
    {
        return something.other.whatever;
    }
}

or

return something?.other?.whatever;

11は好みがありそう。

var list = new List<int>();
or
List<int> list = new();

一応、IL 上での違いはないです。

public class C {
    public void M() 
    {
        var list1 = new List<int>();
        List<int> list2 = new();

        Console.WriteLine(list1);
        Console.WriteLine(list2);        
    }
}
.method public hidebysig 
instance void M () cil managed 
{
// Method begins at RVA 0x2050
// Code size 28 (0x1c)
.maxstack 1
.locals init (
    [0] class [System.Private.CoreLib]System.Collections.Generic.List`1<int32> list1,
    [1] class [System.Private.CoreLib]System.Collections.Generic.List`1<int32> list2
)

IL_0000: nop
IL_0001: newobj instance void class [System.Private.CoreLib]System.Collections.Generic.List`1<int32>::.ctor()
IL_0006: stloc.0
IL_0007: newobj instance void class [System.Private.CoreLib]System.Collections.Generic.List`1<int32>::.ctor()
IL_000c: stloc.1
IL_000d: ldloc.0
IL_000e: call void [System.Console]System.Console::WriteLine(object)
IL_0013: nop
IL_0014: ldloc.1
IL_0015: call void [System.Console]System.Console::WriteLine(object)
IL_001a: nop
IL_001b: ret
} // end of method C::M

参考