そろそろ、.NET Conf の日程も発表され、.NET 11 の GA も近づいてきました。そうなると、何が起こるでしょう?
そう、C# 15 の GA の季節です。
というわけで、C# 15 を復習してみます。
■ .NET Blog
今回 C# 15 を自分で動かして理解し、気になるところは動作確認をしてみました。
色々独自にやってみましたが、機能の基本ということであれば、.NET Blog の次の記事がまとまっているので、ぜひ参照してください。
■ 今回のコード
今回、試してみたコードは GitHub で公開しています。
良かったら、Zip で持っていったり、クローンしたりして皆さんも動かしてみてください。
■ Union types
ユニオン型
「これらの型のうちのどれかが入る変数」というものが作れます。
型定義
Union 型の定義の例です。
ku 型は、Chiyoda 型の値または Taito 型の値が入る変数として使えます。
union Ku(Chiyoda, Taito) { }
record Chiyoda { }
record Taito { }
コード例
Ku 型の変数には、Chiyoda 型または Taito 型を代入できます。
ちなみに、値を持たない Ku 型の変数も可能です (コード例の ku01 のように)。
Chiyoda chiyoda01 = new(); Taito taito01 = new(); Ku ku01 = new(); Ku ku02 = chiyoda01; Ku ku03 = taito01;
Chiyoda 型でも Taito 型でもない型は代入できず、構文エラーです。
// エラー Ku ku04 = new System.Drawing.Point(0,0); // これはできない 「 型 'System.Drawing.Point' を 'Ku' に暗黙的に変換できません 」
使用例
使うときはパターンマッチングで使うのが基本になりそうです。
特に、取り得る型が確定しているので、静的にパターンの不足をチェックしてくれるのがうれしいです。
次の例では Taito 警告がでます。
_ = ku02 switch // ここで警告 「 この switch 式では入力型の可能な値がすべて扱われるわけではありません (すべてが網羅されているわけではありません)。たとえば、パターン 'Taito' がカバーされていません。 」 { Chiyoda c => "Chiyoda", };
網羅されると、警告がなくなります。
_ = ku02 switch // これは取り得る型が網羅されているので、警告が出ない { Chiyoda c => "Chiyoda", Taito c => "Taito", };
キャストの仕方
Union 型は代入した値の型にキャスト できません。
Ku 型に Chiyoda 型を代入できますが、キャストで Ku 型を Chiyoda 型にすることはできません。
// エラー Chiyoda chiyoda02 = (Chiyoda)ku02; // これはできない 「 型 'Ku' を 'Chiyoda' に変換できません 」
代入した値は Ku 型の Value プロパティに居るので、Value プロパティから取り出すことができます。取り出しものはキャストできます。
Chiyoda chiyoda02 = (Chiyoda)ku02.Value;
代入と Value プロパティ
Ku 型に Chiyoda 型を代入するソースコードを書けるが、実体としては Ku 型の Value プロパティに Chiyoda 型が入っています。
Ku 型の Type は Ku で Ku 型の Value プロパティの中身の型が Chiyoda であることが確認できます。
Console.WriteLine(ku01.GetType().FullName); // Ku Console.WriteLine(ku02.GetType().FullName); // Ku Console.WriteLine(ku01.Value?.GetType().FullName); // Console.WriteLine(ku02.Value.GetType().FullName); // Chiyoda Console.WriteLine(chiyoda01.GetType().FullName); // Chiyoda
パターンマッチングの場合のみ、Ku 型のみでなく、Value プロパティの中身とマッチするかも見てくれています。
Console.WriteLine(ku01.Value == null); // True Console.WriteLine(ku01 is null); // True Console.WriteLine(ku02.Value == chiyoda01); // True Console.WriteLine(ku02 is Ku); // True Console.WriteLine(ku02 is Chiyoda); // True
Union 型は ValueType かつ Ku 型は Chiyoda 型ではないので次のコードは静的にエラーです。
// これらはエラー Console.WriteLine(ku01 == null); // これはできない 「 演算子 '==' を 'Ku' と '<null>' 型のオペランドに適用することはできません 」 Console.WriteLine(ku02 == chiyoda01); // これはできない 「 演算子 '==' を 'Ku' と 'Chiyoda' 型のオペランドに適用することはできません 」
Value は不変
Value プロパティは get のみのため set はできません。
// エラー ku01.Value = chiyoda01; // これはできない 「 プロパティまたはインデクサー 'Ku.Value' は読み取り専用であるため、割り当てることはできません 」
Value から値を取り出す
Value プロパティから値を取り出して処理するコードは、せっかくの Union 型の利点をつぶしてしまいます。
`Ku 型の変数へのパターンマッチングでは、あり得ない型 (次の例ではあり得ない Attribute 型) とマッチしようとしてエラーにしてくれるが、
Value から取り出してしまうと、エラーにしてはくれません。
// エラーにしてくれる。うれしい _ =ku02 switch { Attribute c => "" }; // False // これはできない 「 種類 'Ku' の式は、種類 'Attribute' のパターンで処理することができません。 」
二つ目のこの例はエラーにしてくれない。うれしくない。
Console.WriteLine(ku02.Value is Attribute); // False
■ Closed hierarchies
クローズド継承 同一アセンブリ内でのみ継承できる型をつくれます。
型定義のコード
クラスライブラリで次のように定義したとします。
closed キーワードが今回のキモです。
Ku 型を継承して、Chiyoda 型、Taito 型を定義できています。
//-- // クラスライブラリのアセンブリ //-- namespace closed_hierarchies_2; // closed クラスである Ku は同一アセンブリ内では継承できる public closed class Ku { } public class Chiyoda : Ku { } public class Taito : Ku { }
また、アセンブリ外へ公開しない internal クラスが継承することもできます。
// internal クラスでの継承が可能 internal class Bunkyo : Ku { }
別アセンブリでの参照
先のクラスライブラリを別のプロジェクトで参照してみます。
次のように、Ku 型が見える使える状態です。
//-- // クラスライブラリを参照しているアセンブリ //-- using closed_hierarchies_2; // クラスライブラリで定義されている closed 型である Ku は見える Console.WriteLine(typeof(Ku).FullName); // closed_hierarchies_2.Ku
しかし見えるのに継承ができません。
// エラー class Chuo : Ku { } // これはできない 「 'Chuo': 別のアセンブリの閉じた型 'Ku' を基本型として使用することはできません。 」
網羅もされない
closed クラスが定義されたアセンブリ外で継承できないので、値の型のパターンも限定されますことになり、Union 型のような網羅チェックも期待してしまうかもしれません。
残念ながらできません。
Union 型の switch 式のように型の網羅は静的に警告してもらえません、
Ku ku = new Taito(); _ = ku switch // Taito のパターンが書かれていないが、警告はしてもらえない { Chiyoda c => "Chiyoda", };
そもそも Ku 型は Bunkyo 型の値である可能性があるが、このアセンブリでは Bunkyo 型を扱えない
// エラー _ = ku switch // これはできない 「 'Bunkyo' はアクセスできない保護レベルになっています 」 { Bunkyo b => "Bunkyo", };
■ Collection expression arguments
コンストラクタ引数付きコレクション式
コレクション式でコレクションを作成する場合に、以前はコレクション型のコンストラクタ引数を指定できなかったが、with() でコンストラクタ引数を指定できるようになりました。
次の例ではキャパシティを 256 を指定して初期化しています。
List<string> list01 = ["one", "twe"]; List<string> list02 = [with(capacity: 256), "one", "twe"]; Console.WriteLine(list01.Capacity); // 2 Console.WriteLine(list02.Capacity); // 256
独自型で確認してみる
コンストラクタ引数のある型を作って確認してみましょう。
次のように引数のあるコンストラクタと、引数のないコンストラクタを持つクラスを作ってみました。
// コンストラクタ引数のあるコレクション型の定義 class MyCollection<T> : List<T> { public MyCollection(string message) => Console.WriteLine($"MyCollection created with message: {message}"); public MyCollection() => Console.WriteLine($"MyCollection created without message"); public new void Add(T item) => Console.WriteLine($"added:{item}"); }
一つ目は引数のあるコンストラスタが使用される例です。
MyCollection<string> values = [with("saitama!"), "one", "two", "three"]; // コンストラクタ引数あり。引数が与えられている // MyCollection created with message: saitama! // added: one // added: two // added: three
二つ目は引数のないコンストラスタが使用される例です。
MyCollection<string> values2 = ["one", "two", "three"]; // コンストラクタ引数なし。引数なしコンストラクタが使われている // MyCollection created without message // added: one // added:two // added:three
■ Extension indexers
新形式の拡張メソッドでのインデクサ拡張
新形式の拡張メソッドでのインデクサが定義できるようになりました。
コード例
次のように拡張インデクサを定義してみます。
//-- // 拡張メソッド定義 //-- public static class ListExtensions { extension(Object o) { // 拡張メソッドでのインデクサ定義 public string this[int index] => index == 0 ? "zero" : index == 1 ? "one" : ""; } }
使用してみます。本来インデクサをもたない Object 型でインデクサが使えました。
Object o = new(); ; Console.WriteLine($"at 0: {o[0]}"); // at 0: zero Console.WriteLine($"at 1: {o[1]}"); // at 1: one
■ Labeled break and continue
ラベルへの break / continue
ラベルを使って多段のループを狙った階層まで一気に break や continue できます。
飼いならされた goto です。
コード例 (Break)
次の例は、break でいくつかのループを階層を飛び越えて break しています。
count が 1 の場合は break しないので 3 層目のループがもう一度回っています。
count が 2 の場合は、ラベル「la02」のループがブレイクするので 1 層目のループに飛んでいます。
count が 3 の場合は、ラベル「la01」のループがブレイクするので 1 層目のループから抜けループが終了します。
var count = 0; la01: for (; ; ) { Console.WriteLine("la01"); la02: for (; ; ) { Console.WriteLine("l02"); { for (; ; ) { Console.WriteLine("la03"); Console.WriteLine($"count: {++count}"); if (count % 2 == 0) break la02; if (count % 3 == 0) break la01; } } } }
// 実行結果: // l01 // l02 // l03 // count: 1 // l03 ← count: 1 の場合は break しないので 3 層目のループがもう一度回る // count: 2 // l01 ← count: 2 の場合は、ラベル「la02」のループがブレイクするので 1 層目のループに飛ぶ // l02 // l03 // count: 3 // ← count: 3 の場合は、ラベル「la01」のループがブレイクするので 1 層目のループから抜けループが終了する
コード例 (continue)
次の例は、continue でもいくつかのループを階層を飛び越えて continue しています。
count が 1 の場合は break も continue もしないので 3 層目のループがもう一度回っています。
count が 2 の場合は 2 層目 continue なので 2 層目のループがもう一度回っています。
count が 3 の場合は、ラベル「lv01」のループがブレイクするので 1 層目のループから抜けループが終了します。
count = 0; lb01: for (; ; ) { Console.WriteLine("lb01"); lb02: for (; ; ) { Console.WriteLine("lb02"); { for (; ; ) { Console.WriteLine("lb03"); Console.WriteLine($"count: {++count}"); if (count % 2 == 0) continue lb02; if (count % 3 == 0) break lb01; } } } }
// 実行結果: // lb01 // lb02 // lb03 // count: 1 // lb03 ← count: 1 の場合は break も continue もしないので 3 層目のループがもう一度回る // count: 2 // lb02 ← count: 2 の場合は 2 層目 continue なので 2 層目のループがもう一度回る // lb03 // count: 3 // ← count: 3 の場合は、ラベル「lv01」のループがブレイクするので 1 層目のループから抜けループが終了する
飼いならされた
goto のように自由奔放でなく、飼いならされているので、階層の中のでの break と continue しかできません。(うれしい)
次の例はエラーになります。(うれしい)
// エラー // 無関係のラベルへは飛べない for (; ; ) continue lb02; // これはできない 「 ラベル 'lb02' を持つ外側ループが存在しないため、continue を実行できません 」















