現在、多くの企業システムや既存のアプリケーションにおいて、.NET Framework 4.8は依然として現役の実行基盤として利用されています。

しかし、C#という言語自体は進化を続けており、最新の.NET(.NET 8や.NET 9など)に合わせてC# 12やC# 13といった新しいバージョンが次々とリリースされています。

開発者であれば「古いフレームワークでも、最新の便利な構文を使ってコードを書きたい」と考えるのは自然なことでしょう。

本記事では、.NET Framework 4.8においてC#の言語バージョンがどこまで引き上げられるのか、そして具体的にどの機能が使えて、どの機能に制限があるのかを、テクニカルな視点から詳しく解説します。

.NET Framework 4.8とC#言語バージョンの関係

まずは、.NET Framework 4.8におけるC#の「公式な」扱いについて整理しておきましょう。

Microsoftのドキュメントによれば、.NET Frameworkで公式にサポートされているC#の最終バージョンは C# 7.3 です

通常、Visual Studioで.NET Framework 4.8向けのプロジェクトを作成すると、言語バージョンは自動的に 7.3 に設定されます。

これは、.NET Frameworkが「メンテナンスモード」に近い状態にあり、新しい言語機能が依存する「ランタイム(実行時エンジン)側の修正」が行われないためです。

しかし、実際にはプロジェクトファイルを編集することで、C# 8.0や9.0、あるいはそれ以上の機能を限定的に利用することが可能です。

なぜデフォルトで制限されているのか

C#の新機能には、大きく分けて以下の3つのパターンが存在します。

  1. コンパイラのみで完結する機能(糖衣構文:シンタックスシュガー)
  2. 特定のライブラリ(BCL)を必要とする機能(属性や特定の型が必要)
  3. ランタイム(CLR)の変更を必要とする機能(実行エンジンの動作変更)

.NET Framework 4.8のランタイム(CLR 4.0)は、C# 8.0以降で導入された「デフォルトインターフェースメソッド」などのランタイムレベルの変更を必要とする機能をサポートしていません

そのため、Microsoftは一貫して「.NET FrameworkではC# 7.3まで」を公式のサポート範囲としているのです。

言語バージョンを強制的に変更する方法

プロジェクトの言語バージョンを引き上げるには、Visual StudioのUIからではなく、プロジェクトファイル(.csproj)を直接編集する必要があります。

.csproj の編集手順

プロジェクトファイルを右クリックして「プロジェクトファイルの編集」を選択するか、テキストエディタで開き、<PropertyGroup> セクション内に <LangVersion> タグを追加します。

XML
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net48</TargetFramework>
    <!-- 言語バージョンを最新(latest)または特定のバージョンに指定 -->
    <LangVersion>latest</LangVersion>
  </PropertyGroup>
</Project>

指定できる値には以下のようなものがあります。

  • latest:インストールされているコンパイラがサポートする最新の「マイナーバージョンを含む」安定版を使用します。
  • default:ターゲットフレームワークに基づくデフォルト(.NET Framework 4.8なら 7.3)を使用します。
  • 10.011.0 などの数値:特定のバージョンを明示的に指定します。

このように設定することで、.NET Framework 4.8をターゲットにしつつ、C# 8.0以降の構文をコンパイラに解釈させることが可能になります

.NET Framework 4.8で「使える」最新機能と「使えない」機能

言語バージョンを最新に設定したとしても、すべての機能が魔法のように使えるわけではありません。

ここでは、機能ごとに利用可否を分類して解説します。

1. 問題なく使用できる機能(コンパイラのみで完結)

これらは「糖衣構文」と呼ばれるもので、コンパイル時に従来のコードへ変換されるため、.NET Framework 4.8でも制約なく利用可能です。

C# 8.0:using宣言

変数のスコープが終わる際に自動で Dispose を呼び出す簡潔な書き方です。

C#
using System;
using System.IO;

public class Sample
{
    public void WriteFile()
    {
        // 従来の using (var sw = ...) { } の代わり
        using var sw = new StreamWriter("test.txt");
        sw.WriteLine("Hello .NET Framework 4.8 with C# 8.0!");
        
        // メソッドを抜ける際に自動的に Dispose される
    }
}

C# 9.0:ターゲット型指定の new 式

左辺から型が明らかな場合、new の後の型名を省略できます。

C#
// 従来:List<string> list = new List<string>();
List<string> list = new();

C# 10.0:ファイルスコープの名前空間

波括弧を減らしてコードをスッキリさせることができます。

C#
namespace MyNamespace; // ファイル全体がこの名前空間に属する

public class MyClass { }

2. 工夫すれば使用できる機能(ライブラリ依存)

これらは、特定の「属性(Attribute)」や「型」が標準ライブラリ(BCL)に含まれていないため、そのままではコンパイルエラーになります。

しかし、自分でその型を定義する、あるいは外部ライブラリを導入することで利用可能になります。

C# 9.0:init専用セッターとRecord型

プロパティを初期化時のみ書き込み可能にする init キーワードや、それを利用する record は、IsExternalInit というクラスが必要です。

C#
// .NET Framework 4.8には存在しないため、自分で定義する
namespace System.Runtime.CompilerServices
{
    public class IsExternalInit { }
}

// これで record が使えるようになる
public record Person(string Name, int Age);

C# 8.0:null許容参照型 (NRT)

コードの安全性向上に寄与するこの機能は、属性に依存しています。

NuGetパッケージなどで属性を補うことで、.NET Frameworkでも静的解析を有効にできます。

3. 使用できない、または制限が強い機能(ランタイム依存)

以下の機能は、.NET Frameworkの実行エンジン自体が対応していないため、原則として使用できません

  • デフォルトインターフェースメソッド (C# 8.0):インターフェースに実装を持たせる機能。CLRの修正が必要なため不可。
  • 静的抽象メンバー(Static Abstract Members in Interfaces) (C# 11.0):ジェネリックな演算子などを定義する機能。.NET 7以降のランタイムが必須。
  • Span<T> 関連の最適化:一部利用可能ですが、.NET Frameworkでは「Slow Span」と呼ばれる動作になり、最新の .NET ほど高いパフォーマンスは得られません。

PolySharp:制限を打破する強力なライブラリ

.NET Framework 4.8で最新C#機能を使いたい場合、手動で属性を定義するのは手間がかかります。

そこで推奨されるのが PolySharp というNuGetパッケージです。

PolySharpは、C#のソースジェネレーター(Source Generators)を利用して、ターゲットフレームワークに足りない属性や型(IsExternalInit、Index、Range、InterpolatedStringHandlerなど)をコンパイル時に自動生成してくれます。

PolySharpの導入メリット

  1. initrecordRequired members などが設定だけで使えるようになる。
  2. バイナリに不要な依存関係を残さず、ソースコードとしてプロジェクト内にポリフィルが埋め込まれる。
  3. .NET Framework 4.8のプロジェクトを、あたかも最新の.NETプロジェクトであるかのような書き味で維持できる。

実際に C# 9.0 までの機能を取り入れたサンプルコード

以下に、.NET Framework 4.8環境で C# 9.0 までの機能(Record、init、Target-typed new)を盛り込んだコード例を示します。

C#
using System;
using System.Collections.Generic;

// 手動定義、または PolySharp により提供される必要がある
namespace System.Runtime.CompilerServices
{
    internal static class IsExternalInit { }
}

namespace FrameworkModernization
{
    // C# 9.0 の record 型を使用
    public record User(int Id, string Name)
    {
        // C# 9.0 の init 専用セッター
        public string Role { get; init; } = "Guest";
    }

    class Program
    {
        static void Main(string[] args)
        {
            // C# 9.0 のターゲット型指定 new
            List<User> users = new();

            // C# 8.0 のオブジェクト初期化子と init
            var admin = new User(1, "管理者")
            {
                Role = "Admin"
            };

            users.Add(admin);
            users.Add(new User(2, "一般ユーザー"));

            foreach (var user in users)
            {
                // C# 6.0~ の文字列補完
                Console.WriteLine($"ID: {user.Id}, Name: {user.Name}, Role: {user.Role}");
            }

            // Record の Equals 動作確認
            var anotherAdmin = admin with { Name = "管理者(更新)" };
            Console.WriteLine($"Is same user object?: {ReferenceEquals(admin, anotherAdmin)}");
        }
    }
}
実行結果
ID: 1, Name: 管理者, Role: Admin
ID: 2, Name: 一般ユーザー, Role: Guest
Is same user object?: False

このプログラムは、<LangVersion>9.0</LangVersion> 設定と IsExternalInit の定義さえあれば、.NET Framework 4.8上でも問題なく動作します

注意点と運用上のアドバイス

最新のC#機能を .NET Framework 4.8 で利用する際には、いくつか注意すべきリスクがあります。

1. チーム開発での認識合わせ

.csproj を書き換えて言語バージョンを上げる行為は、チーム全員のビルド環境(Visual Studioのバージョンなど)に影響を与えます。

古いバージョンのVisual Studioを使っている開発者がいる場合、新しい構文を解釈できずエラーになる可能性があります。

2. サポート外の構成であること

繰り返しになりますが、Microsoftは「.NET Framework で C# 8.0 以降を使うこと」を公式にはサポートしていません。

コンパイラが生成したコードが、特定の条件下で .NET Framework の古いランタイムと予期せぬ競合を起こす可能性はゼロではありません。

ミッションクリティカルな箇所に導入する場合は、十分なテストが必要です。

3. 移行への準備として活用する

この手法の最大のメリットは、将来的に .NET 8 などの最新ランタイムへ移行する際の「コードのギャップ」を埋められる点にあります。

今のうちから recordusing declaration を使ってモダンな書き方に慣れておくことで、フレームワーク移行時のリファクタリングコストを大幅に削減できます。

まとめ

.NET Framework 4.8環境においても、設定次第でC# 8.0から12.0以降の多くの機能を利用することが可能です。

  • 基本戦略.csproj<LangVersion> を変更する。
  • 構文の活用:糖衣構文(using宣言、ターゲット型newなど)はそのまま使える。
  • 不足の解消recordinit は、PolySharp などのライブラリや自己定義で補う。
  • 限界の把握:ランタイムの変更を伴う機能(デフォルトインターフェースメソッド等)は諦める。

古いフレームワークを使っているからといって、古い書き方に縛られる必要はありません。

適切な設定とツールを活用し、保守性の高い洗練されたC#コードを .NET Framework 4.8 の上でも実現していきましょう。