.NET

F# 9の新機能まとめ|null許容参照型・Isプロパティの実装とC# 13・F# 10との違い

F# 9は.NET 9と同時に出た言語バージョンで、目玉はC#資産と安全につなぐためのnull許容参照型です。ただし機能を並べただけの解説はプロジェクト設定の落とし穴を飛ばしがちで、そのまま真似ても警告が一切出ないまま「null安全になったつもり」で終わります。ここではF# 9の各機能を .NET SDK 9.0.316 と 10.0.302 で実際にビルドし、出た警告番号と実測値をそのまま示します。C# 13との守備範囲の違い、Native AOTの可否、すでにメンテナンス期に入った.NET 9をどうするかまで扱います。

まとめ

F# 9で実務に効くのはnull許容参照型(string | null判別共用体の.Is*プロパティ部分アクティブパターンのbool返却空の計算式FSharp.Coreのランダム関数構造体判別共用体のメモリ削減の6点です。これに加えて、C#のコレクション式からF#のlist/setを初期化できるようになり、--realsig+によるprivateメンバーの実可視化(Real visibility)と等値比較・整数レンジの最適化も入っています。なお「非同期処理の強化」はF# 9の内容ではなく、パターンマッチング関連で入ったのは部分アクティブパターンがboolを返せるようになった点だけです。

実務上の要点は3つです。第一に、null許容参照型はプロジェクトに<Nullable>enable</Nullable>を書かないと構文が通るだけで検査されません。第二に、C# 13でよく挙げられるCountByAggregateByは言語機能ではなく.NET 9のライブラリ追加で、C#のバージョンを上げても.NET 8のままでは使えません。第三に、.NET 9は2026年11月10日でサポートが切れます。新規に選ぶ理由はもうなく、移行先はF# 10・C# 14を含む.NET 10(LTS)です。以下、各機能を実行結果とあわせて見ていきます。

F# 9とC# 13が動く.NETバージョンの対応

F#とC#の言語バージョンは、SDKのターゲットフレームワークとほぼ一対一で決まります。プロジェクトのTargetFrameworkを変えずに言語バージョンだけ上げても、ライブラリ側の新APIは付いてきません。

ランタイム F# C# リリース サポート 終了日 フェーズ
.NET 8 F# 8 C# 12 2023-11-14 LTS 2026-11-10 maintenance
.NET 9 F# 9 C# 13 2024-11-12 STS 2026-11-10 maintenance
.NET 10 F# 10 C# 14 2025-11-11 LTS 2028-11-14 active

終了日とフェーズはMicrosoftが公開しているリリースメタデータ(releases-index.json)のeol-datesupport-phaseです。2026年8月時点でactiveなのは.NET 10だけで、.NET 8と.NET 9はどちらもすでにメンテナンス段階です。なおF# 10はVisual Studio 2026に同梱され、Visual Studio 2022では扱えません。

手元の環境を確かめるにはdotnet --list-sdksでインストール済みSDKを一覧します。新規にF#プロジェクトを作るならdotnet new console -lang F#で、言語バージョンはターゲットフレームワークから決まるため通常<LangVersion>の指定は不要です。以降で示す実行結果は、SDK 9.0.316と10.0.302で得たものです。

null許容参照型|Nullableを書かないと検査されない落とし穴

F#はもともとnullを避ける設計ですが、C#で書かれた.NETライブラリを呼ぶと現実にはnullが入ってきます。F# 9はその境界を型で表す構文string | nullを導入しました。縦棒は判別共用体のケース区切りと同じ記号で、「基底の型」と「null許容参照」の和集合を作ります。

オプトインの有無で警告が出るかを実測

ここが最大の注意点です。この機能は既定でオフで、有効化にはプロジェクトファイルへのプロパティ追加が要ります。追加するとコンパイラに--checknulls+が渡ります。

<PropertyGroup>
  <TargetFramework>net9.0</TargetFramework>
  <Nullable>enable</Nullable>
</PropertyGroup>

次の3行を、このプロパティ有りと無しの両方でビルドして比べました。

let notAValue: string | null = null
let isNotAValue2: string = null
let getLength (x: string | null) = x.Length

プロパティ無しの結果は警告0件・エラー0件でした。| nullという注釈は構文として素通りし、nullを非null型に代入しても、null許容の値をそのまま参照しても何も言われません。プロパティを入れると、2行目にwarning FS3261: Nullness warning: The type 'string' does not support 'null'.、3行目にwarning FS3261: Nullness warning: The types 'string' and 'string | null' do not have compatible nullability.が出ます。注釈を書いたこと自体は安全性の担保になりません。

パターンマッチでnullを消すと警告が消える書き方

警告を黙らせるのではなく、型を絞って解消します。matchの最初の句でnullを処理すると、以降の束縛は非nullとして扱われ、同じ<Nullable>enable</Nullable>のままでも警告は出ませんでした。

let safeLength (str: string | null) =
    match str with
    | null -> -1
    | s -> s.Length

構文上の罠がひとつ。判別共用体のフィールドに| nullを直接書くと縦棒がケース区切りと衝突するため、type DUField = N of (string | null)のように括弧が要ります。

なお、この機能はC#との相互運用のための道具です。F#の中だけで「値が無いかもしれない」を表すなら従来どおりoptionが慣用で、Microsoftのスタイルガイドもそう案内しています。失敗を戻り値で扱う設計は鉄道指向プログラミングとは|Result型で書くエラーハンドリング入門が参考になります。

判別共用体の.Is*プロパティと構造体DUのフィールド共有

F# 9では判別共用体まわりに、書き味とメモリ効率の両方で変化がありました。型そのものの基礎は判別可能なユニオン型(Discriminated Union)の基本とその定義で解説しています。

match式を書かずにケースを判定する.Is*プロパティ

各ケースに対応するIs付きプロパティが自動生成され、「このケースかどうか」を知りたいだけの場面でmatch式が不要になります。

type Contact =
    | Email of address: string
    | Phone of countryCode: int * number: string

let c = Email "[email protected]"
printfn "%b %b" c.IsEmail c.IsPhone

実行結果はtrue falseでした。従来はmatch person.contact with | Email _ -> true | _ -> falseと書いていた判定が1式で済みます。分岐そのものが目的の箇所まで置き換えると網羅性チェックを失うので、真偽値がほしいだけの場所に限って使うのが妥当です。

同名フィールドの共有でsizeofが40バイトから16バイトへ

構造体判別共用体で、複数のケースが同じ名前・同じ型のフィールドを持つ場合、メモリ位置を共有するようになりました。F# 8までは同名フィールドが許されなかったため、既存コードの互換性を壊さずに効く改善です。int64を持つ4ケースの構造体DUで実測しました。

[<Struct>]
type Shared =
    | Length of int64
    | Time of int64
    | Temperature of int64
    | Pressure of int64

printfn "%d" sizeof<Shared>

この形(フィールド名を省略=共有される)が16バイトLength of length: int64のようにケースごとに異なる名前を付けた版が40バイトでした。公式ドキュメントは7ケースの例で60バイトから16バイトへ縮む数字を挙げており、上の40バイトはケース数を4に減らして測り直した値です。いずれにせよ、ケース数に比例して膨らんでいた構造体がタグ+共有フィールドぶんに収まります。単位付きの値を型で区別するようなコードでは、配列に大量に持つほど差が出ます。

F# 9のその他の改善|構文・相互運用・性能

目玉機能の陰に隠れがちですが、日々の記述量と実行性能に効く変更が続いています。ここでは構文の緩和、FSharp.Coreの追加関数、C#との相互運用、そして既存コードを書き換えずに効く最適化の順に見ます。

bool返却の部分アクティブパターンと引用符が不要になったディレクティブ

部分アクティブパターンはSome ()Noneを返す決まりでしたが、F# 9ではboolを返せます。判定関数をそのまま置けるので、意味のないSome ()が消えます。

let (|CaseInsensitive|_|) (pattern: string) (value: string) =
    System.String.Equals(value, pattern, System.StringComparison.OrdinalIgnoreCase)

let classify key =
    match key with
    | CaseInsensitive "foo" -> "foo"
    | _ -> "other"

printfn "%s" (classify "FOO")

classify "FOO"fooを返します。判定関数をそのまま部分アクティブパターンにできるので、Some ()を返すためだけの分岐が不要になります。

あわせて、中身が空の計算式が書けるようになりました。seq { }は空のシーケンスになり(実測でSeq.lengthが0)、内部ではビルダーのZeroメソッドが呼ばれます。従来のbuilder { () }より意図が読み取れます。コンパイラディレクティブの引数から引用符が不要になったのも同じ系統の変更で、#nowarn 0070#nowarn FS0057のいずれも受け付けます。

シャッフルとサンプリングを担うランダム関数

ListArraySeqShuffleChoiceChoicesSampleの4系統が入りました。各系統に、共有のRandomを使う版、Randomインスタンスを渡す版、0.0以上1.0未満を返す乱数生成関数を渡す版の3種類があります。

let players = [ "Alice"; "Bob"; "Charlie"; "Dave" ]
printfn "%s" (players |> List.randomChoice)
printfn "%A" (players |> List.randomSample 2)
printfn "%A" (players |> List.randomChoices 5)

実行するとDave["Dave"; "Charlie"]["Bob"; "Dave"; "Bob"; "Bob"; "Alice"]のように返ります。randomChoicesは重複を許す抽出なので要素数を超える個数を指定でき、randomSampleは重複を許さないため、コレクション長を超える個数を渡すとArgumentExceptionになります。配列にはさらに、新しい配列を作らず既存の配列を直接並べ替えるArray.randomShuffleInPlaceのようなInPlace版があります。テストデータの生成やゲームの初期配置で、毎回自前のシャッフルを書く必要がなくなります。

C#のコレクション式によるF# list・setの初期化

C#側からF#の不変コレクションを作るとき、これまではSetModule.FromArrayのようなモジュール関数を呼ぶ必要がありました。F# 9からはC#のコレクション式がそのまま使えます。

using Microsoft.FSharp.Collections;

FSharpSet<int> mySet = [1, 2, 3];
FSharpList<string> myList = ["a", "b"];

F#のlistとsetは構造的等価性を持つ点がSystem.Collections.Immutableにはない利点で、C#側から等価性で比較したいデータを扱うときに選ぶ理由になります。null許容参照型と並んで、F# 9が相互運用の摩擦を減らしにきた変更です。

Real visibilityと書き換え不要の性能改善

F#にはprivateメンバーがIL上internalとして書き出される癖があり、InternalsVisibleToを持つ他言語プロジェクトから触れてしまっていました。F# 9では<RealSig>true</RealSig>(コンパイラフラグ--realsig+)でこれを本来の可視性にできます。既定はオフなので、有効化して壊れる箇所がないか確かめてから採用してください。

コードを一行も変えずに効く改善もあります。等値比較はボックス化を避けるようになり、公式ベンチマークでは2メンバーの構造体に対するArray.containsの該当なしケースが、約5,191ナノ秒・24,000バイト割り当てから約766ナノ秒・割り当てなしへ変わりました。start..finish形式の整数レンジも、従来はintかつステップ±1のときしか最適化されなかったものが他の整数型や任意のステップへ広がり、ループで1.25倍から8倍の高速化が報告されています。

属性ターゲットの強制がlet束縛に効かない範囲|FS0842のSDK別実測

F# 9はAttributeTargetsの検査強化を掲げ、xUnitで()を付け忘れたテストが黙って無視される例を挙げてerror FS0842になると説明しています。ところが検査が実際に届く範囲はSDKによって違い、この代表例はF# 9では素通りします。xunit 2.9.2を参照した状態で、ドキュメントどおりの例をビルドしました。

module Tests

open Xunit

[<Fact>]
let ``this test always fails`` =
    Assert.True(false)

SDK 9.0.316では警告0件・エラー0件で通ります。同じプロジェクトをSDK 10.0.302でビルドするとwarning FS0842: This attribute cannot be applied to property, field, return value. Valid targets are: methodが出ました。ターゲットフレームワークをnet9.0のままにしてもnet10.0にしても同じです。F# 10のドキュメント自身がこの例に「Silently ignored in F# 9, not a test!」と注記しており、F# 9で無視されることは公式にも認められています。

ただし「F# 9では属性ターゲットが一切見られない」わけではありません。適用先を変えて切り分けると、次のように分かれました。

誤って適用した先 SDK 9.0.316 SDK 10.0.302
クラス error FS0842 warning FS0842
構造体 検知なし warning FS0842
let束縛の値(xUnitの例) 検知なし warning FS0842

SDK 9で確実に止まるのはクラスへの誤適用だけで、そこはドキュメントどおりエラーです。SDK 10は検知対象を構造体とlet束縛まで広げた代わりに、FS0842自体をエラーから警告へ格下げしました。厳しくなったのか緩くなったのかが適用先によって逆になる、という珍しい変更です。

判断はこうなります。テストが静かに無視される事故をビルドで止めたいなら、ターゲットはnet9.0のままでよいのでビルドマシンのSDKだけ10系へ上げてください。ただしSDK 10ではクラスへの誤適用がエラーから警告に落ちるため、TreatWarningsAsErrorsを併用しないと従来止まっていたビルドが通ってしまいます。SDK更新時はこの2方向の変化をセットで見るのが安全です。

F#のNative AOT発行可否|printfが実行時に落ちる条件

発行そのものはできます。Native AOTはC#専用の機能ではなく、プロジェクトに<PublishAot>true</PublishAot>を足してランタイム識別子付きでdotnet publish -r osx-x64 -c Releaseのように発行する手順はF#でも同じです。

<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>

問題は発行が通った後です。実際にF#コンソールアプリをAOT発行したところ、ビルドは成功しつつwarning IL3053: Assembly 'FSharp.Core' produced AOT analysis warnings.warning IL2104が出ました。この警告は自分のコードではなくFSharp.Coreに対するもので、開発者側では消せません。そして生成されたバイナリを実行すると、次のコードの2行目で落ちます。

type Rec = { Name: string; Age: int }

[<EntryPoint>]
let main _ =
    let r = { Name = "Alice"; Age = 30 }
    printfn "%A" r
    printfn "%s is %d" r.Name r.Age
    0

printfn "%A"は正常に出力されるのに、%s%dを組み合わせた次の行でSystem.NotSupportedException: ... 'PrintfImpl+Specializations...CaptureFinal1[System.Int32]' is missing native code. MethodInfo.MakeGenericMethod() is not compatible with AOT compilation.が投げられました。F#の書式指定はジェネリックメソッドを実行時に組み立てて実現しており、その組み立てがNative AOTでは行えません。Native AOTがSystem.Reflection.Emitや動的なジェネリック実体化を禁じ、トリミングを前提とする以上、この依存が残る限りぶつかります。

重要なのは、これがF# 10でも直っていないことです。同じコードをnet10.0とFSharp.Core 10.1.302でAOT発行し直しても、まったく同じ例外で落ちました。F# 10で改善したのはトリミングの手間の方で、大きなF#メタデータを落とすためのILLink.Substitutions.xmlを手で管理する必要がなくなり、実測でもバイナリが約4.11MBから約3.71MBへ1割ほど縮みます。F#リポジトリのAOT・トリミング追跡Issue(dotnet/fsharp#13398)が2022年6月の起票から2026年8月現在もオープンのまま(最終更新2026-04-17)なのは、こうした積み残しがあるためです。

したがって実務上の指針は明確です。F#でAOTを狙うなら、ログや出力をprintfnではなくConsole.WriteLineや文字列補間へ置き換えてください。そのうえで発行後のバイナリを必ず実機で走らせます。ビルドが通ったことは動作の保証になりません。

環境面はF# 9世代で前進しました。Native AOTの対応プラットフォーム表では、.NET 8で実験的扱いだったiOS・tvOS・MacCatalystが.NET 9以降はその表記から外れ、Windowsにx86、LinuxとAndroidにArmが加わっています(Androidは引き続き実験的で、Javaとの相互運用は同梱されません)。

C# 13の新機能とCountBy・AggregateByをめぐる誤解

F# 9と同じ.NET 9世代の言語がC# 13です。両者は同時期に出るため機能が混同されやすく、とくにライブラリの追加が言語機能として語られる誤りが目立ちます。まず公式の一覧を確認してから、その代表例を実行して切り分けます。

C# 13で入った言語機能

C# 13は.NET 9とともに提供され、言語機能としてはparamsコレクション(配列以外にSpanIEnumerable系も受け取れる)、System.Threading.Lockを認識する新しいlock、エスケープシーケンス\e、メソッドグループ自然型の解決改善、オブジェクト初期化子での末尾からのインデックス指定、イテレーターと非同期メソッド内でのrefローカルとunsaferef structによるインターフェイス実装とallows ref struct制約、部分プロパティ・部分インデクサー、OverloadResolutionPriorityAttributeが入りました。fieldキーワードはC# 13ではプレビュー扱いで、正式化はC# 14です。

このうち部分プロパティは、ソースジェネレーターと組み合わせる場面で効きます。実装宣言に{ get; set; }という自動プロパティ構文が使えないなどの制約があるので、C#のpartialとは?部分クラス・部分プロパティの使いどころと書き方で書き分けを確認してください。

CountByとAggregateByは言語機能ではなく.NET 9のライブラリ追加

「C# 13の新機能」としてCountByAggregateByを挙げる解説を見かけますが、これは誤りです。両者はLINQ、つまりSystem.Linq.Enumerableに.NET 9で追加された拡張メソッドで、C#の言語仕様は関係しません。実際に確認しました。

string[] words = ["apple", "avocado", "banana", "blueberry", "cherry"];
foreach (var g in words.CountBy(w => w[0]))
    Console.WriteLine($"{g.Key}:{g.Value}");

net9.0ではa:2 b:2 c:1と出力され、リフレクションで宣言元をたどるとSystem.Linq 9.0.0.0でした。LangVersionを13に固定したままTargetFrameworkだけnet8.0へ下げると、同じコードがerror CS1061でコンパイルできません。言語バージョンではなくランタイムのバージョンが可否を決めていることがこれではっきりします。F#から使う場合も同じで、F# 9かどうかではなく.NET 9で動いているかが条件です。LINQ自体の位置づけはLINQの読み方は「リンク」|C#・.NETの統合言語クエリを基礎から解説にまとめています。

.NET 9のサポート終了とF# 10・C# 14への移行判断

.NET 9はSTS(標準サポート期間)リリースで、2026年11月10日にサポートが終了します。2026年8月時点の最新パッチは9.0.18で、公開メタデータ上のフェーズはすでにmaintenanceです。同日に.NET 8も終了するため、8と9のどちらに留まっていても期限は変わりません。

ここで注意したいのが期限の食い違いです。STSはもともと18か月サポートで、その計算だと.NET 9は2026年5月に切れるはずでした。この枠が24か月へ延長された結果、LTSの.NET 8と同じ2026年11月10日に揃っています。「.NET 9は2026年5月まで」と書かれた記事は延長前の情報です。判断に使うなら、リリースメタデータのeol-dateを直接見てください。

移行先は.NET 10(LTS)です。2025年11月11日にリリースされ、サポートは2028年11月14日まで続きます。作業自体はプロジェクトファイルの<TargetFramework>net10.0へ書き換え、NuGetパッケージを更新して再ビルドするのが基本で、規模が大きい場合は.NET Upgrade Assistantで一括変換できます。C# 14の変更点や移行の詳細は.NET 10(LTS)とは?サポート期限・新機能・C# 14と.NET 8/9からの移行を解説にまとめています。C# 14ではfieldキーワードがプレビューから正式化され、拡張メンバーを宣言するextensionブロックが加わります。さらに次のC# 15は.NET 11とともに提供予定です(.NET 11の新機能・リリース日・ロードマップ総まとめ|C# 15とRuntime Async、.NET 10との違い)。

アップグレードで壊れやすい箇所

再ビルドが一発で通らない典型は3つあります。null許容参照型を有効にすると既存コードにFS3261が大量に出るので、ソリューション全体で一度に入れずC#との境界にあるプロジェクトから順に進めてください。前述のReal visibilityを併せて有効化すると、InternalsVisibleTo経由でprivateメンバーへ触っていたテストコードが通らなくなります。引数付きで呼び出しているプロパティも、同名の拡張メソッドが優先解決されるようになったため束縛先が変わる可能性があります。

F# 10側の破壊的変更も押さえておきます。#nowarnディレクティブは複数行指定と空指定が禁止され、警告番号に三重引用符・文字列補間・逐語文字列を使えなくなりました。スクリプトでの適用範囲も、以前のコンパイル全体から.fsファイルと同じくファイル末尾または対応する#warnonまでに変わります。seqを省いた{ 1..10 }も非推奨で、SDK 10.0.302でwarning FS3873: This construct is deprecated. Sequence expressions should be of the form 'seq { ... }'が出ることを確認しました。まだ警告どまりですが、将来のF#でエラーになる可能性が案内されているため、移行のついでに直しておくのが得策です。ただし#nowarnの間の空白禁止だけは、ドキュメントの記述に反してSDK 10.0.302で# nowarn 0070が警告なく通りました。移行の障害としては見込まなくて構いません。

よくある質問

F# 9はどの.NETで使えますか?

F# 9は.NET 9に同梱されています。プロジェクトのTargetFrameworknet9.0にすれば既定で有効で、インストール済みSDKはdotnet --list-sdksで確認できます。.NET 10のSDKでもnet9.0をターゲットにしたビルドはできますが、属性ターゲット検査のようにコンパイラ側で決まる挙動は10系のものになります。

F#とC#はどちらを選ぶべきですか?

同じ.NETライブラリを共有できるので、二者択一というより適材適所です。不変データと網羅的なパターンマッチで状態遷移を表したい領域、ドメインの制約を型で潰したい領域はF#が短く安全に書けます。一方、UIフレームワークやサンプルコード、求人の量はC#が圧倒的で、Native AOTのようにC#が先行して整備される領域もあります。既存のC#資産にF#プロジェクトを1つ足して境界を切る形が現実的です。

F#はNative AOTで発行できますか?

<PublishAot>true</PublishAot>を指定して発行すること自体はできますが、そのまま動くとは限りません。実測ではprintfn "%s is %d"のような書式指定が実行時にNotSupportedExceptionで落ち、これは.NET 10とFSharp.Core 10.1.302でも同じでした。出力をConsole.WriteLineや文字列補間へ置き換えたうえで、発行後のバイナリを必ず実機で確認してください。F#リポジトリのAOT・トリミング追跡Issueは2026年8月現在もオープンです。

C#の最新バージョンはいくつですか?

2026年8月時点の最新はC# 14で、.NET 10(LTS)に同梱されています。C# 13は.NET 9世代です。次のC# 15は.NET 11とともに提供予定で、.NET 11は2026年8月時点でもプレビュー段階です(最新は2026年7月14日公開の11.0.0-preview.6)。

.NET 9のまま使い続けても大丈夫ですか?

2026年11月10日を過ぎるとセキュリティ修正を含むパッチが出なくなるため、本番環境では推奨できません。すでにメンテナンスフェーズで、新機能の追加はありません。.NET 8も同じ日に終了するので、次の移行先は3年サポートの.NET 10(LTS)です。

関連記事

資料請求

RELATED POSTS 関連記事