C# 8.0は、C#の歴史の中でも極めて重要な転換点となったメジャーアップデートです。
2019年に.NET Core 3.0と共にリリースされたこのバージョンでは、言語仕様の根幹に関わる「null許容参照型」の導入をはじめ、モダンなプログラミングスタイルを加速させる「非同期ストリーム」や「パターンマッチングの強化」など、数多くの革新的な機能が追加されました。
現在、C#はさらに新しいバージョンへと進化を続けていますが、C# 8.0で導入された機能は現代のC#開発において「標準的な作法」として定着しています。
本記事では、C# 8.0の主要な新機能を網羅し、それらがどのようにコードの安全性と生産性を向上させるのかを詳しく解説します。
- null許容参照型 (Nullable Reference Types)
- 非同期ストリーム (Asynchronous Streams)
- パターンマッチングの強化
- インターフェースのデフォルトメソッド (Default Interface Methods)
- インデックスと範囲 (Indices and Ranges)
- using 宣言 (Using Declarations)
- 静的ローカル関数 (Static Local Functions)
- 構造体の読み取り専用メンバー (Readonly Members)
- null合体割り当て演算子 (Null-Coalescing Assignment)
- その他の細かな改善点
- まとめ
null許容参照型 (Nullable Reference Types)
C# 8.0において最も影響力が大きく、かつ学習コストが高い機能が「null許容参照型」です。
これまでのC#では、参照型(クラスなど)の変数は常に null を許容していましたが、これが原因で実行時に NullReferenceException (NRE) が発生するリスクを常に抱えていました。
導入の背景と仕組み
null許容参照型は、参照型においても「nullになる可能性があるかどうか」をコンパイル時にチェックできるようにする仕組みです。
この機能を有効にすると、参照型はデフォルトで「null非許容」として扱われます。
もし null を許容したい場合は、型名の後ろに ? を付与する必要があります。
以下のコードは、null許容参照型を有効にした場合の挙動を示しています。
using System;
#nullable enable // null許容コンテキストを有効化
public class Person
{
public string Name { get; set; } = ""; // null非許容型
public string? MiddleName { get; set; } // null許容型(?を付ける)
}
public class Program
{
public static void Main()
{
Person person = new Person { Name = "Alice" };
// Nameはnull非許容なので、そのままアクセスしても安全
Console.WriteLine(person.Name.Length);
// MiddleNameはnull許容型なので、直接アクセスすると警告が出る場合がある
// 下記のようにnullチェックが必要
if (person.MiddleName != null)
{
Console.WriteLine(person.MiddleName.Length);
}
else
{
Console.WriteLine("MiddleNameはnullです。");
}
}
}
null免除演算子 (null-forgiving operator)
どうしてもコンパイラの警告を抑制したい場合、! 演算子を使用します。
これは「この変数は絶対に null ではない」と開発者がコンパイラに宣言するものです。
public class User
{
public string? Name { get; set; } // Nameはnullになる可能性がある
}
public void PrintUserName(User user)
{
// 通常、Nameがnullだと警告が出るが、
// 「絶対にnullではない」と確信がある場合に ! をつける
string name = user.Name!;
Console.WriteLine(name.Length);
}
しかし、多用すると NREの温床となるため注意が必要 です。
非同期ストリーム (Asynchronous Streams)
C# 5.0で導入された async/await は非同期処理を劇的に簡略化しましたが、複数のデータを非同期に生成・消費するストリーム処理には対応していませんでした。
C# 8.0では、これを解決するために「非同期ストリーム」が導入されました。
IAsyncEnumerable<T> の活用
非同期ストリームは、IAsyncEnumerable<T> インターフェースを利用します。
これにより、yield return を使いながら非同期にデータを返すことが可能になります。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class DataService
{
// 非同期ストリームを生成するメソッド
public async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 3; i++)
{
await Task.Delay(500); // 非同期な待ち時間をシミュレート
yield return i; // データを一つずつ返す
}
}
}
public class Program
{
public static async Task Main()
{
DataService service = new DataService();
Console.WriteLine("データの取得を開始します...");
// await foreach を使用して非同期ストリームを購読
await foreach (var number in service.GetNumbersAsync())
{
Console.WriteLine($"受信データ: {number}");
}
Console.WriteLine("すべてのデータを受信しました。");
}
}
データの取得を開始します...
受信データ: 1
受信データ: 2
受信データ: 3
すべてのデータを受信しました。
この機能により、大量のデータを一度にメモリに載せることなく、生成された順に順次処理することが容易になりました。
APIからのページング取得やログの監視といったシナリオで非常に強力な武器となります。
パターンマッチングの強化
C# 7.0から導入されたパターンマッチングは、C# 8.0で大幅に強化されました。
より簡潔かつ宣言的に条件分岐を記述できるようになっています。
switch 式 (Switch Expressions)
従来の switch 文を式として記述できるようになりました。
これにより、値を返すだけの冗長な case や break を排除できます。
public enum Color { Red, Green, Blue }
public static string GetHexCode(Color color) => color switch
{
Color.Red => "#FF0000",
Color.Green => "#00FF00",
Color.Blue => "#0000FF",
_ => throw new ArgumentException("未定義の色です") // デフォルトケース
};
プロパティパターン
オブジェクトのプロパティを直接参照してマッチングを行うことができます。
public record Address(string State);
public record Customer(Address Address);
public static decimal GetTaxRate(Customer customer) => customer switch
{
{ Address: { State: "WA" } } => 0.06m, // ネストされたプロパティのチェック
{ Address: { State: "NY" } } => 0.04m,
_ => 0.0m
};
タプルパターン
複数の値を組み合わせて一度に評価することが可能です。
public static string GetResult(string first, string second) => (first, second) switch
{
("rock", "paper") => "paper wins",
("rock", "scissors") => "rock wins",
("paper", "rock") => "paper wins",
_ => "draw or invalid"
};
インターフェースのデフォルトメソッド (Default Interface Methods)
これまでのC#では、一度公開したインターフェースにメソッドを追加すると、そのインターフェースを実装しているすべてのクラスを修正しなければならず、破壊的変更となっていました。
C# 8.0では「インターフェースのデフォルト実装」が可能になり、この問題が緩和されました。
仕組みと利点
インターフェース内でメソッドの本体を定義できるようになります。
これにより、既存の実装を壊さずに新しい機能を追加できます。
public interface ILogger
{
void Log(string message);
// デフォルト実装を持つメソッド
void LogError(string message)
{
Log($"[ERROR] {message}");
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
// LogErrorを実装しなくてもコンパイルエラーにならない
}
public class Program
{
public static void Main()
{
ILogger logger = new ConsoleLogger();
logger.Log("通常ログ");
logger.LogError("エラー発生"); // デフォルト実装が呼ばれる
}
}
ただし、デフォルト実装されたメソッドはインターフェース型にキャストした状態でないと呼び出せないという点に注意が必要です。
これは、クラスが複数のインターフェースを実装した際の「ダイヤモンド問題」を回避するための設計上の制約です。
インデックスと範囲 (Indices and Ranges)
配列やコレクションの部分的な取得(スライス)を簡潔に記述するための構文が追加されました。
- ^ 演算子:末尾からのインデックスを指定します(
^1は末尾の要素)。 - .. 演算子:範囲を指定します(開始 .. 終了)。
using System;
public class Program
{
public static void Main()
{
var words = new string[] { "The", "quick", "brown", "fox", "jumps", "over" };
// 末尾の要素を取得
string lastWord = words[^1]; // "over"
// 範囲指定(1番目から3つ前まで)
var subArray = words[1..4]; // "quick", "brown", "fox"
Console.WriteLine($"最後: {lastWord}");
Console.WriteLine($"スライス: {string.Join(" ", subArray)}");
}
}
最後: over
スライス: quick brown fox
これにより、従来の array.Length - 1 といった計算が不要になり、コードの可読性が大幅に向上しました。
特にデータ解析や文字列処理を行う際に重宝する機能です。
using 宣言 (Using Declarations)
リソースの破棄を保証する using ブロックを、より簡潔に書けるようになりました。
従来の波括弧 { } によるスコープ定義を省略し、変数の宣言時に using を付けるだけで、その変数が属するスコープの終わりで自動的に Dispose が呼ばれます。
// 従来の書き方
void WriteFileOld()
{
using (var writer = new System.IO.StreamWriter("test.txt"))
{
writer.WriteLine("Hello");
} // ここでDisposeされる
}
// C# 8.0 の書き方
void WriteFileNew()
{
using var writer = new System.IO.StreamWriter("test.txt");
writer.WriteLine("Hello");
// メソッドの終了時に自動的にDisposeされる
}
この変更により、ネストの深いコードが解消され、ロジックの本質に集中しやすくなりました。
静的ローカル関数 (Static Local Functions)
C# 7.0で導入されたローカル関数ですが、C# 8.0ではこれに static 修飾子を付与できるようになりました。
静的ローカル関数は、外側のスコープの変数(ローカル変数)を直接キャプチャすることができません。
これにより、意図しない変数の参照やメモリ確保を防止でき、パフォーマンスの最適化やバグの抑制に繋がります。
public int Calculate(int x, int y)
{
// static を付けることで、外部の変数にアクセスできないことを保証
return Add(x, y);
static int Add(int left, int right) => left + right;
}
もし静的ローカル関数内で外部の変数にアクセスしようとすると、コンパイルエラーになります。
これは副作用のない純粋な補助関数を作成したい場合に非常に有効です。
構造体の読み取り専用メンバー (Readonly Members)
C# 7.2で導入された readonly struct は構造体全体を読み取り専用にしましたが、C# 8.0では「特定のメンバーのみ」を読み取り専用に指定できるようになりました。
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
// このプロパティは状態を変更しないことを明示
public readonly double Distance => Math.Sqrt(X * X + Y * Y);
public override readonly string ToString() => $"({X}, {Y}) is {Distance} from origin";
}
readonly を付与することで、コンパイラは「防御的コピー」を避ける最適化が可能になり、パフォーマンスの向上が期待できます。
null合体割り当て演算子 (Null-Coalescing Assignment)
変数が null の場合にのみ値を代入する ??= 演算子が追加されました。
List<int>? numbers = null;
// numbersがnullなら新しいインスタンスを代入
numbers ??= new List<int>();
numbers.Add(1);
これは、if (numbers == null) numbers = new List<int>(); という頻出するパターンを極めて簡潔に記述できる構文糖衣です。
その他の細かな改善点
C# 8.0には、他にも開発効率を高めるための細かな改善が含まれています。
補間逐語的文字列の改善
これまでは、$@"..." の順序(補間 $ と 逐語的 @)が固定されていましたが、C# 8.0からは @$"..." という順序も許可されるようになりました。
些細な点ですが、入力ミスによるコンパイルエラーを防ぐことができます。
構造体としての Disposable
ref struct において、インターフェースを実装していなくても Dispose メソッドを持っていれば using パターンを利用できるようになりました。
これは、高パフォーマンスが要求される低レベルなプログラミングにおいて、スタック上に配置される型のリソース管理を容易にします。
アンマネージド構築型
ジェネリック型であっても、型引数がすべてアンマネージド型であれば、その構築型自体もアンマネージド型として扱えるようになりました。
これにより、ポインタ操作や stackalloc の適用範囲が広がりました。
まとめ
C# 8.0は、単なる機能追加に留まらず、「より安全で、より簡潔なコード」を書くための基盤を再定義したバージョンです。
特に「null許容参照型」は、導入当初こそ既存コードへの警告対応に手間がかかるものの、長期的なメンテナンス性と実行時エラーの削減には計り知れない価値があります。
また、「非同期ストリーム」や「パターンマッチング」は、近年のデータ駆動型開発やクラウドネイティブなアプリケーション開発において欠かせない要素となっています。
もし、まだ古いC#の書き方に慣れているのであれば、これらの機能を積極的に取り入れることで、コードの品質は一段上のレベルへと引き上げられるでしょう。
C# 8.0で導入されたこれらの強力なツールをマスターし、モダンな開発スタイルを自身のものにしてください。






