Docker 上でお手軽にC#9（.NET 5）を試す

## .NET 5 をお手軽に試したい

先日めでたく .NET 5がリリースされました。[^dotnet5].NET 5 では C# 9 がサポートされています。
早速試したいところですが手元のPCにインストールせず、少しだけ触りたいので Docker を使って使い捨ての .NET 5 開発環境を用意したいと思います。

Docker のインストール不要なブラウザで使えるクラウド上の使い捨て Docker 実行環境の Play with Docker[^pwd] 上で試します。

## 環境

実行した際の Play with Docker の Linux 環境は`Alpine Linux 3.12`、Docker のバージョンは`19.03.11`です。

- `$ more /etc/issue`

```sh 
Welcome to Alpine Linux 3.12
Kernel \r on an \m (\l)
```

- `$ docker --version `

```sh
Docker version 19.03.11, build 42e35e61f3
```

## .NET 5 の開発環境を用意する

.NET 5 SDK の Docker image を実行します。
`-it`オプションで標準入出力をシェルにつなげてそのまま .NET 5 SDK のコンテナに乗り込みます。
※`--rm`オプションでコンテナ終了時にコンテナが自動的に削除されます[^--rm]。
手元の PC で試す場合は不要なコンテナが残りません。

- `$ docker run -it --rm mcr.microsoft.com/dotnet/sdk:5.0`

```sh
Unable to find image 'mcr.microsoft.com/dotnet/sdk:5.0' locally
5.0: Pulling from dotnet/sdk
bb79b6b2107f: Pull complete 
97805e17b1a2: Pull complete 
48d36279ea43: Pull complete 
5d23a35fbf12: Pull complete 
982bc1066a1e: Pull complete 
6cc6e848c1f3: Pull complete 
df97eda6f03d: Pull complete 
7520ee234b82: Pull complete 
Digest: sha256:ac49854ff6dcc1a2916ffc0981503f571698458187f925da0c2f2b6a0bec8dee
Status: Downloaded newer image for mcr.microsoft.com/dotnet/sdk:5.0
root@e68a47087ad8:/# 
```

.NET 5 がインストールされていることが確認できます。

- `root@e68a47087ad8:/# dotnet --version`

```sh
5.0.100
```

Linux のディストリビューションは Debian なようです。

- `root@e68a47087ad8:/# more /etc/issue`

```sh
Debian GNU/Linux 10 \n \l
```

## C# 9（.NET 5）の新機能を試す

コンソールアプリを作成して C# 9 の新機能を試していきたいと思います。

- `root@e68a47087ad8:/# dotnet new console -o MyConsoleApp && cd MyConsoleApp`

```sh
Getting ready...
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on MyConsoleApp/MyConsoleApp.csproj...
  Determining projects to restore...
  Restored /MyConsoleApp/MyConsoleApp.csproj (in 89 ms).
Restore succeeded.
```

プロジェクトが作成されました。

- `root@e68a47087ad8:/MyConsoleApp# ls -R`

```sh
.:
MyConsoleApp.csproj  Program.cs  obj

./obj:
MyConsoleApp.csproj.nuget.dgspec.json  MyConsoleApp.csproj.nuget.g.targets  project.nuget.cache
MyConsoleApp.csproj.nuget.g.props      project.assets.json
```

ファイル編集したいところですが vim が入っていませんのでインストールしたいと思います。
まず、パッケージをアップデートします。

- `root@e68a47087ad8:/MyConsoleApp# apt update && apt upgrade`

```sh
Get:1 http://security.debian.org/debian-security buster/updates InRelease [65.4 kB]
Get:2 http://deb.debian.org/debian buster InRelease [121 kB]       
Get:3 http://deb.debian.org/debian buster-updates InRelease [51.9 kB]
Get:4 http://security.debian.org/debian-security buster/updates/main amd64 Packages [248 kB]
Get:5 http://deb.debian.org/debian buster/main amd64 Packages [7906 kB]
Get:6 http://deb.debian.org/debian buster-updates/main amd64 Packages [7856 B]
Fetched 8401 kB in 1s (5711 kB/s)                         
Reading package lists... Done
Building dependency tree       
Reading state information... Done
1 package can be upgraded. Run 'apt list --upgradable' to see it.
Reading package lists... Done
Building dependency tree       
Reading state information... Done
Calculating upgrade... Done
The following packages will be upgraded:
  tzdata
1 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Need to get 264 kB of archives.
After this operation, 3072 B of additional disk space will be used.
Do you want to continue? [Y/n] Y
Get:1 http://deb.debian.org/debian buster-updates/main amd64 tzdata all 2020d-0+deb10u1 [264 kB]
Fetched 264 kB in 0s (24.0 MB/s)
debconf: delaying package configuration, since apt-utils is not installed
(Reading database ... 9877 files and directories currently installed.)
Preparing to unpack .../tzdata_2020d-0+deb10u1_all.deb ...
Unpacking tzdata (2020d-0+deb10u1) over (2020a-0+deb10u1) ...
Setting up tzdata (2020d-0+deb10u1) ...
debconf: unable to initialize frontend: Dialog
debconf: (No usable dialog-like program is installed, so the dialog based frontend cannot be used. at /usr/share/perl5/Debconf/FrontEnd/Dialog.pm line 76.)
debconf: falling back to frontend: Readline

Current default time zone: 'Etc/UTC'
Local time is now:      Sat Nov 14 16:16:39 UTC 2020.
Universal Time is now:  Sat Nov 14 16:16:39 UTC 2020.
Run 'dpkg-reconfigure tzdata' if you wish to change it.
```

vim をインストールします。

- `root@e68a47087ad8:/MyConsoleApp# apt install vim`

```sh
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following additional packages will be installed:
  libgpm2 vim-common vim-runtime xxd
Suggested packages:
  gpm ctags vim-doc vim-scripts
The following NEW packages will be installed:
  libgpm2 vim vim-common vim-runtime xxd
0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded.
Need to get 7425 kB of archives.
After this operation, 33.8 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
Get:1 http://deb.debian.org/debian buster/main amd64 xxd amd64 2:8.1.0875-5 [140 kB]
Get:2 http://deb.debian.org/debian buster/main amd64 vim-common all 2:8.1.0875-5 [195 kB]
Get:3 http://deb.debian.org/debian buster/main amd64 libgpm2 amd64 1.20.7-5 [35.1 kB]
Get:4 http://deb.debian.org/debian buster/main amd64 vim-runtime all 2:8.1.0875-5 [5775 kB]
Get:5 http://deb.debian.org/debian buster/main amd64 vim amd64 2:8.1.0875-5 [1280 kB]
Fetched 7425 kB in 0s (59.9 MB/s)
debconf: delaying package configuration, since apt-utils is not installed
Selecting previously unselected package xxd.
(Reading database ... 9877 files and directories currently installed.)
Preparing to unpack .../xxd_2%3a8.1.0875-5_amd64.deb ...
Unpacking xxd (2:8.1.0875-5) ...
Selecting previously unselected package vim-common.
Preparing to unpack .../vim-common_2%3a8.1.0875-5_all.deb ...
Unpacking vim-common (2:8.1.0875-5) ...
Selecting previously unselected package libgpm2:amd64.
Preparing to unpack .../libgpm2_1.20.7-5_amd64.deb ...
Unpacking libgpm2:amd64 (1.20.7-5) ...
Selecting previously unselected package vim-runtime.
Preparing to unpack .../vim-runtime_2%3a8.1.0875-5_all.deb ...
Adding 'diversion of /usr/share/vim/vim81/doc/help.txt to /usr/share/vim/vim81/doc/help.txt.vim-tiny by vim-runtime'
Adding 'diversion of /usr/share/vim/vim81/doc/tags to /usr/share/vim/vim81/doc/tags.vim-tiny by vim-runtime'
Unpacking vim-runtime (2:8.1.0875-5) ...
Selecting previously unselected package vim.
Preparing to unpack .../vim_2%3a8.1.0875-5_amd64.deb ...
Unpacking vim (2:8.1.0875-5) ...
Setting up libgpm2:amd64 (1.20.7-5) ...
Setting up xxd (2:8.1.0875-5) ...
Setting up vim-common (2:8.1.0875-5) ...
Setting up vim-runtime (2:8.1.0875-5) ...
Setting up vim (2:8.1.0875-5) ...
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vim (vim) in auto mode
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vimdiff (vimdiff) in auto mode
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/rvim (rvim) in auto mode
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/rview (rview) in auto mode
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vi (vi) in auto mode
update-alternatives: warning: skip creation of /usr/share/man/da/man1/vi.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/de/man1/vi.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/fr/man1/vi.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/it/man1/vi.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ja/man1/vi.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/pl/man1/vi.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ru/man1/vi.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/man1/vi.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group vi) doesn't exist
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/view (view) in auto mode
update-alternatives: warning: skip creation of /usr/share/man/da/man1/view.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/de/man1/view.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/fr/man1/view.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/it/man1/view.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ja/man1/view.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/pl/man1/view.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ru/man1/view.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/man1/view.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group view) doesn't exist
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/ex (ex) in auto mode
update-alternatives: warning: skip creation of /usr/share/man/da/man1/ex.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/de/man1/ex.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/fr/man1/ex.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/it/man1/ex.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ja/man1/ex.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/pl/man1/ex.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ru/man1/ex.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/man1/ex.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group ex) doesn't exist
update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/editor (editor) in auto mode
update-alternatives: warning: skip creation of /usr/share/man/da/man1/editor.1.gz because associated file /usr/share/man/da/man1/vim.1.gz (of link group editor) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/de/man1/editor.1.gz because associated file /usr/share/man/de/man1/vim.1.gz (of link group editor) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/fr/man1/editor.1.gz because associated file /usr/share/man/fr/man1/vim.1.gz (of link group editor) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/it/man1/editor.1.gz because associated file /usr/share/man/it/man1/vim.1.gz (of link group editor) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ja/man1/editor.1.gz because associated file /usr/share/man/ja/man1/vim.1.gz (of link group editor) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/pl/man1/editor.1.gz because associated file /usr/share/man/pl/man1/vim.1.gz (of link group editor) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/ru/man1/editor.1.gz because associated file /usr/share/man/ru/man1/vim.1.gz (of link group editor) doesn't exist
update-alternatives: warning: skip creation of /usr/share/man/man1/editor.1.gz because associated file /usr/share/man/man1/vim.1.gz (of link group editor) doesn't exist
Processing triggers for libc-bin (2.28-10) ...
```

準備ができましたので C# 9 のコードを試してみます。
詳細は参考にさせていただいた下記の記事をご覧ください。

**[C# 小ネタ：.NET 5.0 で使える C# 9.0 で気に入ってる機能紹介](https://qiita.com/okazuki/items/8043ea6f0095b3019cdf)**

### トップレベルステートメント[^toplevelstatement]

エントリポイントならクラス無しで直接コードを書けます。簡単なバッチなどで便利そうです。

- `root@e68a47087ad8:/MyConsoleApp# vim Program.cs`

```csharp
System.Console.WriteLine("Hello from top.");
```

ビルドします。（以降のサンプルでは省略します。）

- `root@e68a47087ad8:/MyConsoleApp# dotnet build`

```sh
Microsoft (R) Build Engine version 16.8.0+126527ff1 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  MyConsoleApp -> /MyConsoleApp/bin/Debug/net5.0/MyConsoleApp.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:02.44
```

- `root@e68a47087ad8:/MyConsoleApp# dotnet run`

```sh
Hello from top.
```

### レコード型[^record]

イミュータブルなオブジェクトを手軽に作成することができます。
またプロパティの値が一致していれば違う参照のレコード型でも`==`が`true`になります。
またこの記事では試していませんが`with`式を使って一部のプロパティだけ書き換えた新しいレコードを作成することができます。

値オブジェクトづくりがはかどりそうです。

- `root@e68a47087ad8:/MyConsoleApp# vim Program.cs`

```csharp
using static System.Console;

record Money(decimal amount);

class Program  {
  static void Main() {
    var money1 = new Money(10);
    WriteLine($"money1: {Money}");
 
    var money2 = new Money(10);
    WriteLine($"money2: {money2}");

    WriteLine($"money1 == money2: {money1 == money2}");
  }
}
```

- `root@e68a47087ad8:/MyConsoleApp# dotnet run`

```sh
money1: Money { amount = 10 }
money2: Money { amount = 10 }
money1 == money2: True
```

### Target typed new[^targettypednew]

型推論して明らかなときは`new`の型名を省略できます。
今まで右辺から推論する`var`はありましたがメンバー変数で使えませんでした。
また、引数や戻り値で型が明らかな場合でも型名を省略できるようです。

```csharp
using System.Collections.Generic;
using System.Linq;

class Program {
  static readonly Dictionary<int, List<string>> _cache = new();

  static void Main() {
    _cache.Add(1, new() { "aaa" });

    System.Console.WriteLine(_cache[1].First());
  }
}
```

- `root@e68a47087ad8:/MyConsoleApp# dotnet run`

```sh
aaa
```

### 条件式の新機能[^orandnot]

詳しくは参考にさせていただいた下記記事を参照ください

**[C# 9.0 で条件式が革命を起こす](https://qiita.com/Zuishin/items/aac9f0dea33f96c265ac)**

条件式に`or`,`and`,`not`が追加されています。
特徴として評価が１回なので副作用のある式でも使いやすいようです。

- `root@e68a47087ad8:/MyConsoleApp# vim Program.cs`

```csharp
using static System.Console;

int Add2AndPrint(int i)
{
    var res = i + 2;
    WriteLine(res);
    return res;
}

WriteLine(Add2AndPrint(2) is >= 3 and <= 5);

object o = 10;
WriteLine(o is int i && ((i % 3, i % 5) is ((0, int _) or (int _, 0)) and not (0, 0)));
```

- `root@e68a47087ad8:/MyConsoleApp# dotnet run`

```sh
4
True
True
```

## おわり

後から気づいたのですが、ソースコードを変更するたびに`dotnet build`しなくても`dotnet run`だけで勝手にビルドされました。便利です。
読んでいただきありがとうございました。

[^dotnet5]: https://www.publickey1.jp/blog/20/net_51windowsmaclinuxweb.html
[^pwd]: https://qiita.com/tag1216/items/ec7228440f6eaf849cd6
[^--rm]: https://qiita.com/hoshino/items/9545d255cc0103b3d296
[^toplevelstatement]: https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#%E3%83%88%E3%83%83%E3%83%97%E3%83%AC%E3%83%99%E3%83%AB%E3%82%B9%E3%83%86%E3%83%BC%E3%83%88%E3%83%A1%E3%83%B3%E3%83%88
[^record]: https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E5%9E%8B
[^targettypednew]: https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#target-typed-new-%E5%BC%8F
[^orandnot]: https://qiita.com/okazuki/items/8043ea6f0095b3019cdf#%E5%A4%A7%E3%81%AA%E3%82%8A%E5%B0%8F%E3%81%AA%E3%82%8A%E3%81%AE%E6%AF%94%E8%BC%83%E3%82%82%E7%B0%A1%E5%8D%98%E3%81%AB