17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C#Advent Calendar 2020

Day 7

タイムトラベル C#

Last updated at Posted at 2020-12-06

この記事は「C# Advent Calendar 2020」7日目の記事になります。

この記事の対象者

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

TL;DR

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

はじめに

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

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

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

前準備

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

C# 8以降ではnull許容参照型を使いたいので、その設定もします。

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で使えない構文が含まれているため。)

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でしか使用できない。

ImmutableValueClass
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
            };
    }
}

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

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>構造体などを、より広範に扱えるようにするための言語機能が追加された。

変化点(8.0 → 7.3)

null許容参照型

null許容参照型が使えないため、アノテーション?が外れています。
このクラスをnull許容参照型を有効にしたプロジェクト側から使用した場合、「null許容か非許容かわからない」判定を受けます。

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

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

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

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

パターン マッチングのプロパティパターン({})と、switch式が使えないため、nullとの定数パターンを利用した式に変更しています。

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

// 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>構造体関連の言語機能が追加されたバージョン。

変化点(7.3 → 7.2)

タプルの等値性

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

// 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#をより効率よく実行する」ことに重きを置いた新機能が追加されたバージョン。

変化点(7.1 → 7.0)

defaultリテラル

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

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)に変更されています。

変化点

式とステートメント

コンストラクターを式形式で書けないため、ステートメントに変更しています。
また、throw式を書くこともできないため、ifステートメントの分岐で例外をスローするよう変更しています。

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;
}

タプルに対するサポート

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

// 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;
}

パターン マッチング

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

// 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 構造体は「.NET Core 2.1以降」「.NET Framework 4.6.1以降」にしかありません。
そのため、それ以前のフレームワークでは、自力で実装する必要があります。

C# 5

  • .NET Framework 4.5
  • Visual Studio 2012

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

変化点(C# 6 → 5)

読み取り専用の自動プロパティ

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

// 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; }

式形式の関数メンバー

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

// 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);
}

文字列補間

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

// 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 式

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

// 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が必要)

変化点(C# 4 → 3)

省略可能な引数

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

// 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

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

変化点(C# 3 → 2)

自動実装プロパティ

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

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

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

暗黙的な型指定(var)

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

// 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
  • ISO/IEC 23270:2003
  • VersionISO-1を指定すると、1にフォールバックされる

正確にはC# 1.0C# 1.2があるようですが、1.0を明示的に指定することはできないようです。
公式が「現在のバージョンと比べると、機能がはぎ取られたように見えます。 気がつくと冗長なコードを記述している場合があるでしょう。」と書くように、今このバージョンを利用するのは単なる苦行です。

変化点(C# 2 → 1)

ジェネリック

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

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

// C# 1
public class ImmutableValueClass : IComparable

null許容値型(int?, Nullable<int>)

同じAPIを持つNullableInt構造体を定義し、代用しています。
ただし、null許容値型はフレームワーク側でも特別扱いされる5ため、挙動は一致していません。

default 演算子(default(T))

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

// 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
(解説はあちこちでされているかと思いますので、割愛します)

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への移行を検討してはいかがでしょうか?

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

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

  3. X == nullの真偽はXの型における演算子==の実装に依存するため、X is nullと同じ結果を返すとは限りません。UnityのGameObjectはその一例です。

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

  5. 特にボックス化絡み。HasValuetrueの場合は値そのものに、falseの場合はnullに変換される。

  6. .NET 5.0以外で利用する場合は、IsExternalInit.csの移植が必要です

17
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?