そのため、
「では、.NET 8や9から10へ上げよう」
と思うのは自然です。
しかし、その前に必ず見ておきたいページがあります。
Microsoftが公開しているBreaking Changes(破壊的変更)の一覧です。
名前は少し怖いですが、意味を理解するとかなり役立ちます。
Microsoftは.NET 10のBreaking Changesを、大きく次のような種類に分類しています。
既存のバイナリを新しいランタイムで動かしたときに問題が出る可能性がある変更です。
新しいSDKで再コンパイルすると、ソースコード側の修正が必要になる可能性がある変更です。
コンパイルは通りますが、実行時の振る舞いが以前と変わる可能性がある変更です。
特に最後は少し注意が必要です。
エラーになれば気付きやすいですが、
「動いているけれど、前と動作が違う」
という状態は、テストしないと気付けないからです。
.NET 10の互換性一覧を見ると、かなり多くの領域に変更があります。
例えばSDKでは、
dotnet new slnでSLNX形式が既定になる変更があります。
dotnet restore周辺ではセキュリティ監査の強化も行われています。
NuGetやMSBuildにも複数の動作変更があります。
Windows Formsにも変更があります。
例えば.NET 10では、
Form.OnClosingForm.OnClosedなど一部APIがobsolete扱いとなり、利用すると警告が出るようになっています。
「今までコンパイルできていたから大丈夫」
とは限りません。
.NETのバージョンアップというと、
<TargetFramework>net10.0</TargetFramework>
へ書き換えるところに目が行きやすいです。
でも、本当に確認したいのはその周囲です。
アプリケーションは、それ単体では生きていません。
小さな町のように、たくさんの道路で外の世界とつながっています。
.NETだけ新しくしても、その道路が対応していなければ問題が起きます。
LTSだから移行する。
ではなく、
LTSだからこそ、これから数年間安心して使える状態へきちんと移行する。
この考え方の方がよいと思います。
新しい.NETを追いかけることは、新機能を楽しむことだけではありません。
昔のコードを、次の時代へ安全に渡していくことでもあるのです ☕️


長く.NETを触っている人には、とても馴染みのある名前だと思います。
登場から長い年月が経っているので、
「WinFormsって、もうあまり進化していないのでは?」
と思う人もいるかもしれません。
しかし.NET 10では、WinFormsにもかなり面白い改善が入っています。
Microsoftが挙げている主な変更には、
などがあります。
.NET 9ではWinFormsのダークモード機能はまだ実験的な扱いでした。
.NET 10ではApplication.SetColorModeが実験扱いから外れ、ダークモード機能が本格的に統合されています。
例えば、
Application.SetColorMode(SystemColorMode.System);
とすることで、Windows側の色設定を利用できます。
SystemColorModeには、
ClassicSystemDarkが定義されています。
長く使われてきたWinFormsが、現代のWindowsの見た目へ少しずつ追いついているのは面白いところですね。
.NET 10では、
Form.ShowAsyncForm.ShowDialogAsyncTaskDialog.ShowDialogAsyncといった非同期フォームAPIも、実験扱いではなくなりました。
例えば、
DialogResult result = await dialog.ShowDialogAsync();
という形でダイアログを扱えます。
昔のデスクトップアプリでは、
「UIスレッドを止めないようにする」
という問題と戦う場面が多くありました。
async/awaitが当たり前になった現在のC#へ、WinForms側も少しずつ歩み寄っているように見えます。
地味ですが重要なのがクリップボードです。
.NET 10ではWinFormsとWPFでクリップボード関連コードを共有できるよう再設計が進められています。
背景には、セキュリティ上問題となってきたBinaryFormatterから離れていく流れもあります。
一部の古いクリップボードAPIは非推奨となり、JSONを利用した方法などへ移行する方向になっています。
こういう変更は、
「新しいボタンが増えました」
よりずっと地味です。
しかし、長く使われるフレームワークでは、こうした古い仕組みを少しずつ安全な仕組みに置き換えていく作業がとても重要です。
.NET 10ではForm.ScreenCaptureModeも追加されました。
設定によって、
といった制御が可能になります。
ユーザー名やID、パスワードなど機密情報を表示する画面では、使い道を考えられるAPIです。
.NET 10のWinFormsを見ると、
古い技術を捨てるのではなく、現代の.NETへ少しずつ接続し直している
ように感じます。
長年使われてきた道具を、職人が毎年少しずつ研ぎ直しているみたいで、私はこういう進化がけっこう好きです ☕️
「まずプロジェクトを作って……」
というのが長い間当たり前でした。
ところが.NET 10では、1個のC#ファイルから直接アプリを実行するFile-based appsが大きく強化されています。
例えば、
Console.WriteLine("Hello, .NET 10!");
これだけを書いたhello.csを用意して、
dotnet run hello.cs
という形で扱えます。
小さなツールを書くのに、必ずしも.csprojから始めなくてもよくなったのです。
PythonやJavaScriptを使う人には、
「1ファイルを書いて、そのまま実行する」
というスタイルは珍しくありません。
C#は従来、どちらかというとアプリケーション開発向けの構造をきっちり作る言語でした。
.NET 10のFile-based appsは、そのC#にもう一つの顔を与えています。
例えば、
であれば、大きなプロジェクト構造を準備しなくてもよくなります。
Microsoft自身も、File-based appsをスクリプト、ユーティリティ、小規模アプリケーション向けの軽量な選択肢として説明しています。
面白いのはここからです。
.NET 10では、
dotnet publish app.cs
のように、ファイルベースのアプリをpublishすることもできます。
つまり、
「ちょっと1ファイルで書いてみた」
プログラムを、そのまま配布可能なアプリケーションへ育てていく道が用意されています。
これはC#の使い方を少し変える機能だと思います。
.NET 10 SDKではFile-based apps以外にも変化があります。
例えば、
dotnet test周辺の改善などが行われています。
そして、少し驚く変更もあります。
.NET 10では、
dotnet new sln
を実行したとき、従来の.slnではなくSLNX形式が既定で生成されます。
こういう変更は、新しい機能というより、
「あれ? 昔と違う」
と感じるポイントでしょう。
アップデートノートを追う意味は、派手な新機能を知るためだけではありません。
いつの間にか変わった“当たり前”を知ること。
これもかなり大切です。
「パフォーマンスが改善しました」
でも、それだけでは少しモヤッとします。
何が速くなったのでしょうか?
今回は、その裏側にいるJITコンパイラを見ていきます。
私たちが書いたC#のコードは、そのままCPUが実行するわけではありません。
ざっくり説明すると、
C#
↓
中間言語(IL)
↓
機械語
↓
CPUが実行
という流れを通ります。
このILを、実行するコンピュータに適した機械語へ変換する重要な役割を持っているのがJIT(Just-In-Time)コンパイラです。
舞台が始まる直前に、その劇場に合わせて台本を書き直してくれる舞台監督のような存在ですね。
.NET 10では、このJITに多くの改善が入っています。
Microsoftは、インライン化、メソッドのdevirtualization、構造体引数のコード生成などが改善されたとしています。
例えばインターフェース経由でメソッドを呼ぶと、実行時には、
「実際にはどのメソッドを呼べばいいのでしょう?」
という判断が必要になる場合があります。
抽象化はコードを美しく整理するためにとても便利ですが、昔のコンパイラではその柔軟さが少し性能コストになることもありました。
.NETのJITは近年、その「抽象化の向こう側」を見抜く能力をどんどん高めています。
.NET 10でもこの流れが続き、インターフェースやイテレーター、delegateなどを使った普通のC#コードを、より効率よく実行できるよう最適化が進められました。
これはかなり嬉しい変化です。
高速化のためだけに読みにくいコードを書くのではなく、
読みやすいC#を書きながら、ランタイム側に頑張ってもらえる
方向へ進んでいるからです。
.NET 10ではスタック割り当てについても改善が行われています。
メモリを大きな街に例えるなら、
ヒープは広い倉庫街。
スタックは、作業机のすぐ横にある小さな棚です。
一時的なデータをうまくスタック側で扱えれば、メモリ管理の負担を小さくできる場合があります。
もちろん、すべてのアプリが劇的に速くなるわけではありません。
どの程度効果が出るかは処理内容によって異なります。
しかし、こうした小さな最適化がランタイム全体に積み重なることで、
「コードを変えていないのに新しい.NETで速くなる」
という現象が生まれます。
.NET 10ではNativeAOTも強化されています。
通常のJIT方式とは違い、NativeAOTではアプリを事前にネイティブコードへコンパイルできます。
そのため、
といった場面では重要な選択肢になります。
.NET 10のパフォーマンス改善は、単純な「ベンチマークの数字競争」ではありません。
私たちが普通に書いたC#を、ランタイムが裏側でもっと上手に料理してくれるようになった。
私はそこが一番面白いところだと思います。



公開日:2026年9月7日
.NETには毎年、新しいメジャーバージョンが登場します。
「また新しい.NETが出たの?」
そう感じる人もいるかもしれません。
しかし、.NET 10は少し立ち位置が違います。
.NET 10は2025年11月11日に正式リリースされたLTS(Long Term Support)版です。
Microsoftのサポートポリシーでは、.NET 10は2028年11月までサポートされる予定になっています。
つまり、登場したばかりの新技術というより、これから数年間の.NET開発で中心になっていく世代と考えた方が分かりやすいです。
.NETには大きく分けて、
という2種類があります。
現在、.NET 10はLTSです。
業務システムのように数年間運用するアプリケーションでは、このサポート期間がとても重要になります。
新しい機能があるからアップデートするだけではありません。
「セキュリティ更新を受け続けられる期間」も、バージョン選択の大切な基準になります。
.NET 10は、単にランタイムだけが新しくなったわけではありません。
Microsoftの公式資料を見ると、
など、.NETプラットフォーム全体に変更が入っています。
ランタイムではJITコンパイラの最適化やスタック割り当ての改善、NativeAOTの強化などが行われました。
ライブラリではJSON、暗号、ネットワーク、コレクションなど幅広いAPIが追加されています。
つまり、
「.NET 10の新機能」という一言では収まらないくらい、広い範囲が更新されています。
ここは少し慎重に考えたいところです。
新規開発であれば、.NET 10を候補にする価値は高いです。
一方、既存システムを.NET 8から移行する場合は、
「LTSだから、とりあえず上げよう」
では危険です。
利用しているNuGetパッケージ、外部ライブラリ、Breaking Changesなどを確認してから判断する必要があります。
新しいことと、安全に移行できることは別なのです。
.NET 10は2025年11月に登場したLTS版です。
これから.NETを学ぶ人にも、新規システムを作る人にも無視できない存在になっています。
このシリーズでは、
何が変わったのか。
なぜ変わったのか。
私たちのコードにどんな影響があるのか。
を少しずつ見ていきます。
.NETの中では、今日も昔のコードと新しいコードが静かにバトンタッチしているのですね ☕️



ChatGPTにComboBoxのコードを書かせると、それらしいコードはすぐ作れます。
でも確認したいのは、
まで含めて本当に動くかです。
今回はAIが生成したコードを.NET 10で実行します。
C# Windows FormsでComboBoxに
東京、大阪、名古屋を表示してください。
各都市にはIDを持たせ、
SelectedValueでIDを取得できるようにしてください。
public class City
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
comboBox1.DataSource = new List<City>
{
new() { Id = 1, Name = "東京" },
new() { Id = 2, Name = "大阪" },
new() { Id = 3, Name = "名古屋" }
};
comboBox1.DisplayMember = nameof(City.Name);
comboBox1.ValueMember = nameof(City.Id);
値の取得は、
int cityId = (int)comboBox1.SelectedValue;
です。
int cityId = (int)comboBox1.SelectedValue;
これは「必ず値が存在してintである」という前提です。
そこで、
comboBox1.SelectedIndex = -1;
として実行してみます。
if (comboBox1.SelectedValue is not int cityId)
{
MessageBox.Show("都市を選択してください。");
return;
}
MessageBox.Show($"選択されたID: {cityId}");
これなら未選択でも安全です。
必ずしもそうではありません。
「必ず何か選択されている」という前提なら、最初のコードでも動きます。
問題は、その前提がコードから見えにくいことです。
AIコードを見るときは、
どんな前提でこのコードは動くのか?
まで確認した方がよさそうです。
AIはコードを書くところまでなら、とても速いです。
だからHIROs.NETでは、
「AIがこう答えた」で終わらず、「実際に動かしたらこうなった」
まで確かめていこうと思います。
AIコードは、
という流れで確認します。
AIの回答は完成品ではなく、検証を始めるためのコードとして使うのがよさそうです。
]]>3つの違いはシンプルです。
| プロパティ | 取得するもの |
|---|---|
SelectedIndex |
選択位置 |
SelectedItem |
選択されたオブジェクト |
SelectedValue |
ValueMemberの値 |
「大阪(Id=2)」を選択した場合、
SelectedIndex = 1
SelectedItem = Cityオブジェクト
SelectedValue = 2
となります。
public class City
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
comboBox1.DataSource = new List<City>
{
new() { Id = 1, Name = "東京" },
new() { Id = 2, Name = "大阪" },
new() { Id = 3, Name = "名古屋" }
};
comboBox1.DisplayMember = nameof(City.Name);
comboBox1.ValueMember = nameof(City.Id);
「大阪」を選択して確認します。
Debug.WriteLine(comboBox1.SelectedIndex);
Debug.WriteLine(comboBox1.SelectedItem);
Debug.WriteLine(comboBox1.SelectedValue);
if (comboBox1.SelectedIndex == -1)
{
// 未選択
}
選択されているか、何番目かを確認するときに使います。
if (comboBox1.SelectedItem is City city)
{
Debug.WriteLine(city.Name);
}
選択されたデータ全体を使いたい場合に便利です。
if (comboBox1.SelectedValue is int cityId)
{
Save(cityId);
}
IDやコードだけ必要なときに使います。
APIを1つずつ覚えるより、3つを同時に表示すると違いが一気に分かります。
私は、
Index = どこ?
Item = 誰?
Value = 番号は?
と考えると覚えやすいと思っています。
SelectedIndexSelectedItemSelectedValue用途で使い分ければ、それほど難しくありません。
SelectedValueがnullなら、まず確認したいのはこれです。
comboBox1.SelectedIndex
-1なら、現在何も選択されていません。
また、データバインド中にイベントが発生し、一時的に値を取得できないケースにも注意が必要です。
public class City
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
comboBox1.DataSource = new List<City>
{
new() { Id = 1, Name = "東京" },
new() { Id = 2, Name = "大阪" }
};
comboBox1.DisplayMember = nameof(City.Name);
comboBox1.ValueMember = nameof(City.Id);
comboBox1.SelectedIndex = -1;
var value = comboBox1.SelectedValue;
SelectedIndex == -1なら、選択項目そのものがありません。
private void comboBox1_SelectedValueChanged(
object sender, EventArgs e)
{
Debug.WriteLine(
comboBox1.SelectedValue ?? "null");
}
データを設定している途中でもイベントが発生することがあります。
if (comboBox1.SelectedValue is int cityId)
{
Debug.WriteLine(cityId);
}
これならnullや想定外の型でも例外になりません。
SelectedValueがnullになると、ついそのプロパティだけを疑いたくなります。
でも実際には、
SelectedIndex
SelectedItem
DataSource
ValueMember
まで一緒に見る方が、原因へ早くたどり着けます。
SelectedValueがnullなら、
SelectedIndex == -1ではないかを確認してみてください。
]]>Windows Formsの`ComboBox`に`List`をバインドし、`DisplayMember`や`ValueMember`を指定する場合、指定する名前は**データソース側のpublicプロパティ**として公開されている必要があります。
HIROs.NETでは2013年に、次のようなpublicフィールドを使ったコードでハマった記録があります。
public class Employee
{
public int Empno;
public string Name;
}
これをプロパティへ変更すると解決しました。
public class Employee
{
public int Empno { get; set; }
public string Name { get; set; } = "";
}
Microsoft Learnの現在のWindows Formsドキュメントでも、`IList`をバインドする場合、`DisplayMember`にはリスト内の型の**publicプロパティ**を指定すると説明されています。
では、2013年のコードは.NET 10でも同じようにハマるのでしょうか。
今回は、当時の記事をほぼそのまま現在の環境へ持ってきて確認します。
—
次のように設定しているのに、ComboBoxへ期待した名前が表示されません。
comboBox1.DataSource = employees; comboBox1.ValueMember = "Empno"; comboBox1.DisplayMember = "Name";
ぱっと見ると間違っていないように見えます。
`DataSource`も設定している。
`DisplayMember`も指定している。
`ValueMember`も指定している。
それなのに期待どおりにならない。
2013年当時、原因は`Employee`クラスのメンバーをフィールドとして宣言していたことでした。
まずは、当時ハマった形を再現します。
フォームに`ComboBox`を1つ配置し、名前を`comboBox1`とします。
public class Employee
{
public int Empno;
public string Name;
public Employee(int empno, string name)
{
Empno = empno;
Name = name;
}
}
フォームの読み込み時にデータを設定します。
private void Form1_Load(object sender, EventArgs e)
{
var employees = new List
{
new Employee(1, "山田 太郎"),
new Employee(2, "佐藤 花子")
};
comboBox1.DataSource = employees;
comboBox1.ValueMember = "Empno";
comboBox1.DisplayMember = "Name";
}
```
2013年の記事では、この形では期待した表示になりませんでした。
ポイントは`DisplayMember`と`ValueMember`が、単に「同じ名前の何か」を探しているわけではないことです。
Windows Formsのデータバインディングでは、バインド対象の型が持つ**プロパティ**を通して値を取得します。
つまり、
public string Name;
はpublicではありますが、これはフィールドです。
一方、
public string Name { get; set; }
はプロパティです。
見た目は似ていますが、データバインディングから見ると別物です。
ふだんコードを書いていると、
employee.Name
と同じようにアクセスできるので、ここはかなり気づきにくいポイントです。
`Employee`のフィールドをプロパティへ変更します。
public class Employee
{
public int Empno { get; set; }
public string Name { get; set; } = "";
public Employee(int empno, string name)
{
Empno = empno;
Name = name;
}
}
ComboBox側のコードは同じです。
private void Form1_Load(object sender, EventArgs e)
```csharp
private void Form1_Load(object sender, EventArgs e)
{
var employees = new List
{
new Employee(1, "山田 太郎"),
new Employee(2, "佐藤 花子")
};
comboBox1.DataSource = employees;
comboBox1.ValueMember = nameof(Employee.Empno);
comboBox1.DisplayMember = nameof(Employee.Name);
}
2013年の記事では次のように書いていました。
comboBox1.ValueMember = "Empno"; comboBox1.DisplayMember = "Name";
2026年に書くなら、私は`nameof`を使う形をおすすめします。
comboBox1.ValueMember = nameof(Employee.Empno); comboBox1.DisplayMember = nameof(Employee.Name);
これなら、プロパティ名をリファクタリングしたときに文字列だけが古いまま残る事故を減らせます。
たとえば`Name`を`FullName`へ変更した場合、
nameof(Employee.Name)
はコンパイル時に問題へ気づけます。
しかし、
"Name"
は単なる文字列なので、コンパイラは間違いを教えてくれません。
ComboBoxに`List`を渡したとき、Windows Formsはリスト内の要素を調べます。
そのうえで、
DisplayMember = "Name"
なら`Name`プロパティ、
ValueMember = "Empno"
なら`Empno`プロパティ、
という対応を作ります。
イメージとしては、ComboBoxがデータを受け取るときに、
> 「表示用の引き出しはどれ?」
> 「内部値の引き出しはどれ?」
と探しているようなものです。
publicフィールドは見た目こそ似ていますが、このバインディングの「引き出し一覧」に同じ形では並びません。
だからプロパティへ変更すると、ComboBoxが正しく値を取り出せるようになります。
ここが今回の中心です。
public string Name;
と
public string Name { get; set; }
は別物です。
データバインディングでは、まずプロパティを疑ってみると切り分けが早くなります。
次のようなミスもありがちです。
comboBox1.DisplayMember = "Nmae";
`nameof`を使えば、このタイプのミスをかなり減らせます。
`List`なのか、`DataTable`なのか、`BindingSource`なのかで見え方は変わります。
今回の記事は`List`を使った検証です。
今回ここを再検証する理由でもあります。
2013年の記事は古いですが、扱っているのはWindows Formsのデータバインディングという長く使われている仕組みです。
古い記事を削除するより、
> 「今の.NETでも本当に同じ?」
と確かめ直す方が、資料としてずっと面白くなります。
—
Microsoft Learnでは、Windows FormsのComboBox / ListBoxを`IList`へバインドするとき、`DisplayMember`にはリスト内の型のpublicプロパティを指定する方法が説明されています。
– Microsoft Learn: How to bind a Windows Forms ComboBox or ListBox control to data
– Microsoft Learn: ListControl.DataSource
– Microsoft Learn: ListControl.ValueMember
—
2013年の記事を見返していて、「これ、今でも同じなのかな?」と思ったのが今回の再検証のきっかけです。
APIの説明だけならAIに聞けばすぐ答えが返ってきます。
でも、13年前に実際にハマったコードを今の.NETへ持ってきて、同じところで転ぶのか確かめるのは少し違います。
古いコードは、捨てるものではなく比較用のタイムカプセルなのかもしれません。
– `DisplayMember`と`ValueMember`ではデータソース側のプロパティを指定する
– publicフィールドとpublicプロパティは別物
– 2026年に書くなら`nameof`を使うと変更に強くなる
– 古い記事でも、現在環境で再実行すると新しい価値が生まれる
—
– HIROs.NET Blog「[ハマリ] ComboBoxのDisplayMemberとValueMember」(2013年)
– HIROs.NET Blog「DataSource・DisplayMember・ValueMemberプロパティでデータバインドを行う」(2025年
C#で配列を初期化するときは、以前から次のように書いてきました。
int[] numbers = { 1, 2, 3 };
Listなら、
List names = new()
{
"Alice",
"Bob"
};
C# 12では、Collection Expressionsによって[]を使った統一的な書き方ができます。
配列なら、
int[] numbers = [1, 2, 3];
Listも、
List names = ["Alice", "Bob"];
と書けます。
配列とListで見た目が揃うため、コードがすっきりします。
さらに便利なのが..です。
int[] first = [1, 2, 3]; int[] numbers = [0, ..first, 4];
numbersの内容は、
0 1 2 3 4
となります。
JavaScriptなどのspread構文を知っている人には、かなり馴染みやすい書き方です。
Collection Expressionは、代入先の型に応じてコレクションが決まります。
int[] array = [1, 2, 3]; List list = [1, 2, 3];
同じ[1, 2, 3]でも受け側が違います。
そのため、型が分かる形で使うのがポイントです。
C# 12以降では、
[1, 2, 3]
というシンプルな記法でコレクションを作れます。
さらに、
[..items]
で既存の要素を展開できます。
今回のTips:C# 12なら配列やListの初期化にCollection Expressionsを使える。
]]>