C#9.0の進歩が目覚ましいので使いたい
.NET5 がリリースされました。C#も9.0となりました。C#9.0は.NET5以降でしか使えません。
.NET5 を使えるプロジェクトではどんどん新しい文法を活用しましょう。
C#9.0の新機能から、素晴らしいと思った部分をまとめてみました。
使いたい理由1 : record
class
, struct
に加えて record
を用いて型定義することができます。
変更不可能な参照型とのことです。
public record Person // classのかわりにrecord
{
public string Name { get; }
public DateTime BirthDate { get; }
public Person(string name, DateTime birthDate)
{
Name = name;
BirthDate = birthDate;
}
}
これをILSpyなどで覗いてみると以下のようなコードに展開されます。大方の予想通り class ですがリッチです。
public class Person : IEquatable<Person>
{
protected virtual Type EqualityContract
{
[System.Runtime.CompilerServices.NullableContext(1)]
[CompilerGenerated]
get => typeof(Person);
}
public string Name { get; }
public DateTime BirthDate { get; }
public Person(string name, DateTime birthDate)
{
Name = name;
BirthDate = birthDate;
}
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("Person");
stringBuilder.Append(" { ");
if (PrintMembers(stringBuilder))
{
stringBuilder.Append(" ");
}
stringBuilder.Append("}");
return stringBuilder.ToString();
}
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append("Name");
builder.Append(" = ");
builder.Append((object?)Name);
builder.Append(", ");
builder.Append("BirthDate");
builder.Append(" = ");
builder.Append(BirthDate.ToString());
return true;
}
[System.Runtime.CompilerServices.NullableContext(2)]
public static bool operator !=(Person? r1, Person? r2) => !(r1 == r2);
[System.Runtime.CompilerServices.NullableContext(2)]
public static bool operator ==(Person? r1, Person? r2) => (object)r1 == r2 || (r1?.Equals(r2) ?? false);
public override int GetHashCode() => (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name)) * -1521134295 + EqualityComparer<DateTime>.Default.GetHashCode(BirthDate);
public override bool Equals(object? obj) => Equals(obj as Person);
public virtual bool Equals(Person? other) => (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name) && EqualityComparer<DateTime>.Default.Equals(BirthDate, other!.BirthDate);
public virtual Person<Clone>$() => new Person(this); // Clone系の何からしいが不明
protected Person(Person original)
{
Name = original.Name;
BirthDate = original.BirthDate;
}
}
ToString
やGetHashCode
、比較演算子やコピーコンストラクタなどが自動的に実装されます。
比較演算子も「クラスのインスタンス」ではなく「クラスの内容が」等しいかという実装は大変にありがたいものです。
使いたい理由2 : init
先ほどのPersonのコンストラクタを省略します。代わりに init という初期化中だけ使える Setter を用いてみますがこれが大変良い働きをします。
class Program
{
static void Main(string[] args)
{
Person person = new Person() { Name = "まだ名はない", BirthDate = DateTime.Today };
Console.WriteLine(person);
}
}
public record Person
{
public string Name { get; init; }
public DateTime BirthDate { get; init; }
}
重要なのは、コンストラクタを抜けた後のwith式内まで使用可能であることです。
当然ながら record が不変であることも合わせて変更はできません。
person.Name = "ねこ"; // コンパイルエラー
ところで、不変なのにSetterが使えるとはどういうことでしょうか。気になります。
ILを見てみましょう。
NameのSetterは内部的には set_Name となります。第1引数に value を受け取っています。
.method public hidebysig specialname
instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Name (
string 'value'
) cil managed
{
// 省略
}
modreq
というのがポイントのようです。これは呼び出し元が無視してはならない制約という意味です。
外部初期化までは有効、つまり「IsExternalInit」という属性が制約として機能します。
使いたい理由3 : 型省略可能なnew()
new は型が明確である場合に型の記述を省略できるようになりました。プロパティやフィールドの初期化などで同じ型を2回書く必要はもうなくなりました。
ローカル変数でvarとこれのどちらを使うかは迷いどころです。
Person person = new() { Name = "まだ名はない", BirthDate = DateTime.Today };
使いたい理由4 : パターンマッチングの強化
is
のあとに and
or
not
などを記述できるようになりました。
// c is のあとは c に対する条件式
public static bool IsLetterOrSpace(this char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or ' ';
// 従来の場合 c を何度も書く必要があった
public static bool IsLetterOrSpace(this char c) => c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || ' ';
同じものを記述する回数が減ることは基本的に良いことです。大いに活用したいと思います。
使いたい理由5 : staticラムダ式
メソッド内にこのように書くことができます。
Func<double, double> square = static x => x * x;
意味が伝わりにくいかもしれませんが、ラムダ式はClosure(関数閉包)です。ラムダ式がローカル変数やthisを参照できるのは、関数オブジェクトとしてローカル変数やthisを内包しているからです。このキャプチャーと呼ばれる内包処理のために、通常のラムダ式はメソッド内で毎回 delegate を生成します。
しかし、thisもローカル変数も内包する必要のないラムダ式の場合、使用する都度生成する必要もなくなり、static で静的にラムダ式を保持するようになります。つまりその分はパフォーマンスが良いということになります。
しかしそれでは、このように書くのと何が違うのか?となります。
class Foo
{
static Func<double, double> Square = x => x * x;
}
スコープを絞れるメリットがあります。ローカルメソッドのように、そのメソッド内部以外から見えないということは重要です。
使いたい理由6 : 拡張メソッドでもGetEnumeratorがあればforeachできる
GetEnumeratorを拡張メソッドで用意すれば、Rangeですらこのように拡張できてしまいます。これはとても強力です。
class Program
{
static void Main(string[] args)
{
foreach (var index in 1..10)
Console.WriteLine(index);
}
}
public static class RangeExtensions
{
public static IEnumerable<int> GetEnumerable(this Range range)
{
for (int idx = range.Start.Value; idx <= range.End.Value; idx++)
yield return idx;
}
public static IEnumerator<int> GetEnumerator(this Range range)
=> range.GetEnumerable().GetEnumerator();
}
パターンマッチングと組み合わせて何かができそうな気がします。
その他の強化
unsafe
unmanaged
関連の補強も興味深いですが、今回の記事では扱わないことにします。
謝辞
ここで扱った情報はMicrosoftのサイト( https://docs.microsoft.com/ja-jp/dotnet/csharp/whats-new/csharp-9 )の内容から、これはぜひ使いたいと思った機能を抽出し、自分なりに分析を加えたものです。
.NET と C# の発展に携わるすべてのエンジニアの皆様に感謝いたします。
ご案内
この記事は以下の連載記事の第2回目となります。
https://qiita.com/proprogrammer0/items/560ffaf99cdf828c8e52 「.NET 5 を使いたい理由6選」
第3回目記事はTwitterの同名アカウント( https://twitter.com/proprogrammer0 )上から案内予定です。ASP.NET CoreやEF Coreにも触れる予定です。よろしければご覧ください。