シンタックスシュガーとは
◆プログラミング言語において、ある構文と等価で、かつ人間にとって読み書きしやすいように簡略化された構文。◆【語源】「取り扱いやすい」を意味するsweetの第一義が「(砂糖のように)甘い」であることから。
C#にはこのシンタックスシュガーがいくつも存在し、通常の構文に関しても他の言語では馴染みないものあるため、上級者が書いたコードが、初心者にとっては理解しずらいということが多々あります。
今回はC#10.0時点の特殊な構文やシンタックスシュガーをいくつか紹介します。
条件式
switch式
C#のswitchには2通りの書き方があります。
1つ目は標準的なswitch文です。
string result = "";
switch(target)
{
case: "A":
result = "targetはAです"
break;
case: "B":
result = "targetはBです"
break;
default:
result = "targetは「その他」です"
break;
上記のシンタックスシュガー版が2つ目になります。
string result = switch(target){
"A" => "targetはAです",
"B" => "targetはBです",
_ => targetは「その他」です",
}
かなり短くなりました。2つ目のswitch分は引数なtargetに対し、戻り値が1つの場合のみ使用可能なswitch文のシンタックスシュガーになります。「_」(アンダーバー)でdefaultを表現できます。
null合体演算子(A ?? B)
はてなを2つ繋げることで「null合体演算子」という特別な構文を意味します。
これは、左辺がnullの時のみ、右辺を評価したい時に利用します。
int A = null;
int B = null;
// Aに0が代入される
A = B ?? 0;
// BにAが代入される
B = A ?? 1;
左辺がnullの場合は、右辺が評価されていますね。
もう一つ「=」(イコール)とつなげた使い方もできます。
int A = null;
// Aに0が代入される
A ??= 0;
// Aは0のまま
A ??= 1;
Aがnullの場合のみ、= 1という式が評価されます。
三項演算子(条件 : A ? B )
:(コロン)と「?」(はてな)を使った構文を「三項演算子」と呼びます。
これはif文を短く書いたもので、条件がtrueならA、falseならBを評価します。
string A = "A";
string B = "B";
// A == Bはfalseのため、Bが評価され"B"が代入される。
string result = A == B : A ? B;
is and or
is構文は2通りの使い方があります。
1つ目はifの条件式をより短く書けるようにしたものです。
DateTime now = DateTime.Now;
if ((now.Day == 5 || now.Day >= 10) && now.Day != 20)
// 上記のif文をis構文を使うとこのように短く書けます
if (now.Day is (5 or >= 10) and not 20)
2つ目は方チェックとキャストを同時に行えるようにしたものです。
object obj = DateTime.Now;
// 型チェック
if (obj is DateTime)
Console.Write("objはDateTime型です");
// 型チェックとtrueの場合にキャストを同時に行う
if (obj is DateTime now)
Console.Write("objはDateTime型で、今日は{now.Day}日です");
A?.Function()
はてな構文は2通りの使い方があります。
1つ目は変数が「null許容型」として宣言するのに使用します。
// Aにはnullが入ることがあることを宣言することで、Aを使用するとき、nullチェックをしていない箇所についてコンパイラがwarningで知らせてくれるようになる。
DateTime? A = null;
// このAがnullの可能性があることをコンパイラが警告してくれる。
var now = A.Now;
// nullチェックを行うと警告されない
var now = A == null ? null : A?.Now;
2つ目は変数がnullかどうかをチェックし、nullではない場合のみ後ろの式を評価し、nullの場合はnullを返却します。
DateTime? A = null;
// dateTimeがnullのため、Nowが評価されず。now変数にはnullが代入されます。
var now = A?.Now;
// 三項演算子の以下の式と同等になります。
var now = A == null ? null : A.Now;
A!.Function()
このビックリマークは条件式の否定とは違います。
これはnull許容型の変数を非null許容型に変換します。
つまりコンパイラの警告を消すために使用します。
DateTime? A = DateTime.Now;
DateTime B = A!;
処理
複数の戻り値
最近のC#では以下のような書き方で1つの関数から2つの戻り値を受け取ることができます。
public (int, string) MultiReturnFunction()
{
return (0, "0");
}
var (number, text) = MultiReturnFunction();
ラムダ式(() => A)
ラムダ式に関しては既にプログラミングでよく登場するので、ここで深く説明はしませんが、C#でも使用可能です。
(過去のC#ではdelegateという概念でラムダ式と同様のことをしていました。)
例えば先ほどの複数の戻り値を持つ関数をラムダ式にすると以下のように書けます
public (int, string) MultiReturnFunction() => (0, "0");
または
public (int, string) MultiReturnFunction => (0, "0");
Action, Func
C#では関数自体を変数に入れることができます。
戻り値のない関数はAction型、戻り値のある関数はFunc型を使います。
Action変数の定義
Action<int> action = (number) => {
Console.Write(number);
}
// 1が出力
action(1);
一行の場合は短く書けます
Action<int> action = (number) => Console.Write(number);
C#は以下のようにも書いても、型推論で動きます。
var action = (int number) => Console.Write(number);
Func変数の定義
Func<int, string> func = (number) => {
return number.ToString();
}
一行の場合は短くも書けます
Func<int, string> func = (number) => number.ToString();
C#は以下のようにも書いても、型推論で動きます。
var func = (int number) => number.ToString();
// "1"が代入
string one = func(1);
応用としてAciton、Funcを受け取るメソッドを定義してラムダで書くと以下のようなコードになります。
// 関数の定義
public void Method(Action<string> consoleLog, Func<int, string> toString)
{
// 1が文字列に変換される
var text = toString(1);
// "1"が出力される
consoleLog(text);
}
// 関数の呼び出し
Method(
// Actionメソッドをラムダで定義して引数に渡す(型推論によりこのラムダはAction<string>型と判断され,textはstring型になる)
(text) => Console.Write(text),
// Funcメソッドをラムダで定義して引数に渡す(Func<int, string>型と判断され,numberはint型になる)
(number) => number.ToString()
);
using
usingには2通りの使用方法があります。
1つ目はnamespaceの読み込みです。
// csファイルの先頭でusingを使って読み込むDLLを宣言します。
// これによりこのcsファイル内でそのDLLの関数やクラスが使用可能になります。
using System.Text.Json;
2つ目はリソースの自動破棄です。
プログラムでは必ず使用後にメモリから破棄しなければいけないリソースが存在します。
よくあるのはStreamですね、ファイルの読み書きなど。
読み込んだファイルの内容がメモリから破棄されずメモリリーク、解放されないファイルロックなどの問題が起こるため、usingを使った自動破棄を利用します。
// usingを使うとこのfileStreamが関数のスコープ外になると、自動でfileStreamのDispose関数が呼ばれ破棄される
void MyMethod () {
using FileStream fileStream = File.OpenRead("myFile");
Console.log("この直後にusingによる自動破棄が行われます");
}
上記のusingは以下のコードを短く書いたものと同様です。
FileStream fileStream = File.OpenRead("myFile");
try {
Console.log("この直後にusingによる自動破棄が行われます");
} finally {
fileStream.Dispose();
}
2つ目のように自動破棄できるクラスはFileStreamクラスのようにIDisposableインターフェースを実装しているクラスに限定されます。
拡張メソッド(public static void A(this string self))
C#にはメソッドをもっと使いやすくするための拡張メソッドという機能が用意されています。
これは既にコンパイラ側で用意されている型、intやstring、Listなどの型に対して、関数を追加できる機能です。
今回はstring型に対して、Betweenという文字列から間の文字を切り出す関数を追加してみます。
// textにはbが入る
string text = "abc".Between("a", "c");
public static class Extension
{
public static string Between(this string str, string from, string to)
{
string tmp = str.Remove(0, str.IndexOf(from) + 1);
return tmp.Remove(tmp.IndexOf(to));
}
}
拡張メソッドはstaticクラスにstaticメソッドで定義する必要があります。
そして、関数の引数に馴染みのないthis宣言がありますが、これが拡張メソッドの宣言になります。thisに続いて指定した型に対する拡張メソッドになります。
既にお察しかもしれませんが、引数strには”abc”が入ってきます。
ジェネリック(class A<T>)
ジェネリクスとも呼ばれたりしますが、C#の身近なクラスだとListクラスはまさにジェネリックを使ったクラスです。
var stringList = new List<string>();
var intList = new List<int>();
ジェネリックを使うと、<>を使って同じListクラスでも、そのクラス内で扱う変数の型を柔軟に変更できます。
こういったクラスのことをジェネリッククラスといいますが、ジェネリッククラスは自分で作成することができます。
// クラス宣言
public class MyList<T>
{
List<T> list = new List<T>();
public void Add(T item)
{
list.Add(item);
}
}
// string型でクラスを使用
var myStringList = new MyList<string>();
myStringList.Add("abc");
// int型でクラスを使用
var myIntList = new MyList<int>();
myIntList.Add(100);
クラスの宣言側にジェネリックの理解のしずらさがありますので、説明すると、
クラス名についているT
Tには使用する側で型を自由に代入することができます。例ではこれがstringやintになります。
そしてクラス内で宣言されている、List
// stringで宣言すると...
var myStringList = new MyList<string>();
// MyListクラスのTが置き換わるイメージ
public class MyList<stirng>
{
List<stirng> list = new List<stirng>();
public void Add(stirng item)
{
list.Add(item);
}
}
次にジェネリックはクラスではなく、関数でも使用できます。
// listから条件に一致した要素を削除して、削除した要素を返すジェネリック関数
public T RemoveItem<T>(List<T> list, Func<T, bool> func)
{
T? item = null;
list.ForEach(x => {
if(func(x))
item = x;
});
list.Remove(item);
return item;
}
// 使用箇所
var stringList = new List<string>(){ "a", "b", "c"};
var intList = new List<int>(){ 0, 1, 2};
// bが消える
var deleledString = RemoveItem<string>(stringList, (item) => item == "b");
// 2が消える(ちなみに<>を省略しても動く。引数にしてるintListの型によって勝手に推測してくれる)
var deleledInt = RemoveItem(intList, (item) => item == 2);
ジェネリックの宣言はお作法的にTが用いられていますが、実際は何を書いてもいいです。(T123でもRでも)
Linq(Select, Where, FirstOrDefault)
Linq(リンク)は、これを知らずしてC#は扱えないと言っても過言ではないくらいに頻出するライブラリになります。ラムダ式に関する知識があれば、大したものではないです。
C#では配列を使いやすくするためのList型が用意されていて、LinqはListをさらに扱いやすくするためのライブラリです。最近のC#では標準でインストールされているので、usingで指定しなくても使えたりします。
配列、List、Linqを使ったList。それぞれで[0,1,2]という配列から、1の要素を取り出す処理を書いてみます。
配列
var array = new int[] { 0, 1, 2 };
int target;
foreach(var item in array)
{
if (item == 1)
{
target = item;
break;
}
}
// 1
Console.Write(target);
list型
var list = new List<int> { 0, 1, 2 };
int target;
list.ForEach(item =>
{
if (item == 1)
{
target = item;
return;
}
});
// 1
Console.Write(target);
list型 + linq
using System.Linq;
var list = new List<int> { 0, 1, 2 };
var target = list.First(item => item == 1);
// 1
Console.Write(target);
Linqを使うと短く書けるのが使わると思いますが、Firstは何をしているのかを知るために、私がFirst関数を想像で書いてみます。Linqもただの拡張メソッドを集めたライブラリなので挙動がわかれば自分で作れちゃいます。
public T First<T>(this List<T> list, Func<T, bool> func)
{
T? target = null;
list.ForEach(item => {
if (func(item))
{
target = item;
return;
}
})
if (target == null)
throw new Exception();
return target;
}
上記のような拡張メソッドが先ほどのFirst関数の正体です。
要素の一致条件はFuncで指定でき、一致したものをreturnする単純な関数でした。
他にもLinqにはSelectやWhereなどといった便利な拡張メソッドがよういされているため、使ってみてください。
まとめ
コードの表現を豊富にした弊害として、上級者の書いたコードが初心者が理解するのが難しいという状況をよく見かけるようになりました。
そういった方の助けになれば幸いです。