💡

【C#】これ知ってる?9選【初心者~中級者むけTIPS】

に公開2

生文字列リテラルはインデントをいい感じに処理してくれる

C#の生文字列リテラル"""~""")はインデントをいい感じに扱ってくれるため、ソースコードが綺麗になります。

インデントをいい感じにしてくれる!
//本来はクラスやMain関数は不要だけどインデントが多いコードの例;
public class Program
{
	public static void Main()
	{
		var name1 = "John Smith";
		var name2 = "Bill Jones";
		var str = $"""
			men: [{name1}, {name2}]
			women:
			- Mary Smith
			- Susan Williams
			""";
			//↑ここで揃えてくれる!
		Console.WriteLine(str);
	}
}
出力結果は終わりの"""の位置でそろえてくれる
men: [John Smith, Bill Jones]
women:
  - Mary Smith
  - Susan Williams
✖こういう書き方は不要
✖出力結果をそろえるためにこうする必要なし!
public class Program
{
	public static void Main()
	{
		var name1 = "John Smith";
		var name2 = "Bill Jones";
//✖出力結果をそろえるためにこうする必要なし!
		var str = $"""
men: [{name1}, {name2}]
women:
  - Mary Smith
  - Susan Williams
""";
//↑ やだー!!!
		Console.WriteLine(str);
	}
}

具体的には、終わりの"""の位置に合わせて行頭のインデントを消してくれます
しかもまちがった位置にした場合コンパイルエラーにしてくれるので、リアルタイムにエラーがわかる優れもの!

終わりの"""が共通インデントより更にインデントされてるとエラー
// Line does not start with the same whitespace
// as the closing line of the raw string literal.
var str = $"""
	men: [{name1}, {name2}]
	women:
		- Mary Smith
		- Susan Williams
		""";	//← うっかりこれはダメ

多言語の類似記法の場合は、そのままではインデント処理(dedent)出来なかったり(例:JavaScript, Python)、出来ても直接変数埋め込みが出来なかったり(例:Java)するので、C#の生文字列リテラルはかなり使い勝手がよいです。

https://learn.microsoft.com/ja-jp/dotnet/csharp/programming-guide/strings/#raw-string-literals

文字列内容に応じたシンタックスハイライトを指定できる

C#というよりもエディタやIDEの対応が必要ですが、
次の書き方(lang=xxx)をすると色分けや補完が聞くようになります。

//lang=json
var json = """
	{
		"name": "John Doe",
		"age": 30,
		"isEmployed": true,
		"skills": ["C#", "JavaScript", "Python"],
		"address": {
			"street": "123 Main St",
			"city": "San Fransokyo",
			"state": "CA"
		},
		"enabled": false
	}
	""";

また、StringSyntax属性(.NET 7.0+)を使うと引数や文字列のプロパティに対しても指定されます。

void SomeRegex(
	[StringSyntax(StringSyntaxAttribute.Regex)]
	string regex
) { }

SomeRegex(@"([a-zA-Z_]+)(?<name>\w+?\d{3})");

ただし対応しているフォーマットはそんなに多くありませんし、エディタ側の対応もいります。
(VSCodeの場合はjsonregexは対応しているようです。とりあえず実用上はあんまり問題にならないかな?)

https://zenn.dev/masakura/articles/2bcb6c7ee2104e

https://learn.microsoft.com/ja-jp/dotnet/api/system.diagnostics.codeanalysis.stringsyntaxattribute?view=net-9.0

x is {} y というパターンマッチで null をはがせる

// xが`T?`型の時、この記法でnull判定できる
if(x is {} y)
{
	//yは`T`
	Console.WriteLine(y);
}

x is {}はパターンマッチでx is not nullと同じ意味になります。

ただし、宣言パターン(マッチしたあとに変数を定義する書き方) ではis notは使えません。

is not は宣言パターンできない
if(x is not null y) //これはエラー
{
	//yを得られない✖️
}

T?型からnullをはがしたTの変数が欲しい時、かつ別の変数としてわかりやすくしたい場合に便利です。

ディクショナリの空初期化はコレクション式でもできる

C#の「コレクション式」は配列やリスト、Span<T>などのコレクションを共通書式で宣言できる、もうこれが無いと生きていけない、そんな便利な記法です。

ですが、ディクショナリ型Dictionary<K,V>の宣言では、C# 13.0現在は使用することができません
(「ディクショナリ式」というものが提案されています)

//コレクションはOK!
List<int> list = [1, 2, 3];

//error!! ディクショナリはまだ未対応
Dictionary<string, int> dic = [
	"a": 100,
	"b": 200
];

そんなディクショナリに対して使えないコレクション式ですが、
例外的に空のディクショナリの初期化だけなら使えます

Dictionary<string, int> dic = [];	//なんとOK!

ただし、空の初期化だけ可能です。通常の宣言や、空ディクショナリの判定(dic is [])はまだできません。

https://zenn.dev/inuinu/articles/84c6d5ca85c41f#ディクショナリの空初期化

文字列の判定はstr is [~](リストパターン)でできる

C#のstringcharのコレクション[2]と見なすことができる[3]ので、パターンマッチングのリストパターンで空文字列の判定ができます。

//空文字列の判定
if(str is []) Console.WriteLine($"{str}は空です!");

同時にnull判定も一緒にできます。

//参考:string.IsNullOrEmpty(str)相当
if(str is null or []) Console.WriteLine($"{str}はnullまたは空です!");

これまでこのケースでよく使われてきた、string.IsNullOrEmpty()は、str.IsNullOrEmpty()と書きたくなりますが、できません。パターンマッチの方がstring以外の他と一緒の書き方ができるので、個人的には好みです。

空文字列やnullの判定以外もできます。

//イニシャルが'a'の文字列かどうかの判定
if(str is ['a', ..]){}

//'l'で終わる文字列かどうかの判定
if(str is [.., 'l']){}

//丸かっこで囲まれた文字列かどうかの判定
if(str is ['(', .., ')']){}

//数字で始まる文字列かどうかの判定(コードポイントの比較ができる)
if(str is [>= '0' and <= '9', ..]){}

//カタカナで始まる文字列かどうかの判定
if(str is [>= 'ァ' and <= 'ン', ..]){}

複雑な判定は正規表現を使った方がいいと思いますが、コードポイントの比較も組み合わせられるので簡単な判定はリストパターンで結構イケます。

また、逆にReadOnlySpan<char>は文字列と直接判定ができます

//ReadOnlySpan<char> が ['a','p','p','l','e']かどうかの判定
if(span is "apple"){}

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

複数条件の判定はswitch式+パターンマッチが便利

if文の入れ子や条件文を&&とかで組み合わせるよりも、switch式+パターンマッチの方がわかりやすくシンプルにかけます。

従来の方法(入れ子になったif文や && 条件)
// 従来の方法(入れ子になったif文や && 条件)
string GetDiscountTraditional(Customer customer, Order order)
{
	if (customer.IsVip)
	{
		if (order.Amount > 1000)
		{
			return "25% VIP大口割引";
		}
		else
		{
			return "15% VIP割引";
		}
	}
	else if (customer.LoyaltyYears > 2)
	{
		if (order.Amount > 500)
		{
			return "10% 長期顧客割引";
		}
		else
		{
			return "5% 長期顧客割引";
		}
	}
	else if (order.Amount > 800)
	{
		return "3% 大口注文割引";
	}

	return "割引なし";
}
タプル+switch式を使ったケース
string GetDiscount(Customer customer, Order order)
{
	return (
		customer.IsVip,
		customer.LoyaltyYears > 2,
		order.Amount > 1000,
		order.Amount > 500,
		order.Amount > 800
	) switch
	{
		(true,    _,    true,    _,    _) => "25% VIP大口割引",
		(true,    _,    false,   _,    _) => "15% VIP割引",
		(false, true,    _,    true,    _) => "10% 長期顧客割引",
		(false, true,    _,    false,   _) => "5% 長期顧客割引",
		(false, false,   _,     _,   true) => "3% 大口注文割引",
		_ => "割引なし"
	};
}
リスト/配列+switch式を使ったケース
string GetDiscountWithListPattern(Customer customer, Order order)
{
	bool[] conditions =
	[
		customer.IsVip,
		customer.LoyaltyYears > 2,
		order.Amount > 1000,
		order.Amount > 500,
		order.Amount > 800
	];

	return conditions switch
	{
		[true, _, true, _, _]     => "25% VIP大口割引",
		[true, _, false, _, _]    => "15% VIP割引",
		[false, true, _, true, _] => "10% 長期顧客割引",
		[false, true, _, _, _]    => "5% 長期顧客割引",
		[false, false, _, _, true] => "3% 大口注文割引",
		_ => "割引なし"
	};
}

条件を表にしたほうがわかりやすいケースはこう書いたほうがいいです。
タプル(ValueTuple)をつかうケース(位置パターン)と、コレクションを使うケース(リストパターン)は好みですが、C#のタプル(ValueTuple[4]は各要素に名前を付けられるので順番をミスりたくない時は位置パターンの方がいいかもしれません。

string GetDiscountWithNamedTuple(Customer customer, Order order)
{
	return (
		IsVip: customer.IsVip,
		IsLoyalCustomer: customer.LoyaltyYears > 2,
		IsLargeOrder: order.Amount > 1000,
		IsMediumOrder: order.Amount > 500,
		IsModerateOrder: order.Amount > 800
	) switch
	{
		(IsVip: true, IsLargeOrder: true, _, _, _)     => "25% VIP大口割引",
		(IsVip: true, _, _, _, _)                      => "15% VIP割引",
		(IsVip: false, IsLoyalCustomer: true, _, IsMediumOrder: true, _)  => "10% 長期顧客割引",
		(IsVip: false, IsLoyalCustomer: true, _, _, _) => "5% 長期顧客割引",
		(IsVip: false, IsLoyalCustomer: false, _, _, IsModerateOrder: true) => "3% 大口注文割引",
		_ => "割引なし"
	};
}

ローカルstaticフィールドはReadOnlySpan<T>で再現できる

C#[5] は、関数内でローカルなstaticフィールドを宣言・定義できません。

The modifier 'static' is not valid for this item
void MyFunction()
{
	//✖これはできない
	static int[] staticArr = [1,2,3];	//Error!
}

ですが!プリミティブ型(intなど)のローカルReadOnlySpan<T>を関数内で宣言すると、最適化が掛かって内部的には実質ローカルstaticフィールドになるそうです(.NET 8.0+)!

実質ローカルstaticフィールド
void MyFunction()
{
	//✔実質ローカルstaticフィールド
	ReadOnlySpan<int> staticSpan = [1,2,3];
}

バッファには stackalloc + ArrayPool<T>.Shared.Rent()

byte[]のバッファが必要な処理(ファイル・Streamの読み書き、エンコード・デコード処理、暗号化・ハッシュ化、画像・音声バイナリ処理、etc...)をするとき、バッファの確保に毎回new byte[]するのはパフォーマンス上問題があるとされています。

そういう時には次のイディオムが使えます。

バッファのイディオム
byte[]? rented = null;
var isSmallBuffer = buffLength <= 256;
Span<byte> buffer = isSmallBuffer
	? stackalloc byte[buffLength]
	: (rented = ArrayPool<byte>.Shared.Rent(buffLength));

try
{
	//ここでバッファをつかう処理をする
	DoSomething(buffer);
}
finally
{
	if (rented is not null)	//ここはisSmallBufferの判定でもOK
		ArrayPool<byte>.Shared.Return(rented);
}

C#はstackとheapを使い分けられる言語なので、byte列のサイズが小さい場合はstack、大きい場合はheapとし、そのうえでサイズが大きい場合には毎回確保ではなくArrayPool<T>で使いまわすことでパフォーマンスが最適化されます!

https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1?view=net-9.0

ただし、ファイル読込用のバッファとかで毎回確実にサイズがデカいことが分かっている場合は、System.Buffers以下の色々なクラス(
ArrayPool<T>)や「Microsoft.IO.RecyclableMemoryStream」などのライブラリを最初から使うのが良いと思います。このイディオムはどういうバッファサイズになっても最適化したいぞ、というときに使えます。

https://learn.microsoft.com/ja-jp/dotnet/standard/io/buffers

https://synamon.hatenablog.com/entry/2023/04/24/000000

https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream

例外は ThrowIf系メソッド を使うと関数のインライン化が効く

C#では、例外処理を含む場合に関数のインライン化が阻害されます[6]…!

C# コンパイル結果の IL 命令が32バイトを超える場合、インライン化しない
反復処理を含む場合、インライン化しない
例外処理を含む場合、インライン化しない

https://ufcpp.net/study/csharp/structured/miscinlining/#dotnet-inlining

そこでエラーをthrow new XXXException()するのではなく、
各例外クラス(XXXException)に備え付けの ThrowIf 系(「静的 throw ヘルパーメソッド」)
を使う
ことで関数のインライン化がされやすくなります。

例:ArgumentNullException.ThrowIfNull
void MyFunction(object? nullableArg)
{
	//わざわざこんな書かなくても
	//if(nullableArg is null)
	//	throw new ArgumentNullException(nameof(nullableArg));

	//これでOK
	ArgumentNullException.ThrowIfNull(nullableArg);
}
主な静的throwヘルパーメソッド

https://learn.microsoft.com/ja-jp/dotnet/standard/exceptions/best-practices-for-exceptions#use-exception-builder-methods

.NET標準にないものは、.NET Community ToolkitライブラリのThrowHelperGuardが使えます。

https://learn.microsoft.com/en-us/dotnet/communitytoolkit/diagnostics/throwhelper

https://learn.microsoft.com/en-us/dotnet/communitytoolkit/diagnostics/guard

脚注
  1. Syntax highlighterとしてShikiを導入してほしい · Issue #593 · zenn-dev/zenn-community ↩︎

  2. char[]ではなくてIEnumerable<char>を実装するイミュータブルなシーケンス。今のC#ではReadOnlySpan<char>で扱うのが一般的。 ↩︎

  3. ただし文字列はコレクション式で宣言できません。コレクション式でつかうには、ReadOnlySpan<char>に変換する必要があります。 ↩︎

  4. ちなみに名前を付けられないTupleクラスもあります。いらない子… ↩︎

  5. C# 13.0時点 ↩︎

  6. [雑記] インライン化 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C ↩︎

  7. .NET内部ではコードサイズ削減目的のSystem.ThrowHelperがあります。ただしこれは通常使えません。 ↩︎

Discussion

junerjuner

ちなみに名前を付けられないTupleクラスもあります。いらない子…

ジェネリクスで class 制約が付いている場合にとても重宝します。(分割代入も対応していますので)