😐

個人的にあまり使えていないC#の機能

に公開

はじめに

今回は、C#の機能で個人的に便利だと思うけど、使用できていない機能やどうやって使用するかよく忘れる機能をまとめました。必ずしもこの機能が良い悪いとかではないですが、ライブラリなどのソースを読んでいるときふと出てきた際に思考停止しないようしたいので、インプットも兼ねてまとめました。

属性の省略可能プロパティ

アプリ開発やライブラリ開発などで、カスタム属性を作成することはよくあることだと思います。
そしてカスタム属性に何かしら別の値を追加する際に、必須ではなくオプションとしてプロパティを指定したい場合には、省略可能プロパティを使います。

クラスへカスタム属性を追加
// [Test("example")] コンストラクタ引数とは異なります
[Test(ParamName = "example")]
public class MyClass
{
    // ....
}

省略可能プロパティについては、プロパティにセッターを設定するもしくはinit のみのセッターを追加する必要があります。

カスタム属性
[AttributeUsage(AttributeTargets.Class)]
public sealed class TestAttribute : Attribute
{
    public string? ParamName { get; set; } // init; でも可能

    public TestAttribute(){}
}

代表的なものだと色々ありますが、AttributeUsage属性のAllowMultipleプロパティやInheritedプロパティになります。

代表的な属性
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class TestAttribute : Attribute
{
    // ....
}

私自身はコンストラクタ引数のいわゆる必須プロパティをよく使いがちのため、あまり使えていない印象です。

https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/attributes

インデックス演算子と範囲演算子

System.IndexSystem.Rangeを用いたシーケンスへのアクセス方法になります。この二つについてはあまり目にしたことがないかもしれませんが、実は意外に使っている部分はあると思います。例えば、配列の最後の値を取得する際に以下のようにindexer経由で実装すると Visual Studioからこの演算子を提案されます。

Visual Studioから提案されるインデックス演算子
int[] array = [1, 2, 3, 4, 5];

- var lastValue = array[array.Length - 1];
+ var lastValue = array[^1];

^がインデックス演算子で、上記の^1は、後ろから一番目の要素という解釈になります。
^は内部的にはSystem.Indexが使われているので、上記はSystem.Indexで表すことが可能です。

// 両方同じ
var lastValue = array[^1];
var lastValue = array[new Index(1, true)];

範囲演算子については、よくある例はSpan<T>から一部分を取り出す際に使用しています。
例えば、Span<T>.Sliceで一部分を取り出すソースを実装するとVisual Studioからこの演算子を提案されます。

Visual Studioから提案される範囲演算子
int[] array = [1, 2, 3, 4, 5];

- var slice = array.AsSpan().Slice(2);
+ var slice = array.AsSpan()[2..];

..が範囲演算子で、上記の2..は、二番目から最後までの範囲という解釈になります。
..は内部的にはSystem.Rangeであり、上記をSystem.Rangeで表すことも可能です。

// 両方同じ
var slice = array.AsSpan()[2..];
var slice = array.AsSpan()[Range.StartAt(Index.FromStart(2))];

^..をコンパイラが暗黙的に変換してくれるため、通常はSystem.IndexSystem.Rangeをそのまま使用するというケースはあまりないかなと思います。

https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/ranges-indexes
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#index-from-end-operator-
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/ranges#1842-implicit-index-support

もちろんそのまま使っているケースもあり、著名なライブラリだと @neueccさんが作成したZLinq(ゼロアロケーションLinqライブラリ)ではコアロジックとして使われていたりします。

https://github.com/Cysharp/ZLinq/blob/main/src/ZLinq/ValueEnumerable.cs#L59

パターンマッチング

C#では、ifif-elseswitchでサポートされています。パターンとしてはC# ドキュメントによると以下に大別されます。

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/patterns

  1. 宣言パターン: 式の実行時の型を確認し、一致が成功した場合は、宣言された変数に式の結果を割り当てます。
  2. 型パターン: 式の実行時の型を確認します。
  3. 定数パターン: 式の結果が指定した定数と等しいことをテストします。
  4. リレーショナル パターン: 式の結果を指定した定数と比較します。
  5. 論理パターン: 式がパターンの論理的な組み合わせと一致することをテストします。
  6. プロパティ パターン: 式のプロパティまたはフィールドが入れ子になったパターンと一致することをテストします。
  7. 位置指定パターン: 式の結果を分解し、結果の値が入れ子になったパターンと一致するかどうかをテストします。
  8. var パターン: 任意の式と一致し、その結果を宣言された変数に割り当てます。
  9. 破棄パターン: 任意の式と一致します。
  10. リスト パターン: 要素のシーケンスが、対応する入れ子になったパターンと一致することをテストします。 C# 11 で導入されました。

これらのパターンの中でも、特に個人的に使えていない論理パターン、プロパティパターン、リストパターンを主に記載します。

switch式

そもそもswitchステートメントではなく、switch式自体が私自身あまり書き慣れていません(ステートメントと混同しますよね)。例えば、以下のソースだとswitch式にリレーショナルパターン定数パターンを組み合わせたものになります。このパターンはさすがに多くのライブラリや.NETランタイム内のコードでも大量に使われているので、身構えないようにしないといけないです。ただ、今はGithub Copilotのコード補間が優秀なので、多少間違えていても正しい構文に提案してくれますので、とてもありがたい限りです。

var str = number switch
{
    0 => "Zero",
    1 => "One",
    2 => "Two",
    >= 3 => "Three or more",
    _ => "Unknown"
};

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression

論理パターン

先ほどのswitch式に論理パターンを組み合わせると以下のようになります。
andorを利用し、範囲指定や複数条件指定などしたいときに利用します。ただあまりにも、条件が複雑であったりすると個人的には読みにくいので、ちょっとした範囲指定や日付指定などでは活躍してくれそうです。

var str = number switch
{
    >= 0 and <= 3 => "0から3",
    4 or 5 => "4か5",
    _ => "それ以外"
};

プロパティパターン

ここから本格的に使えていないパターンになります。名前の通りプロパティやフィールドを用いる方法になります。以下のソースだと、DateTimeOffsetの指定した各プロパティの値がすべて一致しているとき、つまりDateTimeOffsetが2025/10/27から2025/10/31であればtrueを返します。

var now = DateTimeOffset.Now;

if (now is { Year: 2025, Month: 10, Day: >= 27 and <= 31 })
{
    // ...
}

これも書き慣れていないので、ライブラリで出てきたときには少し焦ります。それでも明示的になっているため、好みが別れるかもしれませんが慣れていればわかりやすいとは思います。

https://github.com/dotnet/dotnet/blob/c22dcd0c7a78d095a94d20e59ec0271b9924c82c/src/runtime/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs#L355-L356

https://github.com/dotnet/runtime/blob/9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs#L1189

リストパターン

C# 11から利用できる比較的新しい記法になります。リストということからコレクション式に近い書き方、そしてインデックス演算子と範囲演算子を用いた記法になります。

このパターンは、この記事を書く上で公式のドキュメントを読みましたが、それだけではなかなか理解が難しいかったです。そのためC#erなら何度もお世話になっていると思われる「++C++; // 未確認飛行 C について」の「【C# 11 候補】リスト パターン【VS 17.1 p2 で追加予定】」にわかりやすくどういう判定しているか書かれているので、詳細が気になる方はこちらを参照いただければと思います。

https://ufcpp.net/blog/2021/12/list-pattern/

簡単なサンプルとしては以下のようなソースになります。

int[] array = [1, 2, 3, 4, 5];

// 以下すべてTrueになります。
Console.WriteLine(array is [1, 2, 3, 4, 5]);    // パターンその1
Console.WriteLine(array is [1, .. , 5]);        // パターンその2
Console.WriteLine(array is [.. , 3, 4, 5]);     // パターンその3
Console.WriteLine(array is [1, 2, _, _, _]);    // パターンその4

リストパターンで実装したソースについては、以下のように解釈されて実行されます。
※これらは、sharplab.ioやLINQPadなどを用いると簡単に確認できます。

その1(「長さが5個」かつ「一番目が1、二番目が2、三番目が3、四番目が4、五番目が5」)

// このように実装する。
Console.WriteLine(array is [1, 2, 3, 4, 5]);

// 実際にはこういう処理が行われている。
Console.WriteLine (array != null 
                && array.Length == 5 
                && array [0] == 1 
                && array [1] == 2 
                && array [2] == 3 
                && array [3] == 4 
                && array [4] == 5);

その2(「長さが2個以上」かつ「最初に来る値が1かつ最後に来る値が5」(それ以外は長さ、値は何でも可))

// このように実装する。
Console.WriteLine(array is [1, .. , 5]);  

// 実際にはこういう処理が行われている。
int value;
if (array != null)
{
     int num = array.Length;
     if (num >= 2 && array [0] == 1)
     {
          value = ((array [num - 1] == 5) ? 1 : 0);
          goto IL_002f;
     }
}
value = 0;
goto IL_002f;
IL_002f:
Console.WriteLine ((byte)value != 0);

その3(「長さが3個以上」かつ「最後から順に543である」)

// このように実装する。
Console.WriteLine(array is [.., 3, 4, 5]);

// 実際にはこういう処理が行われている。
int value;
if (array != null)
{
     int num = array.Length;
     if (num >= 3 && array [num - 3] == 3 && array [num - 2] == 4)
     {
          value = ((array [num - 1] == 5) ? 1 : 0);
          goto IL_0039;
     }
}
value = 0;
goto IL_0039;
IL_0039:
Console.WriteLine ((byte)value != 0);

その4(「長さが5個」かつ「最初から順に12である」)

// このように実装する。
Console.WriteLine(array is [1, 2, _, _, _]);

// 実際にはこういう処理が行われている。
Console.WriteLine (array != null
                && array.Length == 5
                && array [0] == 1
                && array [1] == 2);

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/list-patterns

まとめ

今回まとめた多くものは、一部は見たことがある、知らず知らず内に使ったことがあるような基本的な機能になります。ただ、私自身ちゃんと調べていくとあまり把握していなかった仕様などもあり、特にインデックス演算子と範囲演算子の内容は、Microsoft Learnのチュートリアルにもある内容なのに、知らないことも多くあったため、まだまだ勉強が足らないことも実感しています。

Discussion