タイムトラベル C#

この記事は「[C# Advent Calendar 2020](https://qiita.com/advent-calendar/2020/csharplang)」7日目の記事になります。

## この記事の対象者

- 過去に書いた・書かれたC#のプロジェクトを書き直す、または.NET Coreに移管する人
- 久々にC#を触る人
- C#を始めたいが、手元にある学習書が古い人

## TL;DR

- 最新の.NET 5でも過去のC#バージョン準拠でコーディングできる
  - ただしオススメできないし(特にC# 1)、よほどひねくれた書き方でもない限り、やる意味もない
- モダン C#はいいぞ

## はじめに

近年のC#はオープンソースになったこともあり、「よくあるパターンを最小のコードで表現する」「パフォーマンスを最適化する」ことに主眼を置いて言語機能が追加/更新されています。

その一方で、新構文は「昔の書き方がエラーにならないよう」に実装されます。
つまり「**昔のC#のノリでプログラムを書いても、誰も指摘してくれない[^1]**」ことに繋がるのです。

この記事では「**最新のC#記法を古いバージョンに持っていき、エラーになった部分を古い記法に切り替えていく**」逆マイグレーションをしていくことで、最新記法の良さをアピールしていきます。

[^1]: メジャーな代替表記であれば、コンパイラやIDEから「新しい構文で書けますよ」と、提案事項として指摘されます。

## 前準備
```xml:CSharpXX.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion><!--ここにバージョンを指定--></LangVersion>
  </PropertyGroup>
</Project>
```

C# 8以降では[null許容参照型](https://docs.microsoft.com/dotnet/csharp/nullable-references)を使いたいので、その設定もします。

```xml:CSharpXX.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <Nullable>enable</Nullable> <!--これを追加-->
  </PropertyGroup>
</Project>
```

C# 1のプロジェクトでは、.NET Coreがクラスを自動生成しないよう、下記のように設定します。
(自動生成されるクラスに、C# 1で使えない構文が含まれているため。)

```xml:CSharp10.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>1</LangVersion>
    <!--下記2つを追加-->
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
    <GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
  </PropertyGroup>
</Project>
```

## ベース (C# 8.0実装)

- .NET Core 3.0
- Visual Studio 2019

.NET Coreを主軸に置いた最初のバージョン。
そのため一部の言語機能は.NET Core 3.0でしか使用できない。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp80/ImmutableValueClass.cs)

<details><summary>ImmutableValueClass</summary><div>

```cs:ImmutableValueClass.cs
using System;

namespace CSharp80
{
    public class ImmutableValueClass : IEquatable<ImmutableValueClass>, IComparable, IComparable<ImmutableValueClass>
    {
        public int Id { get; }
        public string Name { get; }
        public string? Remarks { get; }
        public int? ParentId { get; }

        public ImmutableValueClass(int id, string name, string? remarks = null, int? parentId = default)
            => (Id, Name, Remarks, ParentId) = (id, name ?? throw new ArgumentNullException(nameof(name)), remarks, parentId);

        public bool Equals(ImmutableValueClass? other)
            => !(other is null)
            && (Id, Name, Remarks, ParentId) == (other.Id, other.Name, other.Remarks, other.ParentId);

        public override bool Equals(object? obj)
            => obj is ImmutableValueClass other && Equals(other);

        public override int GetHashCode()
            => HashCode.Combine(Id, Name, Remarks, ParentId);

        public override string ToString()
            => $"{nameof(ImmutableValueClass)}: {{ {nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Remarks)}: {Remarks}, {nameof(ParentId)}: {ParentId} }}";

        public void Deconstract(out int id, out string name, out string? remarks, out int? parentId)
            => (id, name, remarks, parentId) = (Id, Name, Remarks, ParentId);

        public int CompareTo(object? obj)
            => obj is null ? 1
            : obj is ImmutableValueClass other ? CompareTo(other)
            : throw new ArgumentException(nameof(obj) + " is not a " + nameof(ImmutableValueClass), nameof(obj));

        public int CompareTo(ImmutableValueClass? other)
            => other is null ? 1 : Id - other.Id;

        public static bool operator > (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id > r.Id,
                ({}, null) => true,
                (null, {}) => false,
                (null, null) => false
            };

        public static bool operator < (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id < r.Id,
                ({}, null) => false,
                (null, {}) => true,
                (null, null) => false
            };

        public static bool operator >= (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id >= r.Id,
                ({}, null) => true,
                (null, {}) => false,
                (null, null) => true
            };

        public static bool operator <= (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id <= r.Id,
                ({}, null) => false,
                (null, {}) => true,
                (null, null) => true
            };
    }
}
```
</div></details>

書き換え不能な、いわゆる「値クラス」を想定した実装です。

## C# 7.3

- Visual Studio 2017 (15.7)
- Visual Studio 2019 (以下フレームワークのデフォルト設定)
  - .NET Core 2.x以前
  - .NET Standard 2.0以前
  - .NET Framework

C# 7.2時代に追加された`Span<T>`構造体などを、より広範に扱えるようにするための言語機能が追加された。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp73/ImmutableValueClass.cs)

### 変化点(8.0 → 7.3)

#### null許容参照型
[null許容参照型](https://docs.microsoft.com/dotnet/csharp/nullable-references)が使えないため、アノテーション`?`が外れています。
このクラスをnull許容参照型を有効にしたプロジェクト側から使用した場合、「null許容か非許容かわからない」判定を受けます。

内部実装的には、コンパイル時に`System.Runtime.CompilerServices`の`NullableAttribute`が付くようなので、同名のクラスを自作することで再現可能？(未検証)

```cs
// C# 8.0
public string? Remarks { get; }

// C# 7.3
public string Remarks { get; }
```

#### switch式・パターンマッチング(プロパティパターン)

パターン マッチングの[プロパティパターン](https://docs.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-8.0/patterns#property-pattern)(`{}`)と、switch式が使えないため、`null`との[定数パターン](https://docs.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-7.0/pattern-matching#constant-pattern)を利用した式に変更しています。

~~書き換え後のほうが効率のよいメソッドなのでは~~

```cs
// C# 8.0
public static bool operator > (ImmutableValueClass? left, ImmutableValueClass? right)
    => (left, right) switch
    {
        (ImmutableValueClass l, ImmutableValueClass r) => l.Id > r.Id,
        ({}, null) => true,
        (null, {}) => false,
        (null, null) => false
    };

// C# 7.3
public static bool operator > (ImmutableValueClass left, ImmutableValueClass right)
    => !(left is null) && (right is null || left.Id > right.Id);
```

## C# 7.2

- .NET Framework 4.7.1
- .NET Core 2.1
- Visual Studio 2017 (15.5)

安全にメモリ空間の処理を行える`Span<T>`構造体関連の言語機能が追加されたバージョン。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp72/ImmutableValueClass.cs)

### 変化点(7.3 → 7.2)

#### [タプルの等値性](https://docs.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-7.3/tuple-equality)

各プロパティメンバー同士の比較に書き換えています。

```cs
// C# 7.3
public bool Equals(ImmutableValueClass other)
    => !(other is null)
    && (Id, Name, Remarks, ParentId) == (other.Id, other.Name, other.Remarks, other.ParentId);

// C# 7.2
public bool Equals(ImmutableValueClass other)
    => !(other is null)
    && Id == other.Id
    && Name == other.Name
    && Remarks == other.Remarks
    && ParentId == other.ParentId;
```

## C# 7.1

- .NET Framework 4.7
- .NET Core 2.0
- Visual Studio 2017 (15.3)

C# 7.0のバグ(?)修正と言語構文の追加。

コードはC# 7.2と同じになるため割愛

## C# 7.0

- .NET Framework 4.6.2
- Visual Studio 2017

タプルやローカル関数など「C#をより効率よく実行する」ことに重きを置いた新機能が追加されたバージョン。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp70/ImmutableValueClass.cs)

### 変化点(7.1 → 7.0)

#### `default`リテラル

`default`リテラルを利用できないため、`default(T)`に変更しています。

```cs:CSharp71
// C# 7.1
public ImmutableValueClass(int id, string name, string remarks = null, int? parentId = default)

// C# 7.0
public ImmutableValueClass(int id, string name, string remarks = null, int? parentId = default(int?))
```

## C# 6

- .NET Framework 4.6
- .NET Core 1.0
- Visual Studio 2015

このバージョン以降、コンパイラがC#での実装([Roslyn](https://github.com/dotnet/roslyn))に変更されています。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp60/ImmutableValueClass.cs)

### 変化点

#### 式とステートメント

コンストラクターを式形式で書けないため、ステートメントに変更しています。
また、[throw式](https://docs.microsoft.com/dotnet/csharp/language-reference/proposals/csharp-7.0/throw-expression)を書くこともできないため、`if`ステートメントの分岐で例外をスローするよう変更しています。

```cs:CSharp70
// C# 7.0
public ImmutableValueClass(int id, string name, string remarks = null, int? parentId = default(int?))
    => (Id, Name, Remarks, ParentId) = (id, name ?? throw new ArgumentNullException(nameof(name)), remarks, parentId);

// C# 6
public ImmutableValueClass(int id, string name, string remarks = null, int? parentId = default(int?))
{
    Id = id;
    if (ReferenceEquals(name, null))
        throw new ArgumentNullException(nameof(name));
    Name = name;
    Remarks = remarks;
    ParentId = parentId;
}
```

#### [タプルに対するサポート](https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-7#tuples-and-discards)

利用個所を変更しています。
また、`Deconstract`メソッドは引き続き定義しているものの、タプルの分解構文が利用できないため、価値が薄れています。[^2]

[^2]: クラスの利用側がC# 7以上ならば、`Deconstract`による分解が利用可能です。

```cs
// C# 7.0
public void Deconstract(out int id, out string name, out string remarks, out int? parentId)
    => (id, name, remarks, parentId) = (Id, Name, Remarks, ParentId);

// C# 6
public void Deconstract(out int id, out string name, out string remarks, out int? parentId)
{
    id = Id;
    name = Name;
    remarks = Remarks;
    parentId = ParentId;
}

```

#### [パターン マッチング](https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-7#pattern-matching)

定数パターン(`X is null`)も含めて利用できないため、`ReferenceEquals(X, null)`の呼び出し[^3]に変更しています。

[^3]: `X == null`の真偽は`X`の型における演算子`==`の実装に依存するため、`X is null`と同じ結果を返すとは限りません。[UnityのGameObjectはその一例です。](https://qiita.com/satanabe1@github/items/e896303859be5d42c188)

```cs
// C# 7.0
public bool Equals(ImmutableValueClass other)
    => !(other is null)
    && Id == other.Id
    && Name == other.Name
    && Remarks == other.Remarks
    && ParentId == other.ParentId;

public override bool Equals(object obj)
    => obj is ImmutableValueClass other && Equals(other);

// C# 6
public bool Equals(ImmutableValueClass other)
    => !ReferenceEquals(other, null)
    && Id == other.Id
    && Name == other.Name
    && Remarks == other.Remarks
    && ParentId == other.ParentId;

public override bool Equals(object obj)
    => Equals(obj as ImmutableValueClass);
```

#### その他

`GetHashCode()`の実装に使っている[HashCode 構造体](https://docs.microsoft.com/dotnet/api/system.hashcode)は「.NET Core 2.1以降」「.NET Framework 4.6.1以降」にしかありません。
そのため、それ以前のフレームワークでは、自力で実装する必要があります。

## C# 5

- .NET Framework 4.5
- Visual Studio 2012

C#の特徴である、非同期構文(`async`)を初めてサポートしたバージョン。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp40/ImmutableValueClass.cs)

### 変化点(C# 6 → 5)

#### [読み取り専用の自動プロパティ](https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-6#read-only-auto-properties)

読み取り専用プロパティを明示的に実装するか、`private`なセッターを持つ自動実装プロパティに変更しています。
ただし後者はクラス内部で変更が可能なことに留意してください。

```cs
// C# 6
public int Id { get; }

// C# 5
private readonly int _id;
public int Id { get { return _id; } }
// or
public int Id { get; private set; }
```

#### [式形式の関数メンバー](https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-6#expression-bodied-function-members)

すべてのメソッドをステートメント形式に変更しています。

```cs
// C# 6
public override bool Equals(object obj)
            => Equals(obj as ImmutableValueClass);

// C# 5
public override bool Equals(object obj)
{
    return Equals(obj as ImmutableValueClass);
}
```

#### [文字列補間](https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-6#string-interpolation)

`string.Format`の呼び出しに変更しています。
ただ、実行速度を考えると`+`による結合の方が早いかも。

```cs
// C# 6
public override string ToString()
    => $"{nameof(ImmutableValueClass)}: {{ {nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Remarks)}: {Remarks}, {nameof(ParentId)}: {ParentId} }}";

// C# 5
public override string ToString()
{
    return string.Format("ImmutableValueClass: {{ Id: {0}, Name: {1}, Remarks: {2}, ParentId: {3} }}", Id, Name, Remarks, ParentId);
}
```

#### [nameof 式](https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-6#the-nameof-expression)

同値となる文字列に置き換えています。

```cs
// C# 6
if (ReferenceEquals(name, null))
    throw new ArgumentNullException(nameof(name));

// C# 5
if (ReferenceEquals(name, null))
    throw new ArgumentNullException("name");
```

## C# 4

- .NET Framework 4
- Visual Studio 2010

`dynamic`型など、外部連携のための機能が強化された。

コードはC# 5と変化がないため割愛

## C# 3

- .NET Framework 2.0, 3.0, 3.5
- Visual Studio 2008

C#のもう一つの特徴である「LINQ」が初めて登場したバージョン。(ただし利用には.NET Framework 3.5が必要)

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp30/ImmutableValueClass.cs)

### 変化点(C# 4 → 3)

#### [省略可能な引数](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments)

メソッドをオーバーロードして、引数を省略できるようにしています。ただし厳密にはAPIが異なります。[^4]

[^4]: オーバーロードは実際に複数のメソッドが作られますが、「省略可能な引数」はコンパイル時に呼び出し側へ定数を埋め込むため、メソッドの実体は一つです。また、`public const`同様に[バージョニングの問題](https://qiita.com/Nossa/items/b874fa6c115898e2a9c8)を引き起こす可能性にも注意する必要があります。

```cs
// C# 4
public ImmutableValueClass(int id, string name, string remarks = null, int? parentId = default(int?))

// C# 3
public ImmutableValueClass(int id, string name) : this(id, name, null, default(int?)) {}

public ImmutableValueClass(int id, string name, string remarks) : this(id, name, remarks, default(int?)) {}

public ImmutableValueClass(int id, string name, int? parentId) : this(id, name, null, parentId) {}

public ImmutableValueClass(int id, string name, string remarks, int? parentId)
```

## C# 2

- .NET Framework 2.0
- Visual Studio 2005
- [ECMA-334:2006](http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/Ecma-334%204th%20edition%20June%202006.pdf)
- [ISO/IEC 23270:2006](https://www.iso.org/standard/42926.html)
 - `Version`に`ISO-2`を指定すると、`2`にフォールバックされる

ようやく「らしく」なってきたバージョン。とはいえまだ「Javaっぽい言語」から脱し切れていない印象を受けました。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp20/ImmutableValueClass.cs)

### 変化点(C# 3 → 2) 

#### [自動実装プロパティ](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/classes-and-structs/auto-implemented-properties)

プロパティを明示的に実装しています。

```cs
// C# 3
public string Remarks { get; private set; }

// C# 2
private readonly string _remarks;
public string Remarks { get { return _remarks; } }
```

#### [暗黙的な型指定](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/var)(`var`)

ローカル変数の型を明示的に指定しています。

```cs
// C# 3
var other = obj as ImmutableValueClass;

// C# 2
ImmutableValueClass other = obj as ImmutableValueClass;
```

## C# 1

- .NET Framework 1.0, 1.1
- Visual Studio .NET (2002), Visual Studio .NET 2003
- [ECMA-334:2003](http://www.ecma-international.org/publications/files/ECMA-ST-WITHDRAWN/ECMA-334,%202nd%20edition,%20December%202002.pdf)
- [ISO/IEC 23270:2003](https://www.iso.org/standard/36768.html)
 - `Version`に`ISO-1`を指定すると、`1`にフォールバックされる

正確には[C# 1.0](https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-version-history#c-version-10)と[C# 1.2](https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-version-history#c-version-12)があるようですが、1.0を明示的に指定することはできないようです。
公式が「**現在のバージョンと比べると、機能がはぎ取られたように見えます。 気がつくと冗長なコードを記述している場合があるでしょう。**」と書くように、今このバージョンを利用するのは単なる**苦行**です。

- [ソース全文](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp10/ImmutableValueClass.cs)

### 変化点(C# 2 → 1)

#### ジェネリック

型パラメーターに対応しないため、`IEquatable<ImmutableValueClass>`, `IComparable<ImmutableValueClass>`の記述ができず、実装もできません。

```cs
// C# 2
public class ImmutableValueClass : IEquatable<ImmutableValueClass>, IComparable, IComparable<ImmutableValueClass>

// C# 1
public class ImmutableValueClass : IComparable
```

#### [null許容値型](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/nullable-value-types)(`int?`, `Nullable<int>`)

同じAPIを持つ[NullableInt構造体](https://github.com/nogic1008/csharp-advent-2020/blob/main/CSharp10/NullableInt.cs)を定義し、代用しています。
ただし、null許容値型はフレームワーク側でも特別扱いされる[^5]ため、挙動は一致していません。

[^5]: 特にボックス化絡み。`HasValue`が`true`の場合は値そのものに、`false`の場合は`null`に変換される。

#### default 演算子(`default(T)`)

`new NullableInt()`に変更しています。

```cs
// C# 2
public ImmutableValueClass(int id, string name) : this(id, name, null, default(int?))
public ImmutableValueClass(int id, string name, string remarks) : this(id, name, remarks, default(int?))

// C# 1
public ImmutableValueClass(int id, string name) : this(id, name, null, new NullableInt())
public ImmutableValueClass(int id, string name, string remarks) : this(id, name, remarks, new NullableInt())
```

## C# 9.0

C# 9.0で追加された`record`型を用いて書くと、以下のようになります。[^6]
(解説はあちこちでされているかと思いますので、割愛します)

[^6]: .NET 5.0以外で利用する場合は、[IsExternalInit.cs](https://github.com/dotnet/runtime/blob/v5.0.0-rtm.20519.4/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsExternalInit.cs)の移植が必要です

```cs:ImmutableValueClass.cs
using System;

namespace CSharp80
{
    public record ImmutableValueClass(
        int Id,
        string Name,
        string? Remarks = null,
        int? ParentId = default) : IComparable, IComparable<ImmutableValueClass>
    {
        public int CompareTo(object? obj)
            => obj is null ? 1
            : obj is ImmutableValueClass other ? CompareTo(other)
            : throw new ArgumentException(nameof(obj) + " is not a " + nameof(ImmutableValueClass), nameof(obj));

        public int CompareTo(ImmutableValueClass? other)
            => other is null ? 1 : Id - other.Id;

        public static bool operator > (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id > r.Id,
                ({}, null) => true,
                (null, {}) => false,
                (null, null) => false
            };

        public static bool operator < (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id < r.Id,
                ({}, null) => false,
                (null, {}) => true,
                (null, null) => false
            };

        public static bool operator >= (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id >= r.Id,
                ({}, null) => true,
                (null, {}) => false,
                (null, null) => true
            };

        public static bool operator <= (ImmutableValueClass? left, ImmutableValueClass? right)
            => (left, right) switch
            {
                (ImmutableValueClass l, ImmutableValueClass r) => l.Id <= r.Id,
                ({}, null) => false,
                (null, {}) => true,
                (null, null) => true
            };
    }
}
```

## おわりに

最初にも挙げたとおり、C#言語自体は「昔の記法がエラーにならないよう」実装されています。
そのためフレームワークやIDEのバージョンに応じた最新のC#を使うことをオススメします。

また、古いC#プログラムからのマイグレーション難易度は、「プログラムそのもの」よりも「使用しているフレームワーク(+依存するライブラリ)」に依ります。
NuGetなどで代替となるライブラリがあるのならば、この機会に汎用性のある.NET Coreへの移行を検討してはいかがでしょうか？