C#の進化を垣間見てみよう - .NET 20周年を記念して

## はじめに

先月2月23日に.NETが20周年を迎えました。つまりC#も20周年ということになるのでしょうか？ 僕はC#が発表されてすぐに使い始めたので、C#との付き合いももう20年近くということになるんですね。随分と時間は流れたものです。

今回は、.NET 20周年にかこつけて、古いC#のコードを最新のC#10に書き換えてみようと思います。これにより、C#がどう進化したかについて見ていければと思っています。

題材は、僕が2011年に出版した『C#プログラミング入門―「オブジェクト指向」の「プログラミング手法」を基礎から解説』に掲載したC#3.0のコードです。（一部コードを変更しています）

## C# 3.0のコード

まずは、『C#プログラミング入門』の70ページに掲載したList2-21のコードをみてみましょう。
書籍ではプログラムコードの一部を抜粋したものでしたが、ここでは実際に動作する完全なコード(コンソールアプリケーション)として示します。

それほど難しいコードではないので、何をやっているかはソースをみていただければわかると思います。



```cs:Person.cs
using System;

namespace Gushwell.Sample
{
    public class Person
    {
        public string Name { get; set; }

        private double _weight;
        public double Weight
        {
            get { return _weight; }
            set
            {
                if (value > 0)
                    _weight = value;
            }
        }
        private double _height;
        public double Height
        {
            get { return _height; }
            set
            {
                if (value > 50)
                    _height = value;
            }
        }
        public DateTime Birthday { get; set; }

        public Person(string name, DateTime birthday)
        {
            Name = name;
            Birthday = birthday;
        }

        public double GetBmi()
        {
            double mh = Height / 100.0;
            return Weight / (mh * mh);
        }

        public int GetAge()
        {
            DateTime today = DateTime.Today;
            int age = today.Year - Birthday.Year;
            if (today.Month < Birthday.Month)
                age--;
            else if (today.Month == Birthday.Month && today.Day < Birthday.Day)
                age--;
            return age;
        }
    }
}
```

```cs:Program.cs
using System;

namespace Gushwell.Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            Person p = new Person("檜山玄太郎", new DateTime(1979, 5, 25));
            p.Weight = 72;
            p.Height = 181;
            int age = p.GetAge();
            Console.WriteLine(age);
            Console.WriteLine("{0:f2}", p.GetBmi());  

            Console.ReadLine();
        }
    }
}
```

上記プログラムの実行結果を以下に示します。

```
42
21.98
```

では、上記コードを今のC#で書き換えるとどうなるかを順にみていきましょう。

## varによる暗黙的な型宣言

最初は`var`による暗黙的な型宣言をしましょう。`var`はC#3.0で導入された機能ですが、上記コードでは、`var`を使っていません。
C#3.0が出た当初は`var`の利用については賛否両論あって、頑なに`var`の利用を拒否する人もいたようですが、今となっては否定派はほとんどいなくなったと思われます。

ここでは、GetAgeメソッドを書き換えてみます。

```cs
    public int GetAge()
    {
        var today = DateTime.Today;
        var age = today.Year - Birthday.Year;
        if (today.Month < Birthday.Month)
            age--;
        else if (today.Month == Birthday.Month &&　today.Day < Birthday.Day)
            age--;
        return age;
    }
```

## get のみの自動プロパティ

プロパティ`Birthday`は、コンストラクタで値を設定し、その後は変化しないプロパティですから、getのみのプロパティに書き換えます。C# 6で導入された機能です。

```cs
    public DateTime Birthday { get; }
```

## 式形式のメソッド

次は、`GetBmi`を式形式のメソッドに書き換えます。ちょっと無理矢理感がありますが、以下のようなコードになります。これもC# 6で導入された機能です。

```cs
    public double GetBmi() => Weight / ((Height / 100.0) * (Height / 100.0));
```

## 式形式のプロパティ

`GetBmi`メソッドは、`Bmi`プロパティにすることもできますね。

```cs
    public double Bmi => Weight / ((Height / 100.0) * (Height / 100.0));
```


## getアクセサ、setアクセサの式形式

ここでは、`Weight`プロパティのgetアクセサ、setアクセサを式形式の記述に変更します。この機能はC# 7で導入された機能ですね。

```cs
    public double Weight
    {
        get => _weight;
        set => _weight = value > 0 ? value : _weight;
    }
```

setアクセサも、式形式使うようにしました（ちょっと強引かな）。


## 最上位レベルのステートメント

Program.csにC# 9で導入された 最上位レベルのステートメントも導入しましょう。

```cs
using System;
using Gushwell.Sample;

var p = new Person("檜山玄太郎", new DateTime(1979, 5, 25));
p.Weight = 72;
p.Height = 181;
var age = p.GetAge();
Console.WriteLine(age);
Console.WriteLine("{0:f2}", p.Bmi); 

Console.ReadLine();
```

`Program`クラス、`Main`メソッドがなくなりました。

## 文字列補間

Program.csの中の、

```cs
Console.WriteLine("{0:f2}", p.Bmi); 
```

の引数部分を文字列補完を使って書き換えます。

```cs
Console.WriteLine($"{p.Bmi:f2}");
```

これはC# 6で導入された機能です。

## グローバル using

次に、C# 10で導入されたグローバルusingを適用します。Visual Studio 2022でコンソールアプリケーションを作成すると、自動で以下の名前空間がglobal usingされます。

```
System;
System.Collections.Generic;
System.IO;
System.Linq;
System.Net.Http;
System.Threading;
System.Threading.Tasks;
```


これにより、Person.csの以下の行を削除できます。

```
using System;
```

なお、作成するプロジェクトのタイプによって何がglobal usingされるのかは変わって来ます。一度アプリケーションをビルドすると、以下のファイルが作成されますので、このファイルの中身を見れば何がusingされているかを確認できます。

`obj/Debug/net6.0/<ProjectName>.GlobalUsings.g.cs`

以下は、今回のコンソールアプリケーションのGlobalUsings.g.csです。

```cs:MyConsoleApp.GlobalUsings.g.cs
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
```


## ファイル スコープ名前空間

さらに、ファイル スコープ名前空間を導入すれば、Person.csは、以下のように変更できます。これもC#10で導入された機能です。

```cs
namespace Gushwell.Sample;

public class Person
{
    ... 省略 ...
}
```
インデントが抑制されるのは、地味に嬉しい機能です。

## レコード型を導入する

もし、Personクラスの`Name`プロパティもreadonlyで良いのならば、C# 9で導入されたレコード型にすることも可能です。
これで、`Name`, `Birthday`プロパティを明示的に記述する必要がなくなります。 

ただし、== 演算子やEqualsメソッドがオーバーライドされて等値判定が変更されますので、そのクラスの特性に合わせてrecord型にするかどうかは判断する必要があると思います。

```cs
public record Person(string Name, DateTime Birthday)
{
    private double _weight;
    public double Weight
    {
        get => _weight;
        set => _weight = value > 0 ? value : _weight;
    }

    private double _height;
    public double Height
    {
        get => _height;
        set => _height = value > 0 ? value : _height;
    }
    ...

}
```


## おまけ：Console.ReadLineの削除

.NET Coreあるいは.NET 5, .NET 6のコンソールアプリケーションならば、Visual Studio からデバッグ実行しても、コンソールウインドウは閉じることがないので、Mainメソッドの`Console.ReadLine();`も不要ですね。

```cs
using Gushwell.Sample;

var p = new Person("檜山玄太郎", new DateTime(1979, 5, 25));
p.Weight = 72;
p.Height = 181;
var age = p.GetAge();
Console.WriteLine(age);
Console.WriteLine($"{p.Bmi:f2}");
```

## 最終的なコード

最終的なコードを示します。

```cs:Person.cs
namespace Gushwell.Sample;

public record Person(string Name, DateTime Birthday)
{
    private double _weight;
    public double Weight
    {
        get => _weight;
        set => _weight = value > 0 ? value : _weight;
    }

    private double _height;
    public double Height
    {
        get => _height;
        set => _height = value > 0 ? value : _height;
    }

    public double Bmi => Weight / ((Height / 100.0) * (Height / 100.0));

    public int GetAge()
    {
        var today = DateTime.Today;
        var age = today.Year - Birthday.Year;
        if (today.Month < Birthday.Month)
            age--;
        else if (today.Month == Birthday.Month &&　today.Day < Birthday.Day)
            age--;
        return age;
    }
}
```

Person.csは、54行から33行になりました。


```cs:Program.cs
using Gushwell.Sample;

var p = new Person("檜山玄太郎", new DateTime(1979, 5, 25));
p.Weight = 72;
p.Height = 181;
var age = p.GetAge();
Console.WriteLine(age);
Console.WriteLine($"{p.Bmi:f2}"); 
```

Program.csは、19行から8行になりました。


## 最後に

選んだ題材があまり適切でなかったせいもあり、進化したC#の機能のほんのわずかな部分しか紹介できませんでした。
async/awaitによる非同期処置、タプル、パターンマッチング、インデックスと範囲、nullに関する様々な機能などなど、紹介できなかった機能がかなりあります。機会があれば第2弾を書ければと思います。