C# 6.0は、プログラミング言語としての柔軟性と記述性を大きく向上させた重要なバージョンです。

それまでのC#が言語仕様の根本的な拡張に注力していたのに対し、C# 6.0では「開発者の生産性向上」と「コードの簡潔化」に主眼が置かれました。

このバージョンで導入された新機能は、現在においてもモダンなC#開発の基盤となっており、ボイラープレート(定型的な記述)を削減し、意図が明確なコードを書くために欠かせないものばかりです。

本記事では、C# 6.0で導入された主要な新機能を網羅し、具体的なサンプルコードを交えながら、それらがどのように実務に貢献するのかを詳しく解説します。

C# 6.0 の背景とコンパイラ「Roslyn」

C# 6.0を語る上で欠かせないのが、コンパイラ基盤である「Roslyn (ロスリン)」の全面的な採用です。

Roslynは、C#とVisual Basicのコンパイラをマネージコード(C#自身)で書き直したオープンソースのプロジェクトです。

従来のコンパイラはブラックボックス化されていましたが、Roslynによってコンパイルの各プロセスがAPIとして公開されるようになりました。

これにより、コードの解析やリファクタリング、構文チェックの自動化が容易になり、言語自体の新機能の実装スピードも飛躍的に向上しました。

C# 6.0で導入された多くの機能は、この新しいコンパイラ基盤があったからこそ実現できた、いわば「シンタックスシュガー(構文糖衣)」の集大成とも言える内容になっています。

プロパティ記述の簡略化と不変性の向上

C# 6.0では、クラスのプロパティをより簡潔に、かつ安全に定義するための機能が追加されました。

自動プロパティの初期化子 (Auto-Property Initializers)

以前のバージョンでは、自動プロパティに初期値を設定する場合、必ずコンストラクタ内で代入を行う必要がありました。

C# 6.0からは、プロパティの宣言と同時に初期値を記述できるようになりました。

C#
using System;

public class UserProfile
{
    // 自動プロパティの初期化子を使用して初期値を設定
    public string Role { get; set; } = "Guest";
    public DateTime CreatedAt { get; set; } = DateTime.Now;

    public void DisplayInfo()
    {
        Console.WriteLine($"Role: {Role}, CreatedAt: {CreatedAt}");
    }
}

class Program
{
    static void Main()
    {
        var profile = new UserProfile();
        profile.DisplayInfo();
    }
}
実行結果
Role: Guest, CreatedAt: 202X/XX/XX XX:XX:XX (実行時の日時)

この機能により、フィールド変数を明示的に定義することなく、簡潔に初期値を設定できるようになり、コンストラクタのコード量を削減することに成功しました。

読み取り専用自動プロパティ (Getter-only Auto-Properties)

オブジェクト指向プログラミングにおいて、オブジェクトの状態を変更不可(イミュータブル)に保つことは、バグを減らすための重要な戦略です。

C# 6.0では、setアクセサを持たない「読み取り専用自動プロパティ」が導入されました。

C#
public class Point
{
    // getのみを定義。これによりコンストラクタ以外からの変更を禁止する
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

以前は、これと同じことを実現するために、バッキングフィールド(プライベートな変数)を用意し、読み取り専用のプロパティを手動で記述する必要がありました。

C# 6.0以降は、コンストラクタ内でのみ代入可能な読み取り専用プロパティを一行で定義できるようになったため、イミュータブルな設計がより身近なものとなりました。

Expression-bodied function members

C# 6.0では、メソッドやプロパティの本体が単一の式である場合に、ラムダ式のような形式で記述できる「Expression-bodied members (式形式のメンバー)」が導入されました。

メソッドへの適用

値を返すだけの単純なメソッドや、一行で終わる処理を大幅に短縮できます。

C#
public class Calculator
{
    // 従来の記述
    public int AddClassic(int a, int b)
    {
        return a + b;
    }

    // Expression-bodied members による記述
    public int AddSimple(int a, int b) => a + b;

    public void PrintSum(int a, int b) => Console.WriteLine(a + b);
}

読み取り専用プロパティへの適用

計算結果を返すだけのプロパティにも有効です。

C#
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    // 読み取り専用プロパティを式形式で記述
    public string FullName => $"{FirstName} {LastName}";
}

この記法を採用することで、コードの視認性が向上し、ロジックの本質が際立つようになります。

ただし、複雑なロジックを無理に一行に詰め込むと可読性を損なうため、単純なアクセサや計算に限定して使用するのがベストプラクティスです。

using static ディレクティブ

特定のクラス内の静的メンバー(メソッドや定数)を、クラス名を付けずに直接呼び出せるようにするのがusing staticです。

通常、Math.SqrtConsole.WriteLineのように、静的メソッドを呼び出す際は常にクラス名が必要でしたが、これによって記述を簡略化できます。

C#
using System;
// Mathクラスの静的メンバーをインポート
using static System.Math;

class Circle
{
    public double Radius { get; set; } = 10;

    public double GetArea()
    {
        // Math.PI や Math.Pow ではなく、直接 PI や Pow を記述可能
        return PI * Pow(Radius, 2);
    }
}

この機能は、数学的な計算を多用するコードや、特定のユーティリティクラスを頻繁に利用する場面で、冗長なクラス名を排除し、数式やロジックそのものに集中できるというメリットがあります。

ただし、乱用すると「そのメソッドがどこで定義されているか」が分かりにくくなるため、注意が必要です。

Null 条件演算子 (Null-conditional Operator)

C# 6.0で最も開発者に喜ばれた機能の一つが、Null 条件演算子 (?. および ?[])です。

これは「エルビス演算子」とも呼ばれることがあります。

記述の簡略化

オブジェクトが null でない場合にのみメンバーにアクセスし、null の場合は null を返すという処理を一行で記述できます。

C#
// 従来の null チェック
if (user != null && user.Address != null)
{
    string city = user.Address.City;
}

// C# 6.0 以降
string city = user?.Address?.City;

イベントの安全な呼び出し

デリゲートやイベントの呼び出しにおいても、スレッドセーフで簡潔な記述が可能になりました。

C#
// 従来のイベント呼び出し(スレッドセーフを考慮した一般的な書き方)
var handler = PropertyChanged;
if (handler != null)
{
    handler(this, new PropertyChangedEventArgs(name));
}

// C# 6.0 以降
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

この演算子の導入により、ネストの深い if 文による null チェックが激減し、コードの安全性が飛躍的に高まりました。

また、Null 合体演算子 (??) と組み合わせることで、null の場合のデフォルト値を指定する記述も非常にスマートになります。

文字列補完 (String Interpolation)

文字列の中に変数の値を埋め込む際、以前は string.Format を使用するのが一般的でした。

C# 6.0 では、より直感的に記述できる「文字列補完」が登場しました。

C#
string name = "田中";
int age = 30;

// 従来の string.Format
string s1 = string.Format("名前: {0}, 年齢: {1}", name, age);

// C# 6.0 の文字列補完
string s2 = $"名前: {name}, 年齢: {age}";

// 書式指定も可能
string s3 = $"価格: {1200:C}"; // 通貨形式

文字列補完の最大の利点は、インデックス番号 ( {0}, {1}… ) と引数の対応関係を意識する必要がなくなる点です。

コードを読み書きする際、どの場所にどの変数が埋め込まれるかが一目でわかるため、ミスが減り、メンテナンス性も向上します。

nameof 演算子

nameof 演算子は、変数、型、またはメンバーの「名前」を文字列として取得するための機能です。

C#
public void UpdateUser(string userName)
{
    if (userName == null)
    {
        // 以前は "userName" と文字列で直接書いていた
        throw new ArgumentNullException(nameof(userName));
    }
}

なぜこれが重要かというと、リファクタリング(名前の変更)に強くなるからです。

文字列リテラルで "userName" と書いている場合、変数の名前をリファクタリング機能で変更しても、文字列の中身までは自動で書き換わりません。

しかし nameof(userName) と記述しておけば、コンパイラが型情報を追跡しているため、変数名が変更されれば同時にこの部分も更新されます。

また、スペルミスがあればコンパイルエラーとして検出できるため、実行時の不具合を未然に防ぐことができます。

インデックス初期化子 (Index Initializers)

Dictionary などのコレクションを初期化する際、より直感的なキーと値の指定が可能になりました。

C#
using System.Collections.Generic;

// 従来の初期化(コレクション初期化子)
var oldDict = new Dictionary<int, string>
{
    { 1, "First" },
    { 2, "Second" }
};

// C# 6.0 以降のインデックス初期化子
var newDict = new Dictionary<int, string>
{
    [1] = "First",
    [2] = "Second",
    [10] = "Tenth"
};

この構文は、JSON オブジェクトの定義や設定ファイルの初期化のような、キーに対して値を割り当てるという意図をより明確に表現できます。

また、内部的にはインデクサ (this[]) を呼び出しているため、独自のインデクサを持つクラスでも同様の記述が可能です。

例外処理の強化

C# 6.0 では、例外処理の制御フローも進化しました。

例外フィルタ (Exception Filters)

catch ブロックに条件式を追加し、その条件が真の場合にのみ例外をキャッチできるようになりました。

C#
try
{
    PerformNetworkOperation();
}
catch (System.Net.Http.HttpRequestException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    // 404エラーの場合のみここを通る
    Console.WriteLine("Resource not found.");
}
catch (System.Net.Http.HttpRequestException e)
{
    // それ以外のHTTPエラー
    Console.WriteLine($"Network error: {e.Message}");
}

従来の C# では、一度 catch 内で例外を捕まえてから if 文で判定し、条件に合わなければ throw し直す必要がありました。

しかし、例外フィルタを使用すれば、コールスタックを維持したまま、条件に一致しない例外を上位に透過させることができます。

これはデバッグ時のエラー解析において非常に有利な特徴です。

catch および finally ブロック内での await

非同期プログラミングにおいて制限されていた await の使用範囲が拡大されました。

C#
try
{
    await DoSomethingAsync();
}
catch (Exception ex)
{
    // catch内でも非同期メソッドを待機できる
    await Logger.LogAsync(ex);
}
finally
{
    // finally内での非同期クリーンアップが可能
    await Resource.CloseAsync();
}

これにより、エラー発生時のログ記録や終了処理を非同期で行うことができ、UIスレッドをブロックせずに一連の例外処理を完結させることが可能になりました。

拡張 Add メソッドによるコレクション初期化

C# 6.0 では、コレクション初期化子の動作が少し柔軟になりました。

これまでは、初期化子を使用するためにはクラス自体に Add メソッドが定義されている必要がありましたが、C# 6.0 からは拡張メソッドとして定義された Add メソッドも初期化子の中で呼び出せるようになりました。

これにより、既存のライブラリに含まれるコレクションクラスなど、自分で直接変更できないクラスに対しても、初期化子を利用した簡潔な記述を提供できるようになります。

まとめ

C# 6.0 は、言語のパラダイムを大きく変えるような劇的な変化というよりは、「日々のコーディングにおけるストレスを解消し、よりクリーンなコードを書くための洗練」をもたらしたバージョンと言えます。

本記事で紹介した主な機能を振り返ると、以下の通りです。

機能名主なメリット
自動プロパティ初期化子コンストラクタを使わずに初期値を設定可能
Getter-only プロパティ不変(イミュータブル)な設計を容易にする
式形式のメンバーメソッドやプロパティを簡潔なラムダ形式で記述
Null 条件演算子null チェックのボイラープレートを劇的に削減
文字列補完文字列への変数埋め込みが直感的かつ安全に
nameof 演算子文字列による名前指定を排除し、型安全性を確保
例外フィルタ条件付きの例外キャッチによりスタック情報を保持

これらの機能は、登場から時間が経過した現在でも C# の標準的な書き方として定着しています。

もし今まで「古い書き方」をしていた部分があれば、これら C# 6.0 の機能を積極的に取り入れることで、より読みやすく、バグの少ない、洗練されたソースコードを実現できるでしょう。

C# はこの後も 7.0、8.0、そして最新バージョンへと進化を続け、パターンマッチングや非同期ストリームなどの強力な機能を次々と追加していきます。

そのすべての基礎となる「簡潔な記述」を学んだことで、より高度な機能もスムーズに習得できるはずです。