はじめに
c#の進化は目覚ましいです。
ほぼ毎年言語のバージョンアップがされていて、日夜多様な進歩を続けています。
WindowsOS依存を克服したクロスプラットフォームへの変化、Githubでのオープンソース化、モダンな実装をへの進化とあげるとかなりの変化変遷があげられます。
しかし、僕は元々C#が好きだったわけではありません。
最初の案件ではVB.netから始まり、無学な僕にとって、その言語でなんでもできると勘違いしていたエンジニアでした(というより、言語の違いなど興味・関心が無かったです)。
プライベートで勉強なんて特にしなかったのですが、ふとある時VB.netのことを調べてみたところ、なんと2020年にVBが新機能の開発停止でしていると判明。
しかも、VBを利用しての開発がだいぶレガシーであること知ってしまった当時の僕は、それなりにショックを受けました。
そんな折にして、タイミング良く当時働いていた現場でVBからC#へのリプレイス案件が本格稼働され始め、そこから私も、VB.netと袂を分かち、C#へと道を進むことになりました。
それから幾数年。覚束ないこともたくさんあるなかでそれはもう現場で相当にしごかれながら、少しずつ.netの理解を深め、生成AIもフル活用しながらC#の変遷や実装方法を学び得てきました。
そしてあるとき、気付きました。
「この言語、変化に対応する力すごくね?」と。
そんなわけで今回は、モダンなC#の活用例を.net Fiddleでコンパイル検証してみて、淡々とまとめていきたいと思います。
本記事内でのモダンなC# とは、C#8.0~12.0と定義しています。どのバージョンから追加されたかも、記載しています。
活用例
1.Nullチェックの簡略 (C# 8.0)
using System;
public class Program
{
public static void Main()
{
User user = new User
{
Address = new Address
{
City = "Tokyo"
}
};
// 古い書き方
// ネストした条件分岐が必要
if (user != null && user.Address != null && user.Address.City != null)
{
Console.WriteLine(user.Address.City);
}
// モダンな書き方(C#8.0)
// 演算子で簡略化できるようになりました
Console.WriteLine(user?.Address?.City); // null条件演算子
string city = user?.Address?.City ?? "住所不明"; // null合体演算子
Console.WriteLine(city);
}
}
public class User
{
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
}
特徴
null演算子のおかけで、簡潔で読みやすいコードになりますね。
演算子はちょっと癖があるので最初は、条件・合体演算子が混乱して、ん?ってなりましたが、慣れてくるとやはりモダンチックで手短に感じます。
2.パターンマッチング (C# 8.0)
using System;
public class Program
{
public static void Main()
{
Shape shape = new Circle { Radius = 5 };
Shape shape2 = new Rectangle { Width = 10, Height = 10 };
Shape shape3 = null;
Shape shape4 = new UnknownShape();
DescribeShape(shape);
DescribeShape(shape2);
DescribeShape(shape3);
DescribeShape(shape4);
}
static void DescribeShape(Shape shape)
{
// 古い書き方
// 型チェックとキャスト処理を個別に行っています
if (shape is Circle)
{
var circle = (Circle)shape;
Console.WriteLine($"[古い] 円: 半径 = {circle.Radius}");
}
else if (shape is Rectangle)
{
var rectangle = (Rectangle)shape;
Console.WriteLine($"[古い] 長方形: 幅 = {rectangle.Width}, 高さ = {rectangle.Height}");
}
else
{
Console.WriteLine("[古い] 不明な図形");
}
// モダンな書き方(switch式)
// 条件分岐を一つにまとめて見やすくなりました
var description = shape switch
{
Circle c => $"[モダン] 円: 半径 = {c.Radius}",
Rectangle r when r.Width == r.Height => $"[モダン] 正方形: 一辺 = {r.Width}",
Rectangle r => $"[モダン] 長方形: 幅 = {r.Width}, 高さ = {r.Height}",
null => "[モダン] 図形がありません",
_ => "[モダン] 不明な図形"
};
Console.WriteLine(description);
Console.WriteLine();
}
// エンティティ定義
public abstract class Shape
{
// 割愛
}
public class Circle : Shape
{
public double Radius { get; set; }
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
}
public class UnknownShape : Shape
{
public string Info { get; set; } = "";
}
}
特徴
switch式は従来caseとbreakが必要でしたが、それもなくなり直感的に変数遷移を見ることができます。
もっとも、switchステートメント自体あんまり使ってこなかったので、こんなのあるんだなぁ、という感覚が正直なところです(笑)。
3.レコード型 (C# 9.0)
using System;
public class Program
{
public static void Main()
{
// 古い書き方
var oldPerson1 = new OldPerson("田中", "太郎", 30);
var oldPerson2 = new OldPerson("田中", "太郎", 30);
Console.WriteLine($"[古い] oldPerson1 == oldPerson2: {oldPerson1 == oldPerson2}"); // false(同一参照が違う)
Console.WriteLine($"[古い] oldPerson1.Equals(oldPerson2): {oldPerson1.Equals(oldPerson2)}"); // true(値が同じ)
// モダンな record
// recordは==を中身ベースで比較するように定義されてる
var person1 = new Person("田中", "太郎", 30);
var person2 = person1 with { Age = 31 }; // withで変更も可能
Console.WriteLine($"\n[モダン] person1: {person1}");
Console.WriteLine($"[モダン] person2: {person2}");
Console.WriteLine($"[モダン] person1 == person2: {person1 == person2}"); // false
Console.WriteLine($"[モダン] person1.Equals(person2): {person1.Equals(person2)}"); // false
var person3 = new Person("田中", "太郎", 30);
Console.WriteLine($"[モダン] person1 == person3: {person1 == person3}"); // true
}
}
// 古い書き方
// データオブジェクトを定義
// 値の比較をする場合、追加実装が必要
public class OldPerson
{
public string FirstName { get; }
public string LastName { get; }
public int Age { get; }
public OldPerson(string firstName, string lastName, int age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
public override bool Equals(object obj)
{
if (obj is OldPerson other)
{
return FirstName == other.FirstName &&
LastName == other.LastName &&
Age == other.Age;
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(FirstName, LastName, Age);
}
public override string ToString()
{
return $"{FirstName} {LastName} ({Age})";
}
}
// モダンな書き方
// Equals, GetHashCode, ToStringを自動で行ってくれる
// 参照型なのに値ベースでの比較もできる
public record Person(string FirstName, string LastName, int Age);
特徴
イミュータブルな値を簡潔に書いてくれます。データオブジェクトとかで使用できそうですね。
record型は参照型(class)なのに値比較も自動で行ってくれます。値比較ができるということは、record structで値型の構造体にすることもできます。細かいオブジェクト型で使用する際は、万能なスタック領域になりますね!
4.インデックスレンジ (C# 8.0)
using System;
class IndexAndRangeDemo
{
public static void CompareOldAndModernIndexing()
{
// 配列
var array = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine("=== 従来の書き方 ===");
// 古い書き方: 最後の要素を取得
var lastOld = array[array.Length - 1];
Console.WriteLine($"最後の要素: {lastOld}");
// 古い書き方: 部分配列を取得(index 1〜3)
var sliceOld = new int[3];
Array.Copy(array, 1, sliceOld, 0, 3);
Console.WriteLine("部分配列 (index 1〜3): " + string.Join(", ", sliceOld));
Console.WriteLine("\n=== モダンな書き方 (C# 8.0〜) ===");
// モダンな書き方: 最後の要素を取得
var lastModern = array[^1];
Console.WriteLine($"最後の要素: {lastModern}");
// モダンな書き方: 部分配列を取得(index 1〜3)
var sliceModern = array[1..4];
Console.WriteLine("部分配列 (index 1〜3): " + string.Join(", ", sliceModern));
// モダンな書き方: 末尾の3要素を取得
var lastThreeModern = array[^3..];
Console.WriteLine("末尾の3要素: " + string.Join(", ", lastThreeModern));
}
static void Main()
{
CompareOldAndModernIndexing();
}
}
特徴
配列 or コレクションの末尾からのインデックス指定と範囲指定が、^と..演算子で操作できるようになりました。かなり直感的になりましたが、array操作に慣れていると演算子操作はちょっと苦戦しそうです(ただの感想)。
5.初期化専用プロパティ (C# 9.0)
using System;
class InitOnlyPropertyDemo
{
// === 古い書き方(C# 8以前) ===
public class ConfigurationOld
{
// 読み取り専用プロパティをコンストラクタで設定する
public string ApiKey { get; }
public int Timeout { get; }
public ConfigurationOld(string apiKey, int timeout) // 古い書き方
{
ApiKey = apiKey;
Timeout = timeout;
}
}
// === モダンな書き方(C# 9.0〜) ===
public class ConfigurationModern
{
// init アクセサを使うことで、初期化時だけ代入可能なプロパティにできる
public string ApiKey { get; init; }
public int Timeout { get; init; }
}
public static void CompareOldAndModernInitialization()
{
Console.WriteLine("=== 従来の書き方 ===");
var configOld = new ConfigurationOld("abc123", 30);
Console.WriteLine($"ApiKey: {configOld.ApiKey}, Timeout: {configOld.Timeout}");
Console.WriteLine("\n=== モダンな書き方 (C# 9.0〜) ===");
var configModern = new ConfigurationModern
{
ApiKey = "abc123",
Timeout = 30
};
Console.WriteLine($"ApiKey: {configModern.ApiKey}, Timeout: {configModern.Timeout}");
// 以下はエラーになる(コメントアウトしてます)
// configModern.Timeout = 60; // エラー: init プロパティは初期化後に変更不可
}
static void Main()
{
CompareOldAndModernInitialization();
}
}
特徴
initアクセサは、オブジェクト初期化時だけ値をセットできるプロパティです。readonlyなどを使わずとも、イミュータブルなオブジェクトをコンストラクタ時に定義できます。割愛していますが、先に記載したrecord型との組み合わせると、不変なデータ構造が簡潔にできそうですね。
6.raw文字列リテラル (C# 11.0)
using System;
class RawStringLiteralDemo
{
public static void CompareStringLiterals()
{
// === 従来の書き方 ===
Console.WriteLine("=== 従来の書き方 ===");
// 古い書き方:エスケープシーケンスを多用
string jsonOld = "{\r\n" +
" \"name\": \"田中太郎\",\r\n" +
" \"age\": 30,\r\n" +
" \"email\": \"tanaka@example.com\"\r\n" +
"}"; // 古い書き方
Console.WriteLine("jsonOld:");
Console.WriteLine(jsonOld);
// 古い書き方:@ を使うが、二重引用符のエスケープが必要
string jsonAt = @"{
""name"": ""田中太郎"",
""age"": 30,
""email"": ""tanaka@example.com""
}"; // 古い書き方(@リテラル)
Console.WriteLine("\njsonAt:");
Console.WriteLine(jsonAt);
// === モダンな書き方(C# 11.0〜) ===
Console.WriteLine("\n=== モダンな書き方 (C# 11.0〜) ===");
// Raw文字列リテラル:エスケープ不要で可読性が高い
var jsonModern = """
{
"name": "田中太郎",
"age": 30,
"email": "tanaka@example.com"
}
"""; // 新しい書き方
Console.WriteLine("jsonModern:");
Console.WriteLine(jsonModern);
// 補間付き raw 文字列リテラル
var name = "田中太郎";
var age = 30;
var jsonWithInterpolation = $$"""
{
"name": "{{name}}",
"age": {{age}},
"email": "tanaka@example.com"
}
"""; // モダンな書き方
Console.WriteLine("\njsonWithInterpolation:");
Console.WriteLine(jsonWithInterpolation);
}
static void Main()
{
CompareStringLiterals();
}
}
特徴
複数行の文字列を代入するときに、エスケープ文字を使用しなくてもraw文字列リテラル(""")を使えば、JSON形式でも楽々記述できるようになりました。バックエンドのweb API実装のとき、MOCKデータを作成するときなどで、かなり使えそうですね(かつての自分に伝えたい)。
終わりに
記事がずいぶん長くなってしまいました。旧バージョンのC#と進化したC#の大きな違いは、総じて以下のポイントにまとめられます。
・コードを簡潔による可読性の向上
・コンパイラ時にエラー検出が可能になった安全性の向上
・可読性向上によるメンテナンスのしやすさ
本稿で取り上げたモダンC#は、まだ一部分ですので、拙著ももう少し勉強してまた活用事例の記事を作ってみたいですね!
というわけで、モダンC#活用事例Part2でお会いしましょう。