C#は、Microsoftが開発したオブジェクト指向プログラミング言語として、長年にわたり進化を続けてきました。

その中でもC# 7.0は、開発者の生産性を飛躍的に向上させるための重要なアップデートとして位置付けられています。

このバージョンでは、タプル、パターンマッチング、ローカル関数といった、現代的なプログラミングにおいて欠かせない強力な機能が多数導入されました。

本記事では、C# 7.0で導入された主要な新機能のすべてを網羅し、具体的なコード例とともにその使い方を徹底解説します。

従来の記述方法とどのように変わり、どのようなメリットが得られるのかを深く掘り下げていきましょう。

1. out 変数(out variables)の導入

C# 7.0以前のバージョンにおいて、out引数を持つメソッドを使用する場合、あらかじめ変数を宣言しておく必要がありました。

しかし、C# 7.0からは引数リストの中で直接変数を宣言できる「out 変数」が導入され、コードが大幅に簡略化されました。

1.1 従来の記述との比較

例えば、文字列を数値に変換するint.TryParseメソッドを使用する場合、以前は以下のような記述が必要でした。

C#
// C# 6.0 以前の書き方
int result; // 事前に宣言が必要
if (int.TryParse("123", out result))
{
    Console.WriteLine(result);
}

これがC# 7.0では、次のように記述できます。

C#
// C# 7.0 以降の書き方
if (int.TryParse("123", out int result))
{
    Console.WriteLine(result);
}

1.2 スコープと型推論

out 変数で宣言された変数のスコープは、その変数を囲むブロック内に及びます。

そのため、if文の外側でもその変数を利用することが可能です。

また、型を明示せずにvarを使用して型推論を行うこともできます。

C#
if (int.TryParse("456", out var val))
{
    // val は int 型として推論される
    Console.WriteLine($"数値: {val}");
}
// ここでも val を使用可能
Console.WriteLine($"スコープ外での利用: {val}");
実行結果
数値: 456
スコープ外での利用: 456

2. タプル(Tuples)の強化

C# 7.0における最大の目玉機能の一つが、タプル(Tuples)の言語サポートです。

これまでもSystem.Tupleクラスが存在していましたが、参照型であることによるパフォーマンスの問題や、要素へのアクセスがItem1Item2といった不透明な名前であることなど、不便な点が多くありました。

C# 7.0で導入された新しいタプルは、値型(System.ValueTuple)であり、軽量かつ柔軟なデータ構造を提供します。

2.1 タプルの宣言と戻り値

複数の値を一つの戻り値として返したい場合、これまでは独自クラスや構造体を定義するか、out引数を使用していました。

タプルを使えば、メソッドから複数の値を簡潔に返せます。

C#
using System;

class Program
{
    static void Main()
    {
        // メソッドからタプルを受け取る
        var person = GetPersonInfo();
        
        // 名前を付けてアクセス可能
        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
    }

    // タプルを戻り値に持つメソッド
    static (string Name, int Age) GetPersonInfo()
    {
        return ("田中太郎", 30);
    }
}
実行結果
Name: 田中太郎, Age: 30

2.2 タプルの分解(Deconstruction)

タプルは受け取る側で「分解」して、個別の変数に代入することも可能です。

これにより、一時的なタプル変数を作成せずに直接値を利用できます。

C#
// タプルを分解して変数に代入
(string name, int age) = GetPersonInfo();
Console.WriteLine($"{name}さんは{age}歳です。");

// 型推論を利用した分解
var (n, a) = GetPersonInfo();

また、特定の値を無視したい場合は、アンダースコア_を用いた「破棄(Discards)」が利用できます。

C#
// 年齢だけが必要な場合、名前を破棄する
(_, int onlyAge) = GetPersonInfo();
Console.WriteLine($"年齢のみ取得: {onlyAge}");

3. パターンマッチング(Pattern Matching)

パターンマッチングは、オブジェクトの型や値の構造に基づいて分岐処理を行うための強力な仕組みです。

C# 7.0では、主にis式とswitch文が拡張されました。

3.1 is 式によるパターンマッチング

これまでのis式は型のチェックしかできませんでしたが、C# 7.0からは「型チェックと同時に変数への代入」が行えるようになりました。

C#
public void PrintArea(object shape)
{
    // shape が Circle 型であれば c に代入してブロック内で使用する
    if (shape is Circle c)
    {
        Console.WriteLine($"円の面積: {c.Radius * c.Radius * Math.PI}");
    }
    else if (shape is Rectangle r)
    {
        Console.WriteLine($"長方形の面積: {r.Width * r.Height}");
    }
}

3.2 switch 文の拡張

switch文でも型による分岐が可能になり、さらにwhen句を使用することで追加の条件(ガード条件)を指定できるようになりました。

C#
public void DisplayShapeInfo(object shape)
{
    switch (shape)
    {
        case Circle c when c.Radius > 10:
            Console.WriteLine("大きな円です。");
            break;
        case Circle c:
            Console.WriteLine("小さな円です。");
            break;
        case Rectangle r when r.Width == r.Height:
            Console.WriteLine("正方形です。");
            break;
        case null:
            throw new ArgumentNullException(nameof(shape));
        default:
            Console.WriteLine("未知の図形です。");
            break;
    }
}

注意点として、case文の評価順序が重要になります。

以前のC#ではswitch文の順序は任意でしたが、パターンマッチングでは上から順に評価されるため、より具体的な条件を上に記述する必要があります。

4. ローカル関数(Local Functions)

ローカル関数とは、メソッドの内部で定義されるメソッドのことです。

特定のメソッド内でしか使用されないヘルパー関数を、プライベートメソッドとしてクラス全体に公開することなく、適切にカプセル化できます。

4.1 基本的な使い方

C#
public void ProcessData(int[] numbers)
{
    if (numbers == null) return;

    // ローカル関数の定義
    int CalculateSquare(int n) => n * n;

    foreach (var number in numbers)
    {
        Console.WriteLine($"{number} の2乗は {CalculateSquare(number)}");
    }
}

4.2 ラムダ式との違い

ローカル関数はラムダ式(匿名関数)と似ていますが、以下の点で優れています。

特徴ローカル関数ラムダ式 (Func/Action)
パフォーマンス低コスト(デリゲートの割り当てがない)デリゲート生成によるオーバーヘッドがある
再帰呼び出し容易に記述可能宣言と代入を分ける必要があり複雑
ジェネリクスサポートされているサポートされていない
記述場所定義場所より前で呼び出し可能変数宣言後でないと呼び出し不可

特にイテレータ(yield return)や非同期メソッド(async/await)において、引数のバリデーションを即座に行いたい場合にローカル関数は非常に有効です。

5. リテラルの改善

数値リテラルの可読性を高めるために、2つの大きな改善が行われました。

5.1 2進数リテラル

これまで16進数は0xで表現できましたが、C# 7.0からは0bを用いることで2進数を直接記述できるようになりました。

フラグ管理やビット演算を行う際に非常に便利です。

C#
int binaryData = 0b0010_1010; // 10進数で 42

5.2 桁区切り記号(Digit Separators)

大きな数値を記述する際、桁を把握しやすくするためにアンダースコア_を区切り文字として挿入できるようになりました。

C#
long bigNumber = 1_000_000_000;
double pi = 3.1415_9265_35;
int hex = 0xFF_AA_00;

このアンダースコアはコンパイル時に無視されるため、実行時のパフォーマンスに影響はありません。

6. 参照戻り値と参照ローカル変数(Ref returns and locals)

パフォーマンスを極限まで追求するシナリオにおいて、値のコピーを避けるための機能が追加されました。

それが参照戻り値(ref return)です。

通常、メソッドが値を返すとその値はコピーされますが、refキーワードを使用すると、変数そのもの(メモリ上の場所)への参照を返すことができます。

C#
class DataStorage
{
    private int[] _numbers = { 1, 10, 100, 1000 };

    // 特定の要素の参照を返す
    public ref int GetElementRef(int index)
    {
        return ref _numbers[index];
    }

    public void PrintAll() => Console.WriteLine(string.Join(", ", _numbers));
}

// 利用側
var storage = new DataStorage();
ref int element = ref storage.GetElementRef(1); // 参照を受け取る
element = 99; // 参照先(_numbers[1])が直接書き換わる

storage.PrintAll();
実行結果
1, 99, 100, 1000

この機能は、巨大な構造体(struct)を扱うゲーム開発や、高頻度で配列要素にアクセスする数値計算ライブラリなどで特に効果を発揮します。

7. 一般化された非同期戻り値型(ValueTask)

C# 5.0で導入されたasync/awaitは非常に便利ですが、戻り値が常にTaskまたはTask<T>であるため、ヒープへのメモリ割り当てが発生するという課題がありました。

C# 7.0では、ValueTask<T>の導入により、この問題を解決しました。

7.1 ValueTask のメリット

もし非同期メソッドの結果が、呼び出し時点で既に判明している(同期的に完了できる)場合、ValueTaskを使用することでTaskオブジェクトの生成(アロケーション)を回避できます。

C#
public async ValueTask<int> GetDataAsync(int id)
{
    // キャッシュにデータがあれば、Taskを作成せずに即座に値を返す
    if (_cache.ContainsKey(id))
    {
        return _cache[id];
    }

    // 実際的な非同期処理
    int result = await FetchFromDatabaseAsync(id);
    _cache[id] = result;
    return result;
}

高頻度で呼び出される非同期処理において、ガベージコレクション(GC)の負荷を軽減するために極めて重要な機能です。

8. 式形式のメンバーの拡充(Expression-bodied members)

C# 6.0で導入された式形式のメンバー(=>を用いた簡略記述)が、C# 7.0ではさらに多くの場所で使えるようになりました。

新たにサポートされた対象:

  • コンストラクタ
  • デストラクタ
  • プロパティの get/set アクセサ
C#
class User
{
    private string _name;

    // コンストラクタを式形式で記述
    public User(string name) => _name = name;

    // プロパティのアクセサを式形式で記述
    public string Name
    {
        get => _name;
        set => _name = value ?? "ゲスト";
    }
}

9. throw 式(throw expressions)

これまでthrowは「文」であり、式の中に記述することはできませんでした。

C# 7.0からは「throw 式」として、条件演算子やNull合体演算子の中に組み込めるようになっています。

C#
public void SetTitle(string title)
{
    // Null合体演算子の中で throw を使用
    _title = title ?? throw new ArgumentNullException(nameof(title));
}

// 条件演算子の中での例
int GetLength(string s) => s != null ? s.Length : throw new Exception("文字がありません");

これにより、バリデーションロジックをより簡潔に、流れるように記述することが可能となりました。

10. まとめ

C# 7.0は、言語の表現力を大幅に高め、冗長なコードを削減することに主眼を置いたアップデートでした。

本記事で紹介した主要機能を振り返ってみましょう。

カテゴリ機能名主なメリット
構文の簡略化out 変数事前宣言を不要にし、コードをクリーンにする
データ構造タプルと分解複数の戻り値を軽量に扱い、直感的に代入できる
制御フローパターンマッチング型判定とキャストを統合し、複雑な分岐を整理する
カプセル化ローカル関数メソッド内の補助処理を適切に隠蔽し、効率化する
可読性数値リテラルの改善2進数や大きな数値を視覚的に分かりやすくする
最適化ref 戻り値 / ValueTaskメモリコピーやアロケーションを抑制し、性能を追求する
記述の柔軟性throw 式 / 式形式の拡充より少ないコード行数で意図を表現できる

これらの機能は、現在のC#開発においても基盤となる重要な要素ばかりです。

特にタプルやパターンマッチングは、その後のC# 8.0、9.0、そして最新バージョンへと続く進化の出発点となりました。

古いコーディングスタイルから脱却し、C# 7.0で導入されたこれらの機能を積極的に活用することで、より安全で、読みやすく、パフォーマンスの高いアプリケーションを構築することができるでしょう。

各機能を自分のプロジェクトにどう適用できるか、ぜひこの機会に試してみてください。