はじめに
先月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のコードをみてみましょう。
書籍ではプログラムコードの一部を抜粋したものでしたが、ここでは実際に動作する完全なコード(コンソールアプリケーション)として示します。
それほど難しいコードではないので、何をやっているかはソースをみていただければわかると思います。
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;
}
}
}
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メソッドを書き換えてみます。
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で導入された機能です。
public DateTime Birthday { get; }
式形式のメソッド
次は、GetBmi
を式形式のメソッドに書き換えます。ちょっと無理矢理感がありますが、以下のようなコードになります。これもC# 6で導入された機能です。
public double GetBmi() => Weight / ((Height / 100.0) * (Height / 100.0));
式形式のプロパティ
GetBmi
メソッドは、Bmi
プロパティにすることもできますね。
public double Bmi => Weight / ((Height / 100.0) * (Height / 100.0));
getアクセサ、setアクセサの式形式
ここでは、Weight
プロパティのgetアクセサ、setアクセサを式形式の記述に変更します。この機能はC# 7で導入された機能ですね。
public double Weight
{
get => _weight;
set => _weight = value > 0 ? value : _weight;
}
setアクセサも、式形式使うようにしました(ちょっと強引かな)。
最上位レベルのステートメント
Program.csにC# 9で導入された 最上位レベルのステートメントも導入しましょう。
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の中の、
Console.WriteLine("{0:f2}", p.Bmi);
の引数部分を文字列補完を使って書き換えます。
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です。
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で導入された機能です。
namespace Gushwell.Sample;
public class Person
{
... 省略 ...
}
インデントが抑制されるのは、地味に嬉しい機能です。
レコード型を導入する
もし、PersonクラスのName
プロパティもreadonlyで良いのならば、C# 9で導入されたレコード型にすることも可能です。
これで、Name
, Birthday
プロパティを明示的に記述する必要がなくなります。
ただし、== 演算子やEqualsメソッドがオーバーライドされて等値判定が変更されますので、そのクラスの特性に合わせてrecord型にするかどうかは判断する必要があると思います。
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();
も不要ですね。
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}");
最終的なコード
最終的なコードを示します。
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行になりました。
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弾を書ければと思います。