OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

【C# 機能別】知らん書き方が出てきたらこれを見ろ!C# 10 までの進化を1ページで

祝 .NET 6 GA アドベントカレンダー、23日の記事になります。

.NET 6 のリリースに伴い、C# の言語バージョンがついに 10.0 となりました。

C# の進化は早く、ちょっと気を抜いている隙に、見たことのない書き方のコードがどんどん出現します。
その一方で、業務の現場では、5年前10年前に書かれたソースコードを保守することも決して珍しくありません。

新しいコードでも古いコードでも、「なんだっけこれ?」という書き方がでてきたときに、同じことを従来の書き方/現在の書き方でどうやるかのリファレンスにできるよう、主要な機能・構文ごとに縦断的に整理してみました。

以下お品書きです。

言語バージョンと対応フレームワーク

C# のバージョンごとに、おおよそ同時期のフレームワークと Visual Studio バージョンをまとめておきますので参考にしてください*1

C# 発表年 .NET Framework .NET Core .NET Visual Studio
1.0 2002 1.0 2002
1.2 2003 1.1 2003
2.0 2005 2.0 2005
3.0 2007 3.5 2008
4.0 2010 4.0 2010
5.0 2012 4.5 2012
6.0 2015 4.6 1.0 2015
7.0 2017 4.7 2017
7.1 2017 2.0 2017
7.2 2017 2.1 2017
7.3 2018 4.8 2.2 2017
8.0 2019 3.0 2019
9.0 2020 5 2019
10.0 2021 6 2022

おことわり

  • 各機能の登場バージョンについては極力正確を期すよう努めましたが、間違いがあったらごめんなさい
  • サンプルコードについて、当該機能と関係ない点については、必ずしも同言語バージョンで可能な記法になっていないかもしれません
    • たとえば C# 1.0 のコードで var を使ってたりするかも

プロパティ

C# 1.0 : get / set

C# では、Javaでいうgetter, setterメソッドにあたるものを、プロパティとして実装することができます。

class Person
{
    private string _name;

    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
        }
    }
}

---

Person person = new Person();
person.Name = "tanaka";
Console.WriteLine(person.Name);

C# 2.0 からは、get/set の片方のみアクセシビリティを変えることも可能になりました。

    public string Name
    {
        get
        {
            return _name;
        }
        // set だけ private
        private set
        {
            _name = value;
        }
    }

C# 3.0 : 自動実装プロパティ

自動実装プロパティ の構文が追加され、privateフィールドをラップするだけのプロパティであれば実装を省略できるようになりました。

class Person
{
    public string Name { get; set; }
}

C# 6.0 : get-only プロパティと初期化子

自動実装プロパティを任意の値で初期化できるようになりました。
同時に、get のみ(=読み取り専用)のプロパティが定義できるようになりました。

class Person
{
    public string Name { get; } = "tanaka";
}

また、C# 6.0 では 式形式メンバ の記法が追加され、get のみのプロパティは式形式で書くことができるようになりました。

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

    // FullName { get { return $"{FirstName} {LastName}"; } } と書くのと同じ
    public string FullName => $"{FirstName} {LastName}";
}

C# 7.0 からは、get/set のそれぞれを式形式で定義することも可能になりました。

C# 9.0 : init-only プロパティ

set の代わりに、初期化のみ可能な init というアクセサが定義できるようになりました。
get-only プロパティと似ていますが、オブジェクト初期化子での値設定が可能です*2

class Person
{
    public string Name { get; init; }
}

---

var person = new Person { Name = "tanaka" };

// コンパイルエラー
person.Name = "sato";

関連:クラス・構造体・レコード > オブジェクト初期化子

値の参照渡し

C# 1.0 : ref / out

C# では当初から、引数の参照渡しの方法として、 ref / out 修飾子を使うことができました。

// out 引数のメソッド
public bool TryGetValue(out int value)
{
    if (!someCondition)
    {
        value = 0;  // 必ず初期化して返す必要あり
        return false;
    }
    value = this.someValue;
    return true;
}

---

// 呼び出し側
int val;
if (TryGetValue(out val))
{
    Console.WriteLine(val);
}
// ref 引数のメソッド
public bool TryCountUp(ref int count)
{
    if (!someCondition)
    {
        return false;
    }
    count++;
    return true;
}

---

// 呼び出し側
int count = 0;  // 呼び出し元で初期化されている必要あり
while(TryCountUp(ref count))
{
    Console.WriteLine(count);
}

C# 7.0 : out 変数宣言、参照ローカル変数、ref 戻り値

out 引数の利用時に同時に変数宣言できるようになり、あらかじめ変数を宣言しておく必要がなくなりました。

if (TryGetValue(out int val))  // ここで変数を宣言できる
{
    Console.WriteLine(val);
}

また、ローカル変数に ref を付与し、 参照ローカル変数 としてエイリアス的なものを宣言できるようになりました。

var num = 0;
ref var numRef = ref num;

num = 10;
Console.WriteLine(num);     // 10
Console.WriteLine(numRef);  // 10

numRef = 99;
Console.WriteLine(num);     // 99
Console.WriteLine(numRef);  // 99

// C# 7.3 から ref の付け替えも可能
var num2 = 1;
numRef = ref num2;
num2 = 1111;
Console.WriteLine(numRef);  // 1111

さらに、メソッドの戻り値としても ref を返すことができるようになりました。

private int num = 1;

public ref int GetNumRef()
{
    return ref this.num;
}

---

// 値として受け取る
var numVal = GetNumRef();
numVal = 99;
Console.WriteLine(numVal);      // 99
Console.WriteLine(this.num);    // 1

// 参照を受け取る
ref var numRef = ref GetNumRef();
numRef = 99;
Console.WriteLine(numRef);      // 99
Console.WriteLine(this.num);    // 99

C# 7.2 : 読み取り専用の参照渡し

書き替えできない変数の参照渡しを表す in 引数 が追加されました。

public struct Point
{
    public int x;
    public int y;
}

public Point Add(in Point p1, in Point p2)
{
    // 参照渡しだが、書き替えはできない
    // p1 = p2;
    // p1.x = 0;

    var p = new Point();
    p.x = p1.x + p2.x;
    p.y = p1.y + p2.y;
    return p;
}

さらに ref 戻り値についても、参照先が書き替え不可であることを示す ref readonly が追加されました。

private int num = 1;

public ref readonly int GetNumRef()
{
    return ref this.num;
}

---

// 呼び出し側
ref readonly var numRef = ref GetNumRef();
// エラー
// numRef = 99;

クラス・構造体・レコード

C# 1.0 : クラスと構造体

当初より C# には、クラスと構造体がありました。

クラスは参照型であるのに対し、構造体は値型です*3

構造体は値型であるゆえに、初期値が決まっていなければならず、そのために生成の仕方についてクラスよりも若干の制限があります。

class PersonClass
{
    private string name = "";   // フィールド初期化子
    private int age;

    public PersonClass(string name, int age)
    {
        this.name = name;
        this.age = age;
    }
    public PersonClass(string name)
    {
        this.name = name;
    }
    public PersonClass() { }
}

struct PersonStruct
{
    // フィールド初期化子は不可
    // private string name = "";
    private string name;
    private int age;

    public PersonStruct(string name, int age)
    {
        this.name = name;
        this.age = age;
    }
    public PersonStruct(string name)
    {
        this.name = name;

        this.age = 0;   // ageにも明示的に割り当てないとコンパイルエラー
    }
    // 引数のないコンストラクタは定義できない(自動的に生成される)
    // public PersonStruct() { }
}

C# 2.0 : 部分型

partial 修飾子により、クラスや構造体を分割定義できるようになりました。

特に Windows.Forms において、GUIのデザイナから自動生成されるレイアウトクラスと、そのクラスのロジックを別ファイルで管理するために使用されました*4

partial class PersonClass
{
    private string name = "";
    public PersonClass(string name)
    {
        this.name = name;
    }
    public PersonClass()
    {
    }
}
partial class PersonClass
{
    private int age;

    public PersonClass(string name, int age)
    {
        this.name = name;
        this.age = age;
    }
}

C# 3.0 : オブジェクト初期化子と匿名型

オブジェクトの生成と同時に、フィールドやプロパティに値を割り当てるような書き方が可能になりました。

class PersonClass
{
    public string Name { get; set; }
    public int Age { get; set; }
}

---

// 以下を1行で
// var person = new PersonClass();
// person.Name = "tanaka";
// person.Age = 20;
var person = new PersonClass { Name = "takana", Age = 20 };

これを用いて、型定義なしにオブジェクトを生成できる 匿名型 が誕生しました。

var person = new { Name = "tanaka", Age = 20 };

// 書き替え不可
// person.Name = "sato";

C# 7.2 : readonly 構造体

構造体に readonly を付与することにより、書き替えのできない構造体を定義できるようになりました。

readonly struct ReadOnlyPerson
{
    // setter は定義できない
    // public string Name { get; set; }
    public string Name { get; }

    // フィールドは readonly 必須
    // private int age;
    private readonly int age;

    public ReadOnlyPerson(string name, int age)
    {
        this.Name = name;
        this.age = age;
    }
}

---

var person = new ReadOnlyPerson("takana", 20);
// NG
// person.Name = "sato";

C# 8.0 : 構造体の readonly メンバ

構造体のメソッドに対して readonly を付与し、そのメソッドが構造体の値を書き替えないことを明示できるようになりました。

struct Person
{
    private string name;

    public readonly string GetName()
    {
        // メソッド内で書き替えようとするとコンパイルエラー
        // this.name = "";

        return name;
    }
}

C# 9.0 : レコード型

クラス/構造体と異なる レコード型 が導入されました。

「レコード」という名前が示すように、ひとまとまりのデータを格納する目的で使用する参照型です。
構造体だと値型のためコピーによるメモリ消費が大きくなるし、等価比較のパフォーマンスが悪い。クラスだと自前で Equals 実装めんどい、といった場合にレコードが選択候補となります。

record PersonRecord
{
    public string Name { get; init; }
    public int Age { get; init; }
}

---

var p1 = new PersonRecord { Name = "Tanaka", Age = 20 };
var p2 = new PersonRecord { Name = "Tanaka", Age = 20 };
var p3 = new PersonRecord { Name = "Sato",   Age = 20 };

Console.WriteLine(p1 == p2);    // True
Console.WriteLine(p1 == p3);    // False

// ToString() がいい感じになる
Console.WriteLine(p1);  // PersonRecord { Name = Tanaka, Age = 20 }

上記のようなシンプルなレコードであれば、 位置指定構文 を使って以下のように簡潔に定義することもできます。
この場合、完全コンストラクタが自動生成されます。

record PersonRecord(string Name, int Age);

---

var p = new PersonRecord("Tanaka", 20);

また、 with 式を使って、一部の値が異なるコピーを生成することができます。

var p1 = new PersonRecord("Tanaka", 20);
var p2 = p1 with { Name = "Sato" };

Console.WriteLine(p2);  // PersonRecord { Name = Sato, Age = 20 } 

関連:変数の文字列化 > レコード型の既定実装

C# 10.0 : with 式の拡張と record struct

レコードのほかに、構造体や匿名型でも with 式が使えるようになりました。

var p1 = new { Name = "Tanaka", Age = 20 };
var p2 = p1 with { Name = "Sato" };

Console.WriteLine(p2.Name); // Sato
Console.WriteLine(p2.Age);  // 20

また、値型のレコードである レコード構造体 が追加されました。

レコード構造体を定義する場合は、 record と書く代わりに record struct と書くだけです。
参照型のレコードは、これまで同様 record と書く代わりに record class と書くこともできるようになりました。

このほか、構造体についての機能追加として、フィールド初期化子や引数なしコンストラクタの定義が可能になりました。

struct PersonStruct
{
    private string name = "";   // OK
    private int age;
    public PersonStruct(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    // OK
    public PersonStruct()
    {
        this.age = 0;
    }
}

コレクションと LINQ

C# 1.0 : ArrayList の時代

C# に当初から配列はあり、 int[] のように要素の型を固定できましたが、ジェネリクスの概念はなく、 ArrayList のような可変長のコレクションにおいて、要素はすべて object 型として扱われました。

foreach 文により、暗黙的にキャストした値をループで取り出すことは可能でしたが、意図しない型の値が混ざっていた場合はキャストエラーになります。

ArrayList list = new ArrayList();
list.Add(123);
list.Add("abc");

// objectで取り出したものをキャスト
int num = (int)list[0];

foreach (int item in list)  // InvalidCastException
{
    Console.WriteLine(item % 2 == 0);
}

C# 2.0 : ジェネリックなコレクションと反復子

ジェネリクスの登場により、 System.Collections 名前空間のコレクションは System.Collections.Generic に置き換わり、 System.Collections.IEnumerable<T> を実装するものになりました。

以降、ArrayListHashtable のような非ジェネリックなコレクションは、互換性のためだけに残される存在となりました。

List<int> list = new List<int>();
list.Add(123);
// コンパイルエラー
// list.Add("abc");

int num = list[0];

List<T> には、条件に一致する要素を見つけるための Find() FindAll() といったメソッドがありました。
条件式となるメソッドは、ラムダ式登場以前は delegate を使って匿名メソッドとして指定していました。

int even = list.Find(delegate (int n) { return n % 2 == 0; });

また、 yield return反復子 を返すことにより、 foreach 文で列挙可能なメソッドを実装することができるようになりました。

private IEnumerable<int> CountToTen()
{
    int n = 1;
    while(true)
    {
        if (n > 10)
        {
            yield break;
        }
        yield return n;
        n++;
    }
}

---

foreach (int n in CountToTen())
{
    Console.WriteLine(n);
    // 1
    // 2
    // ...
    // 10
}

関連:非同期処理・並列処理 > 非同期ストリーム

C# 3.0 : ラムダ式と LINQ

匿名メソッドを簡潔な構文で記述できる ラムダ式 と、コレクション操作が直感的な順序で書ける LINQ のメソッド群が登場し、コーディングスタイルに大きな変革をもたらしました。

int num =
    Enumerable.Range(1, 100)            // 1~100のうち
    .Where(n => n % 3 == 0)             // 3の倍数で
    .FirstOrDefault(n => n * n > 500);  // 2乗が500を超える最初の値
Console.WriteLine(num);     // 24
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

---

List<User> users;

string[] names = users
    .Skip(100).Take(100)    // 101~200番目のユーザを
    .OrderBy(u => u.Id)     // ID順に
    .Select(u => u.Name)    // 名前を取り出して
    .ToArray();             // 配列化

ラムダ式と同時に、SQL に似た クエリ式 も追加されましたが、現在は(当時から?)あまり利用されていません。

string[] names = (
    from u in users
    // skip, take などはクエリ式にない
    orderby u.Id
    select u.Name
    ).ToArray();

その他、 コレクション初期化子 を用いて以下のような初期化が可能になりました。

var list = new List<int> { 1, 2, 3 };

var dic = new Dictionary<int, string>
{
    { 1, "aaa" },
    { 2, "bbb" },
    { 3, "ccc" },
};

関連:非同期処理・並列処理 > Parallel LINQ

C# 6.0 : インデックス初期化子

インデクサを持つオブジェクトについて、以下のように インデックス初期化子 での初期化も可能になりました。

var dic = new Dictionary<int, string>
{
    [1] = "aaa",
    [2] = "bbb",
    [3] = "ccc"
};

C# 8.0 : インデックスと範囲

index from end 演算子 ^範囲演算子 .. を、配列などのインデクサに対して指定できるようになりました。

var array = new int[] { 1, 3, 5, 7, 9 };

// ^1 は array.Length-1 と同じ
int n = array[^1];      // 9

// i..j のとき、index i から j-1 の範囲をコピー
int[] ns = array[1..3]; // [3, 5]

これらの演算子を伴う値は、 Index 型・ Range 型として扱うことができます*5

Index i = ^1;
Range r = 1..3;

C# 10.0頃 : LINQ メソッドの追加

.NET 6 で、以下のような LINQ メソッドが新たに追加されました*6

var num = Enumerable.Range(1, 100)
    .Chunk(3)                          // [1,2,3], [4,5,6]... と3つずつに分割
    .Select(cnk => cnk[0])
    .Take(10..20)                      // Skip(10).Take(10) と同じ
    .FirstOrDefault(n => n > 50, -1);  // 条件一致がないときは -1  

タプル

C# 1.0

C# に複数の値をまとめて扱う簡単な仕組みはなく、たとえばメソッドから複数の値を返したい場合、配列やオブジェクトにつめて返すか、out 引数を使う必要がありました。

C# 4.0 頃 : Tuple

複数の値をひとまとめにして扱うために、 .NET Framework 4.0 で System.Tuple というクラスが追加されました。
これは C# としての新しい言語機能の追加ではなく、自作しようと思えばできる汎用クラスのような位置づけのものでした。

// Tuple<int, string>
var tuple = Tuple.Create(123, "abc");

Console.WriteLine(tuple.Item1);     // 123
Console.WriteLine(tuple.Item2);     // "abc"

// 書き替え不可
// tuple.Item1 = 345;

// classなのでnull許容
tuple = null;

C# 7.0 : タプル構文と分解

.NET Framework 4.7 から、Tuple と似たような System.ValueTuple という型が追加されました。
Tuple型が class だったのに対しこちらは構造体で、値の書き換えが可能です。

// ValueTuple<int, string>
var tuple = ValueTuple.Create(123, "abc");
Console.WriteLine(tuple.Item1);     // 123
Console.WriteLine(tuple.Item2);     // "abc"

// 書き替え可
tuple.Item1 = 345;

// 構造体なので null不可
// tuple = null;

ValueTuple 型とあわせて、 C# 7.0 には ValueTuple を簡単に扱うための構文が追加されました。

var tuple = (123, "abc");
Console.WriteLine(tuple.Item1);     // 123
Console.WriteLine(tuple.Item2);     // "abc"

// 名前もつけられる
(int Age, string Name) person = (20, "tanaka");
// または var person = (Age: 20, Name: "tanaka");
Console.WriteLine(person.Age);
Console.WriteLine(person.Name);

// C# 7.1 から以下でも名前がつく
var age = 20;
var name = "tanaka";

var p = (age, name);
Console.WriteLine(p.age);
Console.WriteLine(p.name);

タプルは個別の変数に 分解 することができます。

var tuple1 = (123, "abc");

// int num, string str に分解される
var (num, str) = tuple1;
Console.WriteLine(num); // 123
Console.WriteLine(str); // "abc"

// 宣言済みの変数へも分解代入可能
var tuple2 = (345, "cde");
(num, str) = tuple2;

あわせて、タプルでなくても Deconstruct という名前のメソッドさえ定義すれば分解ができるようになりました。

public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }
    public bool IsMale { get; init; }

    public void Deconstruct(out string name, out int age, out bool isMale)
    {
        name = this.Name;
        age = this.Age;
        isMale = this.IsMale;
    }
}

---

var person = new Person { Name = "Tanaka", Age = 20, IsMale = true };
var (name, age, isMale) = person;

Console.WriteLine(name);    // Tanaka
Console.WriteLine(age);     // 20
Console.WriteLine(isMale);  // True

使用する必要のない変数は、同じく C# 7.0 で追加された、 破棄 を表す _ で宣言することができます。

var person = new Person { Name = "Tanaka", Age = 20, IsMale = true };
var (_, age, _) = person;

Console.WriteLine(age); // 20
// 参照不可
// Console.WriteLine(_);

C# 7.3 : タプルの等価比較

タプルに対して等価演算子での比較が可能になりました。

// 左から順に、すべての要素が同じ値か(名前は考慮されない)
var tuple1 = (123, "abc");
var tuple2 = (n:123, s:"abc");

Console.WriteLine(tuple1 == tuple2);    // True

C# 10.0 : 分解の制限撤廃

宣言済みの変数と、新しく宣言する変数との混在に対しても分解代入できるようになりました。

var tuple = (123, "abc");

int num = 0;
(num, string str) = tuple;

変数の文字列化

C# 1.0 : string.Format()

すべての object は ToString() メソッドを持っており、変数を文字列化する際に明示的/暗黙的に実行されます。
ToString() には 書式指定子 を指定することで、ゼロ埋めや日付の整形ができます*7

任意の文字列中に変数の値を埋め込みたい場合、もっともシンプルには + 演算子や StringBuilder で連結しながら組み立てていく方法があります。

class Person
{
    string name;
    int age;
    DateTime birthday;

    public override string ToString()
    {
        return "name=" + name
            + ",age=" + age  // 暗黙的に age.ToString() される
            + ",birthday=" + birthday.ToString("yyyy/MM/dd");
    }
}

しかし情報量が増えると可読性が悪くなるので、大抵は string.Format() が使われていました。

public override string ToString()
{
    return string.Format("name={0},age={1},birthday={2:yyyy/MM/dd}",
                        name, age, birthday);
}

C# 6.0 : 文字列補間

string.Format() の問題点は、文字列が長くなるほど、変数を埋め込むプレースホルダの位置とその変数を指定する位置が離れていくことで、プレースホルダの数と変数の数が一致しないバグなども起こりがちでした。

そこで、 $ を使った 文字列補間 の記法が追加されました。

public override string ToString()
{
    return $"name={name},age={age},birthday={birthday:yyyy/MM/dd}";
}

C# 9.0 : レコード型の既定実装

C# でオブジェクトを ToString() した際の既定実装は、MyNamespace.Person のように型名だけが出力されるというもので、デバッグなどでオブジェクトの中身を出力したい場合は、前例のとおり自前で ToString() をオーバーライドする必要がありました。

C# 9.0 から追加されたレコード型では、こういったユースケースが多くなることを見越して、既定の実装で格納値が出力されるようになっています。

record Person(string Name, int Age, DateTime Birthday);

---

var person = new Person("Tanaka", 20, new DateTime(2001, 1, 1));
Console.WriteLine(person);
// -> Person { Name = Tanaka, Age = 20, Birthday = 2001/01/01 0:00:00 }

または、 PrintMembers というメソッドを実装することで、 ToString() 内の一部だけ自前で実装することもできます。

record Person
{
    public string Name { get; init; }
    public int Age { get; init; }
    public DateTime Birthday { get; init; }

    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append($"Name = {Name}, Age = {Age}, Birthday = {Birthday:yyyy/MM/dd}");
        return true;
    }
}

---

var person = new Person { Name = "Tanaka", Age = 20, Birthday = new DateTime(2001, 1, 1) };
Console.WriteLine(person);
// -> Person { Name = Tanaka, Age = 20, Birthday = 2001/01/01 }

ToString() をオーバーライドするのと何が違うんだという話ですが、レコードを継承する際、子のほうで ToString() 自体をオーバーライドしようとすると親の ToString() 実装を使いまわすことができない(型名まで含まれてしまうので)という不便があります。

record SimplePerson(string Name, int Age);

record DetailPerson : SimplePerson
{
    public DateTime Birthday { get; }
    public DetailPerson(string name, int age, DateTime birthday)
        : base(name, age) => this.Birthday = birthday;

    // base.ToString() の中に「SimplePerson { }」が含まれるので使いにくい
    /*
        public override string ToString()
        {
            return "DetailPerson { "
                + $"Name = {Name}, Age = {Age}, Birthday = {Birthday:yyyy/MM/dd}"
                +" }";
        }
    */

    // base.PrintMembers() との差分のみで良い
    protected override bool PrintMembers(StringBuilder builder)
    {
        if (!base.PrintMembers(builder)) return false;

        builder.Append($", Birthday={Birthday:yyyy/MM/dd}");
        return true;
    }
}

上記のように継承されたレコードのほうで勝手に ToString() をオーバーライドできないように、 C# 10.0 ではレコードの ToString()sealed で宣言できるようになりました。

C# 10.0 : const 補間文字列

定数文字列のみを埋め込んだ補間文字列は、const で宣言できるようになりました。

const string Lang = "C#";
const string Version = "10.0";
const string LangInfo = $"{Lang} v{Version}";

null の扱い

C# 1.0

C# では、参照型は null 許容、値型は null 非許容というのが基本です。
そのため、class は null 許容ですが、struct は null 非許容となります。

class C { ... }
struct S { ... }

---

// OK
string str = null;
C c = null;

// NG
// int n = null;
// S s = null;

C# 2.0 : null 許容値型と null 合体演算子

null をとりうる値型を定義するために、 null 許容値型 T? が追加されました。

int? numOrNull = null;

// 値型へ暗黙的な変換は不可
// int num = numOrNull;

// キャストは可(ただしnullなら実行時例外)
int num = (int)numOrNull;

// 検査してからアクセス
if (numOrNull.HasValue)
{
    int num = numOrNull.Value;
}

同時に、 null 合体演算子 ?? が追加されたため、上記 null 許容値型も、もう少しスマートに値型へ変換できます。

int? numOrNull = null;

// nullの場合は0になる
int num = numOrNull ?? 0;

C# 6.0 : null 条件演算子

さらに null 条件演算子 ?. が追加され、null かどうかによる分岐をスマートに書けるようになりました。

string s = null;

// sがnullだと実行時例外になる
// int length = s.Length;
// char first = s[0];
// string trimmed = s.Trim();

// sがnullの場合、`?` 以降を評価せずnullを返す
int? length = s?.Length;
char? first = s?[0];
string trimmed = s?.Trim();

// nullなら0にする場合
int length = s?.Length ?? 0;

関連:イベント > null 条件演算子のイディオム

C# 7.0 : throw 式

?? の右側に throw 式を書けるようになり、nullチェックと例外のスローを1行で書くイディオムが誕生しました*8

class Person
{
    private string name;

    public Person(string name)
    {
        this.name = name ?? throw new ArgumentNullException(nameof(name));
    }
}

C# 8.0 : null 合体割り当てと null 許容参照型

左側が null である場合のみ右側を評価して代入する、 null 合体割り当て演算子 ??= が追加されました。

int? initialScore;
public int GetScore() { ... }

---

// initialScore が null の場合のみ、GetScore() を評価して結果を代入
initialScore ??= GetScore();

そして、型安全を大きく強化する null 許容注釈コンテキスト が追加されました。
これは、すべての参照型を null 非許容とし、null を許容としたい場合には値型と同様 ? を付与することを求めるコンパイラオプションです。

#nullable enable

string  strNG = null;   // 警告
string? strOK = null;   // OK

var lenNG = strOK.Length;        // 警告
var lenOK = strOK?.Length ?? 0;  // OK

.NET 6 からは、このオプションがデフォルトでオンとなります。

型推論

C# 1.0

型推論機能のなかった時代は、ローカル変数の型名を明示的に記述する必要がありました。

int num = 123;

StringBuilder builder = new StringBuilder();

C# 3.0 : 暗黙的な型 var

var を使って、ローカル変数の型を暗黙的に指定することができるようになりました。

// num is int
var num = 123;

// builder is StringBuilder
var builder = new StringBuilder();

// 型を推論できないものはNG
// var n = null;

C# 7.1 : default リテラル

型の既定値*9を表す default 式について、型が推論できる場合に default 演算子から型の指定を省略し、 default リテラル として記述できるようになりました。

// default(int) と同じ
int num = default;

C# 9.0 : ターゲットによる型推論の強化

型が推論できる場合、 new 式の型名を省略できるようになりました。

StringBuilder builder = new();

// 推論できないのでNG
// var builder = new();

また、三項条件演算子についても、ターゲットの型がわかっている場合は型を推論できるようになりました。

bool condition;

// OK
int? n = condition ? 123 : null;

// これは不可
// var n = condition ? 123 : null;

C# 10.0 : ラムダ式の自然型

ラムダ式で定義された匿名メソッドが 自然型 を持つようになりました。
式の実装から引数・戻り値の型が推論できる場合、対応する Action または Func 型が割り当てられます。

ラムダ式の戻り値が推論できない場合、戻り値の型を明示することもできるようになりました。

// parseInt is Func<string, int>
var parseInt = (string s) => int.Parse(s);

// tryParseInt is Func<string, int?>
var tryParseInt = int? (string s) => int.TryParse(s, out int n) ? n : null;

これにより、デリゲート型の基底クラスである Delegate を引数にとるメソッドに対して直接ラムダ式を記述することなども可能になります。

メンバ名の参照

C# 1.0

ログ出力や例外のスロー、 INotifyPropertyChanged の実装などにおいて、実行中のメソッド名などをコードから参照したいことがあります。

もっとも素朴な実装は、単に文字列リテラルとしてメンバ名を指定することです。
当然、コンパイラによるチェック対象にはならず、修正漏れやタイポを防ぐことはできません。

public event PropertyChangedEventHandler PropertyChanged;

private void RaisePropertyChanged(string propertyName)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

private int _age;
public int Age
{
    get { return _age; }
    set
    {
        _age = value;

        // "Age" というプロパティ名で更新を通知
        RaisePropertyChanged("Age");
    }
}

C# 3.0 : 式木の活用

式木を利用して、頑張ればタイプセーフにメンバ名を取得できるようになりました。
INotifyPropertyChanged の実装などではたまに見かけることのあったハックです。

//メンバ名を取得するためのメソッド
private static string NameOf<MemberType>(Expression<Func<MemberType>> expression)
{
    var body = expression.Body as MemberExpression;
    if (body == null) return null;
    return body.Member.Name;
}

public event PropertyChangedEventHandler PropertyChanged;

private void RaisePropertyChanged(string propertyName)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

private int _age;
public int Age
{
    get { return _age; }
    set
    {
        _age = value;

        RaisePropertyChanged(NameOf(() => Age));
    }
}

C# 5.0 : CallerMemberName 属性

CallerMemberName 属性 により、メソッドの引数として呼び出し元のメンバ名を受け取ることができるようになりました。

public event PropertyChangedEventHandler PropertyChanged;

private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

private int _age;
public int Age
{
    get { return _age; }
    set
    {
        _age = value;

        RaisePropertyChanged(); // CallerMemberName として "Age" が渡る
    }
}

C# 6.0 : nameof 演算子

nameof 演算子 により、メソッドの呼び出し元メンバ名に限らず、任意のメンバの名前を文字列として取得できるようになりました。

public event PropertyChangedEventHandler PropertyChanged;

private void RaisePropertyChanged(string propertyName)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

private int _age;
public int Age
{
    get { return _age; }
    set
    {
        _age = value;

        RaisePropertyChanged(nameof(Age));
    }
}

C# 10.0 : CallerArgumentExpression 属性

C# 10 では、メソッドの呼び出し元情報を取得する方法として、 CallerArgumentExpression 属性 が追加されました。
引数として渡した評価式を丸ごと文字列として取得できる*10ため、ログ出力などで重宝しそうですが、変数のみを渡せば変数名を取得する目的でも使用できます。

public event PropertyChangedEventHandler PropertyChanged;

private void RaisePropertyChanged<T>(T val, [CallerArgumentExpression("val")] string propertyName = null)
{
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

private int _age;
public int Age
{
    get { return _age; }
    set
    {
        _age = value;

        RaisePropertyChanged(Age);  // CallerArgumentExpression として "Age" が渡る
    }
}

非同期処理・並列処理

STA(Single Thread Apartment)を前提とする Windows.Forms や WPF といった C# の GUI アプリケーション開発においては、重い処理の実行中に画面をフリーズさせないように、以下のような非同期処理の実装は避けて通れないものでした。

  • 重い処理をバックグラウンドスレッドで実行する
  • 実行が完了したら、メインスレッドに合流する(結果の表示など)

C# 1.0 : Thread, ThreadPool

もっとも古い非同期の実装は、 Thread クラスを直接使うものでした。

private void DoSomething()
{
    ...
}

---

Thread th = new Thread(new ThreadStart(DoSomething));
th.Start();

または、 ThreadPool を使い、一度生成した Thread を使いまわすことによって、Thread 生成/破棄のオーバーヘッドを避けることができます。

private void DoSomething(object state)
{
    ...
}

---

ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomething));

C# 2.0頃 : BackgroundWorker

.NET Framework 2.0 で BackgroundWorker というクラスが追加されました。

これは主に Windows.Forms のアプリで、時間のかかるバックグラウンド処理を、画面に進捗状況を表示しつつ実行することを想定したものです。

BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerAsync();

---

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    // バックグラウンド処理の実行内容
    var worker = (BackgroundWorker)sender;
    ...
    worker.ReportProgress(50);
    ...
    worker.ReportProgress(100);
    e.Result = "Done.";
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // 進捗の更新時
    Console.WriteLine(e.ProgressPercentage);
}

private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // 完了時
    Console.WriteLine(e.Result);
}

C# 4.0頃 : Task と Parallel

.NET Framework 4.0 で、スレッドを抽象化して扱うための Task クラスが登場しました。
これにより、 ThreadThreadPool を直接利用するケースはほとんど無くなりました。

// .NET Framework 4.5 からは Task.Run() でもよい
var task = Task.Factory.StartNew(() =>
{
    ...
}).ContinueWith(t =>
{
   ...
}, TaskScheduler.FromCurrentSynchronizationContext());  // 後続処理をメインスレッドで行う

Task は内部的に ThreadPool を利用しますが、バックグラウンドで動かし続ける処理なのでスレッドプールを占拠してほしくないという場合は、 TaskCreationOptions.LongRunning オプションを付与すれば、タスクスケジューラがよしなにしてくれます。

また、並列処理を簡単に記述する方法として、 Parallel クラスや Parallel LINQ が追加されました。

var values = new[] { "aaa", "bbb", "ccc" };

// Parallelクラスのメソッド
Parallel.ForEach(values, v =>
{
    // 重い加工処理など
    Thread.Sleep(1000);

    Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}, Value: {v}");
    // 出力例
    // Thread: 1, Value: aaa
    // Thread: 2, Value: bbb
    // Thread: 3, Value: ccc
});

// Parallel LINQ
values.AsParallel().ForAll(v =>
{
    Thread.Sleep(1000);

    // 結果は↑と同様
    Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}, Value: {v}");
});

C# 5.0 : async/await

他の言語に先駆けて*11 async / await のモデルが導入されました。

これにより、スレッドを極力意識せずに非同期処理を書くということがだいぶ容易になりました。

// ボタンクリックのイベントハンドラ
private async void button1_Click(object sender, RoutedEventArgs e)
{
    var val = await GetValue(); // 別スレッドで実行されるので画面を固めない
    this.textBlock1.Text = val; // メインスレッドで実行されるのでエラーにならない
}

private async Task<string> GetValue()
{
    await Task.Delay(1000);
    return "abc";
}

async で修飾されたメソッドであっても、非同期的に実行されるかどうかは、メソッド内に await の呼び出しが含まれるかどうかによります。

private int? cache;

private async Task<int> GetValue()
{
    // 取得済みの値があればそれを返す
    if (cache.HasValue)
    {
        return cache.Value;
    }

    int val = await FetchValueFromSomewhere();
    this.cache = val;
    return val;
}

// 外部から非同期でデータ取得するイメージ
private async Task<int> FetchValueFromSomewhere()
{
    await Task.Delay(1000);
    return 123;
}

---

var res1 = await GetValue();    // 非同期的に実行
var res2 = await GetValue();    // 同期的に実行

C# 7.0 : ValueTask と AsyncMethodBuilder 属性

当初、非同期メソッドの戻り値は、以下のいずれかにする必要がありました。

  • void : 呼び出し元に await させる必要がない場合
  • Task : 呼び出し元に await させるが、メソッドから値を返す必要がない場合
  • Task<TResult> : メソッドから値を返す場合

しかし、前項例示のとおり async となっていても実際には非同期化されないケースにおいては、戻り値を Task<TResult> とすると、本来不要な Task インスタンスの生成に余計なコストを要することになります。

これを解消するために、構造体である ValueTask<TResult> 型が追加されました。

ValueTask<TResult> は、内部に Task<TResult>TResult のフィールドを持っておいて、非同期処理が必要になる場合のみ Task を生成するようにしたものです。
非同期処理が必要にならない場面の多いメソッドでは、 Task<TResult> の代わりに ValueTask<TResult> を戻り値とすることでパフォーマンスが改善される可能性があります。

これとあわせて、任意の型を非同期メソッドの戻り値とするための AsyncMethodBuilder 属性が追加されました。

特定の条件を満たす型にこの属性を付与することで、非同期メソッドの戻り値として利用できるようになります*12

C# 7.2 : async Main

エントリポイントとなる Main メソッドを async にすることができるようになりました。

C# 8.0 : 非同期ストリーム

非同期的に反復子を返すメソッドを await foreach で列挙する 非同期ストリーム の構文が追加されました。

// awaitしながら要素を返す
private async IAsyncEnumerable<int> EnumerateValuesAsync()
{
    await Task.Delay(1000);
    yield return 1;

    await Task.Delay(1000);
    yield return 2;

    await Task.Delay(1000);
    yield return 3;
}

---

// 呼び出し側
await foreach (var val in EnumerateValuesAsync())
{
    Console.WriteLine(val);
}

関連:IDisposable と using ステートメント > 非同期破棄

C# 10.0 : メソッドの AsyncMethodBuilder 属性

非同期メソッドの戻り値として使いたい型に対してではなく、個別の非同期メソッドに対しても AsyncMethodBuilder が付与できるようになりました。

IDisposable と using ステートメント

C# 1.0 : using ステートメント

File I/O やストリームなど、リソースの解放を確実に行いたい場合、 try - finally で実装する代わりに、 IDisposable インタフェースを実装し、 using ステートメントによって Dispose メソッドを確実に実行させることができます*13

using (FileStream fs = new FileStream("hoge.txt", FileMode.Open))
using (StreamReader reader = new StreamReader(fs))   // 複数になる場合、入れ子にせず並べて書ける
{
    string content = reader.ReadToEnd();
}

C# 8.0 : using 宣言と非同期破棄

using 宣言 により、ブロックを作らない書き方が可能になりました。

using var fs = new FileStream("hoge.txt", FileMode.Open);
using var reader = new StreamReader(fs);
var content = reader.ReadToEnd();

また、await using により非同期的な破棄を可能にするインターフェースとして IAsyncDisposable が導入されました*14

public class AsyncObj : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        // 破棄処理
        await Task.Run(() =>
        {
            Console.WriteLine("dispose");
        });
    }
}

---

await using var obj = new AsyncObj();

パターンマッチ

C# 1.0 : パターン以前

以下のような is 演算子や switch 文は、当初から使うことができました。

object maybeInt;

// 型が int であることを確認
if (maybeInt is int)
{
    // キャスト
    Console.WriteLine((int)maybeInt > 0);
}
string str;

switch (str)
{
    case "A":
        DoA();
        break;
    case "B":
        DoB();
        break;
    default:
        DoOther();
        break;
}

as 演算子はキャストと似ていますが、変換できない場合は null となるため、 is + キャスト の代わりとして使うこともできます。

object maybeString;

string str = maybeString as string;
if (str != null)
{
    Console.WriteLine(str.Length);
}

C# 7.0 : 宣言パターン

is 演算子とあわせて、型変換された変数を宣言することができるようになりました。

object maybeInt;

if (maybeInt is int n)
{
    Console.WriteLine(n > 0);
}

この宣言パターンは、 null に対してマッチすることがないため、Nullチェックの代わりとして利用することもできます。
また、 null かどうかを確認したい場合のために、 is null という書き方もできるようになりました。

int? maybeInt;

if (maybeInt is int n)
{
    Console.WriteLine(n > 0);
}

if (maybeInt is null)
{
    Console.WriteLine("null");
}

同様に switch 文の case として、宣言パターンを使用することができます。
この場合は when を使うことでさらにマッチの条件を絞り込むことができます。
型名を var として宣言した場合は、すべてにマッチする条件となります。

object obj;

switch (obj)
{
    // intの場合
    case int n:
        Console.WriteLine(n > 0);
        break;

    // stringで、かつ1文字以上の場合
    case string s when s.Length > 0:
        Console.WriteLine(s.Length);
        break;

    // nullの場合
    case null:
        Console.WriteLine("null");
        break;

    // その他
    case var other:
        Console.WriteLine(other);
        break;
}

なお C# 7.0 ではジェネリックな型に対応していませんでしたが、7.1 で対応されました。

C# 8.0 : 再帰パターンと switch 式

再帰パターン と呼ばれる、以下のようなパターンのマッチングが可能になりました。

public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }

    // 分解の定義
    public void Deconstruct(out string name, out int age)
    {
        name = this.Name;
        age = this.Age;
    }
}

---

object obj;

string result;
switch (obj)
{
    // プロパティパターン:Ageプロパティ=20 のPersonにマッチ
    case Person { Age: 20 } p:
        result = $"二十歳の{p.Name}さん";
        break;

    // 位置指定パターン:分解による代入先の2番目(age)=30 のPersonにマッチ
    case Person(_, 30) d:
        result = $"三十路の{d.Name}さん";
        break;

    // その他(破棄)
    case var _:
        result = "不明";
        break;
}

判定対象の型が分かっているときは、case での型指定は省略できます。

Person person;

string result;
switch (person)
{
    case { Age: 20 }:
        result = $"二十歳の{person.Name}さん";
        break;

    case (_, 30):
        result = $"三十路の{person.Name}さん";
        break;

    case var _:
        result = "不明";
        break;
}

タプルも分解可能なので、当然タプルに対しても位置指定パターンでのマッチングが可能です。
タプルの場合、特に以下のような書き方ができます。

string name;
int age;

string result;
switch (name,age)  // いきなりここでタプルを作れる
{
    // name=null にマッチ
    case (null, _):
        result = "名無しさん";
        break;

    // age=30 にマッチ
    case (_, 30):
        result = $"三十路の{name}さん";
        break;

    // その他
    case (_,_):
        result = "不明";
        break;
}

さらに、上記のような switch 文を簡略化するため、 switch 式 の構文が追加されました。

Person person;

string result = person switch
{
    { Age: 20 } => $"二十歳の{person.Name}さん",
    (_, 30) => $"三十路の{person.Name}さん",
    _ => "不明"
};

関連:タプル > タプル構文と分解

C# 9.0 : 関係演算パターンなどの追加

複数の条件を and or でつないだり、数値の大小比較などが可能になりました。

int age;

var ageInfo = age switch
{
    // 0未満
    < 0 => "不正な年齢",
    // 10以上20未満
    >= 10 and < 20 => "10代",
    
    20 => "二十歳",
    
    _ => "その他"
};

object obj;

var objInfo = obj switch
{
    // Person型で、Ageが0未満か200以上(プロパティで)
    Person { Age: < 0 } or Person { Age: >= 200 } => "不正な年齢",
    // Person型で、Nameがnullでなく、Ageが20以上(位置指定で)
    Person(not null, >= 20) p => $"成人の{p.Name}さん",
    // Person型
    Person => "その他の人",

    _ => "不明"
};

C# 10.0 : 入れ子のプロパティパターン

入れ子になったプロパティーを参照できるようになりました。

Person person;

var result = person switch
{
    // { Name: { Length: > 50 } } という書き方なら 9.0でも可能
    { Name.Length: > 50 } => "名前が長すぎます",
    _ => "OK"
};

イベント

C# 1.0

C# のイベントは、 event キーワードを使って public なフィールドとして定義します。

public event EventHandler Updated;

もしくは add remove を明示的に実装することもできますが、あまり使う機会はないと思います。

private event EventHandler _updated;
public event EventHandler Updated
{
    add
    {
        _updated += value;
    }
    remove
    {
        _updated -= value;
    }
}

以下のように購読します。

Updated += new EventHandler(HandleUpdated);   // 購読
Updated -= new EventHandler(HandleUpdated);   // 購読解除

---

// ハンドラ
private void HandleUpdated(object sender, EventArgs e)
{
    Console.WriteLine("updated");
}

以下のように発火します。

// ※厳密にはスレッドセーフでない(後述)
if (Updated != null)
{
    Updated(this, EventArgs.Empty);
}

イベントにパラメータを持たせたい場合、ジェネリクスのない時代は、デリゲートの定義からやる必要がありました。

// イベントのパラメータとして使うクラス
public class UpdatedEventArgs : EventArgs
{
    public bool Result { get; set; }
}

// デリゲートを定義
public delegate void UpdatedEventHandler(object sender, UpdatedEventArgs e);

// イベントを定義
public event UpdatedEventHandler Updated;

---

Updated += new UpdatedEventHandler(HandleUpdated);   // 購読
Updated -= new UpdatedEventHandler(HandleUpdated);   // 購読解除

---

// ハンドラ
private void HandleUpdated(object sender, UpdatedEventArgs e)
{
    Console.WriteLine(e.Result);
}

C# 2.0 : ジェネリックなイベント

ジェネリクスの導入とあわせて、 .NET Framework 2.0 から EventHandler<TEventArgs> が追加され、いちいちデリゲートを定義する必要がなくなりました。

public event EventHandler<UpdatedEventArgs> Updated;

また デリゲート推論 により、 new EventHandler( ... ) という書き方が不要になりました。

Updated += HandleUpdated;   // 購読
Updated -= HandleUpdated;   // 購読解除

C# 3.0 : ラムダ式のハンドラ

ラムダ式 の導入により、イベントハンドラをいきなりラムダ式で書くことも可能になりました。

EventHandler<UpdatedEventArgs> handler = (s, e) =>
{
    Console.WriteLine(e.Result);
};

Updated += handler;     // 購読
Updated -= handler;     // 購読解除

C# 6.0 : null 条件演算子のイディオム

イベントを発火する際、ハンドラが登録されていないと Null参照例外になってしまうので、通常は null でないことを確認してから発火する必要があります。

if (Updated != null)
{
    Updated(this, new UpdatedEventArgs() { Result = true });
}

しかし上記の書き方だと、マルチスレッド環境では、null でないことを確認してから発火するまでの間に null になる可能性があるため、スレッドセーフではありません。

そのため、最初に空のハンドラを登録してしまう、一時変数に代入してからチェックするなどの工夫がされていました*15

C# 6.0 で null 条件演算子が導入されたことにより、以下のような書き方が一般的になりました。

Updated?.Invoke(this, new UpdatedEventArgs() { Result = true });

C# 7.0頃 : EventArgs 制約の廃止

.NET Core のイベントでは、引数が EventArgs を継承したクラスである必要がなくなりました。

public event EventHandler<bool> Updated;

---

Updated += (s, result) =>
{
    Console.WriteLine(result);  // result is bool
};

以上

執筆にあたっては、特に下記ページを参考にさせていただきました。

C# の歴史 - C# ガイド _ Microsoft Docs
C# 10 の新機能 - C# ガイド _ Microsoft Docs
C# によるプログラミング入門 _ ++C++; __ 未確認飛行 C

*1:参考:https://qiita.com/nskydiving/items/3af8bab5a0a63ccb9893 , https://ufcpp.net/study/csharp/cheatsheet/listfxlangversion/ , https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-version-history

*2:get-only との違いについてより詳しくは https://ufcpp.net/study/csharp/oo_property.html#init-only 参照

*3:クラスと構造体の違いや使い分けについては https://ufcpp.net/study/csharp/resource/rm_struct/ が詳しい

*4:その他、部分型の細かい制約等は https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/partial-classes-and-methods 参照

*5:より詳しくは https://ufcpp.net/study/csharp/data/dataranges/ 参照

*6:一覧は https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-6#new-linq-apis

*7:https://docs.microsoft.com/ja-jp/dotnet/standard/base-types/standard-numeric-format-strings 参照

*8:この他の利用ケースについては https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/throw#the-throw-expression 参照

*9:既定値については https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/default-values 参照

*10:詳細は https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/caller-information#argument-expressions

*11:C# が最初というわけではなさそうです。参考になるトピックがありました: Who did async/await first?

*12:自前で実装することは少ないと思います。詳細は https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/attributes/general#asyncmethodbuilder-attribute 参照

*13:Dispose メソッドの実装パターンについては https://docs.microsoft.com/ja-jp/dotnet/standard/garbage-collection/implementing-dispose 参照

*14:実装の詳細は https://docs.microsoft.com/ja-jp/dotnet/standard/garbage-collection/implementing-disposeasync 参照

*15:詳細は以前書いたC# イベントを一時変数に入れてスレッドセーフにnullチェックするあれ参照