【C#】これ知ってる?9選【初心者~中級者むけTIPS】
生文字列リテラルはインデントをいい感じに処理してくれる
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#の生文字列リテラルはかなり使い勝手がよいです。
文字列内容に応じたシンタックスハイライトを指定できる
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の場合はjson
やregex
は対応しているようです。とりあえず実用上はあんまり問題にならないかな?)
x is {} y
というパターンマッチで null
をはがせる
// xが`T?`型の時、この記法でnull判定できる
if(x is {} y)
{
//yは`T`
Console.WriteLine(y);
}
x is {}
はパターンマッチでx is not null
と同じ意味になります。
ただし、宣言パターン(マッチしたあとに変数を定義する書き方) では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 []
)はまだできません。
str is [~]
(リストパターン)でできる
文字列の判定はC#のstring
はchar
のコレクション[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"){}
複数条件の判定は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 "割引なし";
}
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% 大口注文割引",
_ => "割引なし"
};
}
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% 大口注文割引",
_ => "割引なし"
};
}
ReadOnlySpan<T>
で再現できる
ローカルstaticフィールドはC#[5] は、関数内でローカルなstatic
フィールドを宣言・定義できません。
void MyFunction()
{
//✖これはできない
static int[] staticArr = [1,2,3]; //Error!
}
ですが!プリミティブ型(int
など)のローカルReadOnlySpan<T>
を関数内で宣言すると、最適化が掛かって内部的には実質ローカルstaticフィールドになるそうです(.NET 8.0+)!
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>
で使いまわすことでパフォーマンスが最適化されます!
ただし、ファイル読込用のバッファとかで毎回確実にサイズがデカいことが分かっている場合は、System.Buffers
以下の色々なクラス(
ArrayPool<T>
)や「Microsoft.IO.RecyclableMemoryStream」などのライブラリを最初から使うのが良いと思います。このイディオムはどういうバッファサイズになっても最適化したいぞ、というときに使えます。
ThrowIf
系メソッド を使うと関数のインライン化が効く
例外は C#では、例外処理を含む場合に関数のインライン化が阻害されます[6]…!
C# コンパイル結果の IL 命令が32バイトを超える場合、インライン化しない
反復処理を含む場合、インライン化しない
例外処理を含む場合、インライン化しない
そこでエラーをthrow new XXXException()
するのではなく、
各例外クラス(XXXException
)に備え付けの ThrowIf
系(「静的 throw ヘルパーメソッド」)
を使うことで関数のインライン化がされやすくなります。
void MyFunction(object? nullableArg)
{
//わざわざこんな書かなくても
//if(nullableArg is null)
// throw new ArgumentNullException(nameof(nullableArg));
//これでOK
ArgumentNullException.ThrowIfNull(nullableArg);
}
主な静的throwヘルパーメソッド
- .NET Standard 1.0
- .NET 7.0+
- .NET 8.0+
- ArgumentException.ThrowIfNullOrEmpty
- ArgumentException.ThrowIfNullOrWhiteSpace
-
ArgumentOutOfRangeException
- ThrowIfEqual
- ThrowIfGreaterThan
- ThrowIfGreaterThanOrEqual
- ThrowIfLessThan
- ThrowIfLessThanOrEqual
- ThrowIfNegative
- ThrowIfNegativeOrZero
- ThrowIfNotEqual
- ThrowIfZero
.NET標準にないものは、.NET Community ToolkitライブラリのThrowHelper
やGuard
が使えます。
-
Syntax highlighterとしてShikiを導入してほしい · Issue #593 · zenn-dev/zenn-community ↩︎
-
char[]
ではなくてIEnumerable<char>
を実装するイミュータブルなシーケンス。今のC#ではReadOnlySpan<char>
で扱うのが一般的。 ↩︎ -
ただし文字列はコレクション式で宣言できません。コレクション式でつかうには、
ReadOnlySpan<char>
に変換する必要があります。 ↩︎ -
ちなみに名前を付けられない
Tuple
クラスもあります。いらない子… ↩︎ -
C# 13.0時点 ↩︎
-
.NET内部ではコードサイズ削減目的の
System.ThrowHelper
があります。ただしこれは通常使えません。 ↩︎
Discussion
ジェネリクスで class 制約が付いている場合にとても重宝します。(分割代入も対応していますので)
なるほど!たしかに!