C# 7 早わかりリファレンス(前編)
C# 基礎文法 最速再入門【7.0対応】
「あれ、どう書くんだっけ?」を素早く解決するための、C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。前編では、C#の歴史/開発ツール/プログラムの実行と制御/型と変数/演算子/ステートメントを説明する。
本連載(全3回)は、執筆時点で最新のC# 7.0の主要な文法をサンプルコードと一緒に一覧できるようにするために書いたものである。文法の解説などは特に注意するべきことなどに絞っているため、初学者の学習用というよりはある程度C#を知っている方の参考用として書いている。
今回の記事を書くに当たって、C#言語仕様などを確認していたが、筆者自身初めて知った文法ルールもあった。この内容は全てを覚えるというよりも、C#コードを書いていて気になったときに参考にしてもらえると幸いである。
ちなみに、古いC# 6.0対応版が読みたい場合は、こちらを参照してほしい。また、C# 7.0の新機能をまだ知らない場合は、事前に「C# 7.0で知っておくべき10の新機能」を読んで言語機能を理解しておいてほしい(※本連載は簡潔さを優先して説明をはしょっているため)。
本連載のサンプルコードに関しては、初出時に公開したLINQPad形式に加えて、クロスプラットフォームで動作するように.NET Core向けのコンソールプロジェクト形式のものも用意した。両形式を含むコードを下記のリンク先で配布しているので、まずは2からZipファイルをダウンロードしてほしい。
- 1GitHub: tanaka-takayoshi/csharp-grammar-sample-for-linqpd
- 2Zipファイルによるサンプルコードの一括ダウンロード
- 3LINQPadにSamplesとしてインポートできるzipファイルのURL(※右クリックして[リンクのアドレスをコピー])
LINQPadを利用する場合は、無償で利用できるStandard Editionで動作可能である。ぜひLINQPad 5の最新版をインストールした上でLINQPad用サンプルコード(MITライセンス)を活用していただきたい。
また、コンソールプロジェクト形式のものは、コードの主な部分はLINQPad用と同じだが、Mainメソッドから全てのサンプルコードが実行できるようにクラスやメソッドの構造に手を入れている。Visual StudioもしくはVisual Studio Codeなど各種開発ツールで実行してほしい。
【参考】LINQPad用のサンプルコードの基本的な使い方と注意点
LINQPadをインストールすれば、.linqファイルをダブルクリックするだけで、そのファイルがLINQPadで開かれる。その状態で、[Language]欄で「C# Program」を選択して、F5キー(もしくは[Execute]ボタン)を押すだけで実行できる。
またLINQPadには、ウィンドウ左下にある[Samples]タブの[Download/import more samples]のメニューから直接サンプルコードを取り込む機能がある。上記の3のURLを指定すると、この機能を使ってサンプルコードをまとめてLINQPadに取り込むことが可能である(お薦め)。
参照するアセンブリやusing
でインポートする名前空間は、F4キーを押して(もしくはメニューバーの[Query]-[References and Properties]を選択して)[Query Properties]ダイアログを表示し、そのダイアログ上の[Additional References](アセンブリ参照の追加)タブや[Additional Namespace Imports](名前空間インポートの追加)タブで指定できる。
LINQPadでは、あらゆるオブジェクトの内容を確認するためのDump()
メソッドが提供されており、LINQPad用サンプルコード内では用いているところが多数ある(※本稿に掲載しているコード例ではDump()
メソッドは省略しており、LINQPad用サンプルコードと少しコードが異なる部分があるので注意してほしい)。
文法の説明に入る前に、C#の最新動向に詳しくない方に向けて、C# 7.0までの成り立ちや最新の開発ツール事情を簡単に紹介しておこう。
C#の歴史
C#の正式版は、2002年にVisual Studio .NET 2002と一緒に、C# 1.0としてリリースされた。この.NETというのは.NET Frameworkを意味しており、実際に.NET FrameworkもC# 1.0と同時にリリースされている。
.NET Frameworkは、基本クラスライブラリ(BCL)と、実行環境となる共通言語ランタイム(CLR)で構成される。CLRは、共通言語基盤(CLI)という共通的な仕様に基づいて実装されているので、C#以外の言語でもこの仕様にのっとった中間言語(IL)コードを出力すれば、CLR上で実行可能である。実際、VB.NET(Visual Basice .NET)やF#がCLR上で実行可能であり、しかもコードレベルで異なる言語で書かれたたクラスを参照するといったことも可能になっている。
C# 2.0以降は、C#と.NET FrameworkとVisual Studioは歩みをそろえてバージョンアップしていく。C# 2.0ではジェネリクスの機能が追加されたが、これは.NET Framework側に機能追加された結果であり、C#ではランタイムレベルでジェネリクスの機能が利用できることになった。C# 3.0では拡張メソッドとラムダ式が追加され、LINQが利用できるようになった。C# 4.0ではdynamic
型が、C# 5.0ではasync
/await
キーワードによる非同期メソッドが、といったように着実に機能が追加されていった。
C#の言語機能が追加されていく中、時代はGitHubを中心としたオープンソースなプログラムを利用する流れが強まっていった。そんな中、マイクロソフト自身もライブラリをオープンソースにする流れに変わっていったが、「C#ソースコードからILコードにコンパイルするコンパイラーをサービスとして利用できるようにし、それ自体をオープンソースにしよう」というプロジェクトが生まれた。“Roslyn”というコードネームで呼ばれたプロジェクトである。既存のコンパイラーから、オープンソースのRoslynコンパイラーに変えつつ、同時にC#の言語機能も追加してリリースされたのがC# 6.0である。
C# 6.0以降は言語機能の追加に関する議論自体がオープンな場所で進められている。追加される機能内容が最初からオープンな場所で議論された最初のバージョンアップがC# 7.0であり、この流れは次期8.0以降にも続いている。
一方、C# 6.0~7.0の間で、C#が動作する環境にも大きな変更があった。当初、C#および.NET Frameworkの動作がサポートされるOSはWindowsのみだった。Monoプロジェクトと呼ばれるLinuxへの移植を行うプロジェクトはあったが(ちなみに、Monoプロジェクトの流れは、今のXamarinに続いている)、マイクロソフトが公式でサポートするのはWindowsのみであった。ところが、.NET Coreとよばれる新しいフレームワークが発表された。この.NET Coreは、ランタイムと、クラスライブラリなどのフレームワーク一式がGitHub上で公開されており、Windowsに加えて、LinuxやmacOS(Mac)の各プラットフォームをサポートするように開発が進められている。加えて、Windows向けの商用サポートはマイクロソフトが、Linuxのディストリビューションの一つであるRed Hat Enterprise Linux向けの商用サポートはレッドハット(Red Hat)が提供している。
現在のC#は、言語機能としてたゆまぬ進化が続いている中で、Windows以外のプラットフォームでも商用としても十分に利用に耐えられる、クロスプラットフォームで利用可能な言語として歩みを始めた状態である。
開発ツール
C# 1.0のリリース当初より同時にリリースされてきたという歴史の長さから分かるように、Visual StudioがC#サポート機能を最も豊富に搭載した開発ツールといえるだろう。最新のVisual Studio 2017では、従来の.NET Frameworkに加え、クロスプラットフォーム対応の.NET Coreもサポートしている。基本的には有償の商用ツールではあるが、無償版のCommunityエディションなども用意されている(無償版には、この他にExpressエディションもあるが、執筆時点ではVisual Studio 2017向けのものがまだ提供されていない。どちらの無償版を利用できるかなど利用用途やライセンスについては、公式サイトの説明を参照してほしい)。また、Visual Studioの拡張機能を開発することもでき、無償・有償を問わず多数の便利な拡張が公開されていることもメリットの一つだ。
ただし、Visual StudioはWindowsのみのサポートであるため、.NET Core向けの開発をLinuxやmacOS上で行う場合は利用できない。そのためLinuxやmacOSでの.NET Core開発環境としては、GUIツールの代わりに、.NET Core SDKで提供されるCUIツールを利用するのが一般的だ。こちらのリンク先の手順(英語)に従って.NET Core SDKをダウンロードすると、.NET Core CLI(コマンドライン・インターフェース)がインストールされ、ターミナルなどでのコマンドライン実行のみで開発が開始できるようになる。それに加えて、Windows/Linux/macOSで動作するコードエディターのVisual Studio Codeを使うと便利だ。Visual Studio Codeに対しても(Visual Studioと同様に)拡張機能を開発することができ、C#開発用の機能も拡張機能として提供されている。どうしてもVisual StudioのようなGUIツール(いわゆるIDE)を使いたい場合は、サードパーティから提供されているJetBrainsのRiderやEclipseプロジェクトのEclipse Cheを利用することもできる。また、macOSのみサポートされるが、マイクロソフトが提供するVisual Studio for Mac(執筆時点ではプレビュー版で、開発が進行中)も利用可能である。
プログラムの実行と制御
この章のサンプルコードは、LINQPad上だと標準出力などの挙動が異なるため、LINQPadのサンプルには載せていない。サンプルを試すには、C#のコンソールアプリとしてプロジェクトを作成されたい。
Mainメソッドと標準出力・標準エラー出力
using System;
class Program
{
static void Main(string[] args)
{
// 標準出力。Console.Out.WriteLineも同じ
Console.WriteLine("こんにちは、C#!");
// 標準エラー出力
Console.Error.WriteLine("こんにちは、C#!");
// Visual Studioからデバッグ実行するときなどは、コンソールウインドウがすぐに閉じるのを防ぐために入力を待つとよい
Console.ReadLine();
}
}
|
C#ではエントリポイントと呼ばれるMain
メソッドを実行環境が呼び出すことにより、アプリケーションの起動が開始される。エントリポイントとなるMain
メソッドは、static
メソッドであり、返り値はvoid
もしくはint
型、仮パラメーターはなしもしくはstring[]
型でなければならない。
またプロセスからの出力ストリームとして、標準出力、標準エラー出力が用意されている。
コマンドライン引数
using System;
class Program
{
static void Main(string[] args)
{
foreach (var arg in args)
{
Console.WriteLine(arg);
}
Console.ReadLine();
}
}
|
アプリケーションの実行時に指定されるコマンドライン引数を利用するためには、Main
メソッドにstring[]
型の仮パラメーターを指定する。コマンドライン引数が指定されていない場合は空の配列となり、nullにはならない。
また、Visual Studioでデバッグ実行する場合は、プロジェクトのプロパティを開き、デバッグの開始オプションのコマンドライン引数を指定する(図1)。
本連載のサンプルは、C# 7.0に対応しているVisual Studio 2017を使ってビルドして検証しているので注意してほしい。
Visual Studio Codeの場合は、デフォルトではプロジェクトフォルダー直下に作成される.vscodeフォルダー内にあるlaunch.jsonファイルを編集することで、コマンドライン引数を編集できる。configurations
キー以下に複数のデバッグ設定をオブジェクトの配列として定義し、コマンドライン引数を追加する場合は、該当するデバッグ設定オブジェクトのargs
キーの値に配列として指定する。
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceRoot}\\CheatSheetConsoleApp\\bin\\Debug\\netcoreapp1.1\\CheatSheetConsoleApp.dll",
"args": ["Hello", "C#"],
"cwd": "${workspaceRoot}\\CheatSheetConsoleApp",
"console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
}
//,
//{
// ……このようにして別のデバッグ設定のオブジェクトを複数設定可能……
//}
]
}
|
プログラムのファイルパスの一部がnetcoreapp1.1
となっているように、本連載のサンプルは.NET Core 1.1でビルドして検証している。1.0では本連載で示す一部のサンプルコードがビルドできないので、注意してほしい。
標準入力の読み取り
using System;
class Program
{
static void Main(string[] args)
{
var input = Console.ReadLine();
Console.WriteLine(input);
Console.ReadLine();
}
}
|
標準入力から入力された文字列を読み取ることができる。
プログラムの終了とステータスコード
using System;
class Program
{
static void Main(string[] args)
{
Environment.Exit(1);
}
}
|
class Program
{
static int Main(string[] args)
{
return 1;
}
}
|
実行しているプロセスを終了した際のステータスコードを指定できる。指定せずに終了した場合のステータスコードは0であり、これは正常に完了したことを意味する。明示的に指定する場合はEnvironment.Exit
メソッドの引数として指定するか、Main
メソッドの返り値として指定できる。なお、Environment.Exit
メソッドを実行すると、その時点で実行中のプロセスは終了する。
型と変数
組み込み型とリテラル表記
class Program
{
static void Main(string[] args)
{
// bool型
var flg1 = true;
var flg2 = false;
// int、uint、long、ulongのうち表現できる最初の型。この場合はint型
var int1 = 1;
// uまたはUサフィックスを付けた場合はuintもしくはulongのうち表現できる最初の型。この場合はuint型
var int2 = 1U;
// lまたはLサフィックスを付けた場合はlongもしくはulongのうち表現できる最初の型。この場合はuint型
var int3 = 1L;
// ulもしくはULサフィックスを付けた場合はulong型
var int4 = 1UL;
// 単精度浮動小数点
var number1 = 1f;
// 指数表記
var number2 = 1.2e-3f;
// 倍精度浮動小数点
var number3 = 1d;
// 指数表記
var number4 = 1.2e-3d;
// 高精度小数(decimal型)
var number5 = 0.1m;
// char型
var char1 = '1';
var char2 = '\'';
var char3 = '\xFFFF';
// C# 7.0で導入された桁区切り
var int5 = 1_000_000;
// _は先頭と最後以外であれば任意の場所に任意の数だけ使える
var int6 = 1_000__00000_0UL;
// 小数部にも使える
var number6 = 0.00_00__01f;
var numbder7 = 100__00_0.0__1m;
// 16進数表記は従来から利用可能
var int7 = 0xdeadbeaf;
// C# 7.0より2進表記が利用可能に
var int8 = 0b10011100;
// C# 7.0より16進表記や2進表記と桁区切りも併せて利用可能
var int9 = 0xdead_beaf;
var int10 = 0b1001_1000_1111;
}
}
|
C#では値型と参照型という2種類の型をサポートしている。そのうち、値型のいくつかはリテラル(=ソースコード上に直接記述した数値や文字列)で値を表現できる。
整数型の場合、指定した数値が表現できる型(int
型であれば–2147483648から2147483647まで、long
型であれば–9223372036854775808から9223372036854775807まで、など)によってどの型のリテラル表現になるかが決まる。
浮動小数点表記の場合、指定したリテラルが表現可能な精度より高い精度を指定している場合は四捨五入される。decimal
型は28~29桁の有効精度を持っており、10の累乗でスケールされた整数として表現している。そのため、float
型やdouble
型では無限小数となるような0.1といった値を正確に表現できる。
char型は整数型と同じ分類になるが、Unicode文字セットに属する文字を表現するのに利用する。\x
でエスケープすることで、Unicodeのコード値でリテラル表記できる。
また、C# 7.0より桁区切りとしてアンダースコア_
が利用可能となった。リテラル表記の数値の先頭と末尾以外であれば、任意の場所に任意の数だけ指定することができる。
C# 6.0以前より先頭に0x
を付けることでリテラル数値を16進表記できていたが、C# 7.0より0b
を付けることで2進表記もできるようになった。16進表記や2進表記は桁区切りと併せて利用することもできる。
文字列リテラルと逐語的リテラル文字列
class Program
{
static void Main(string[] args)
{
var str01 = "Hello World"; // 文字列リテラル
var str02 = @"Hello World"; // 逐語的リテラル文字列
var str03 = "Hello \t World"; // Hello World
var str04 = @"Hello \t World"; // Hello \t World
var str05 = "\"Hello\" World"; // "Hello" World
var str06 = @"""Hello"" World"; // "Hello" World
var str07 = "C:\\tmp\\log.txt"; // C:\tmp\log.txt
var str08 = @"C:\tmp\log.txt"; // C:\tmp\log.txt
var str09 = "Hello\r\nWorld";
var str10 = @"Hello
World";
}
}
|
C#では文字列リテラルを二重引用符"
でくくって表現する。二重引用符そのものやタブや改行といった一部の制御文字は円記号\
でエスケープして表現する。
一方、二重引用符の前に@
を付加した逐語的リテラル文字列では改行や空白などの二重引用符以外の全ての文字を逐語的に(=一語一語、忠実に)解釈する。
文字列補間
using System;
class Program
{
static void Main(string[] args)
{
var name = "テスト";
var s1 = $@"名前は
{name}";
var s2 = $"Now: {DateTime.Now:f}";
}
}
|
C# 6.0で文字列補間という機能が導入された。今までstring.Format
メソッドで記述していたフォーマット文字列をリテラルに近い記法で記述できるようになった。逐語的文字列と組み合わせたり、書式を指定したりすることも可能だ。
配列
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
// 要素数を指定して配列を初期化
var array1 = new int[2];
// 要素を指定して配列を初期化
var array2 = new[] { 1, 2, 3 };
// プロパティ、フィールドなど左辺に型を記述する場合、new[]は不要
double[] array3 = { 1d, 2.3d };
// 配列はIList<T>を実装している
IList<string> list3 = new[] { "A", "B" };
// 2次元配列
var array5 = new[,] { { 1, 2 }, { 3, 4 } };
// 配列の配列
var array6 = new[] { new[] { 1, 3, 5 }, new[] { 2, 4, 6, 8 } };
}
}
|
配列は、配列の長さを指定する方法と、要素自体を指定する方法で生成できる。要素数を指定した場合、各要素は配列要素に指定した型のデフォルト値で初期化される。また、配列T[]
はIList<T>
インターフェースを実装している。
C#では多次元配列と、配列の配列(=ジャグ配列とも呼ばれる)は別のものだ。配列の配列の各要素は次元や長さが異なっていても構わない。
演算子
算術演算子(*、/、%、+、-)
class Program
{
static void Main(string[] args)
{
// 加算・減算
var x0 = 1 + 4; // 5
// int + double は double + double として加算。結果も double
var x1 = 1 + 2.5d; // = 3.5
// int - long は long - long
var x2 = int.MinValue - 1L; // = -2147483649
// 乗算
var x3 = 2 * 4; // = 8
// uncheckedコンテキストではオーバーフローしても処理が続行される
// int.MaxValue * 2L だと 4294967294L
unchecked
{
var x4 = int.MaxValue * 2; // = -2
}
// 整数の除算は演算結果も整数
var x5 = 7 / 2; // = 3
// 割られる数がdoubleであれば演算結果もdoubleになる
var x6 = 7f / 2; // = 3.5
// 割る数がdoubleの場合もdouble型の除算が行われる
var x7 = 7 / 2.0; // = 3.5
// 剰余
var x8 = 7 % 2; // = 1
// 浮動小数点
var x9 = 7f % 2.1; // = 0.7
}
}
|
乗算*
、除算/
、剰余%
、加算+
、減算-
は算術演算子と呼ばれ、演算子の左右の値を算術演算する。
同じ型(例えばint
型)同士の演算であれば演算の結果もint
型になるが、例えばint
とdouble
を演算する場合はint
型の値がdouble
型に変換されてdouble
型の演算がオーバーロードにより実行され、double
型が返される。
また、unchecked
コンテキストの中では演算結果がオーバーフローしても処理を続行する。checked
コンテキストの中ではオーバーフローする場合は、コンパイルエラーが発生するか、もしくは実行時にSystem.OverflowException
がスローされる。コンパイルオプションでプログラム全体のデフォルトをunchecked
もしくはchecked
に指定できるが、このコードのように適宜明示的に指定することもできる。
剰余はいわゆる割り算の「余り」を計算する。浮動小数点でも剰余演算が定義されている。.NETでは浮動小数点の剰余はで計算される(はx以下の最大の整数)。
シフト演算子(<<、>>)
class Program
{
static void Main(string[] args)
{
// 0001 << 00001(1bit左シフト) = 0010
var x0 = 1 << 1; // 2
// intまたuintのときは下位5bitをシフトカウントとして使う(要するに32bitの数値なので0~31bitのシフトが可能ということ)
// 33 = 0010 0001 だが、下位5bitの 0 0001 でシフトすることになる
// 0001 << 00001(1bit左シフト) = 0010
var x1 = 1 << 33; // 2
// longまたはulongのときは下位6bitでシフトする(要するに64bitの数値なので0~63bitのシフトが可能ということ)
// 0001 << 100001(33bit左シフト) = 0010 0000 0000 0000 0000 0000 0000 0000 0000
var x2 = 1L << 33; // 8589934592
// 97 = 0110 0001の下位6bitの 10 0001 でシフトする
// 0001 << 100001(33bit左シフト) = 同上
var x3 = 1L << 97; // 8589934592
// 0011 1110 1000 >> 3(3bit右シフト) = 125 (0111 1101)
var x4 = 1000 >> 3; // 125
}
}
|
参考:例えばint
型の1は2進数で表すと0001となる。
左シフト演算子<<
と右シフト演算子>>
はシフト演算を行う。左シフトの場合、左辺がint
値もしくはuint
値の場合は右辺の数値の下位5ビット(bit)がシフトカウントとして使われ、long
値もしくはulong
値の場合は右辺の数値の下位6bitがシフトカウントとして使われる。シフト演算の場合、オーバーフローは発生しない。
比較演算子(<、>)
class Program
{
static void Main(string[] args)
{
var f1 = 1 > 1; // False
var f2 = 1 >= 1; // True
var f3 = 1 < 2; // True
var f4 = 1 <= 2; // True
}
}
|
比較演算子は数値型もしくは列挙型に対して左辺と右辺の値を比較した結果をbool
(ブール)値で返す。
等値演算子(==、!=)
class Program
{
static void Main(string[] args)
{
var f1 = 1 == 2; // False
var f2 = 1 != 1; // False
var f3 = new object() == new object(); // False
var f4 = "abc" == string.Copy("a") + "bc"; // True
}
}
|
等値演算子==
および非等値演算子!=
は左辺と右辺の値を比較する。string
以外の参照型の場合は、同一のインスタンスを比較した場合のみtrueが返される。string
型の場合は文字列の値を比較する。
is/as演算子とキャスト式
using System;
class Program
{
static void Main(string[] args)
{
var b = new B();
var c = new C();
var f0 = b is B; // True
var f1 = b is A; // True
var f2 = c is A; // False
// A型の変数として宣言
A a = new B();
// isで型チェック後、cast式でキャストする
if (a is B)
{
((B)a).Test(); // I'm B
}
// 上記のis+キャストはas+nullチェックで代用可能
var b2 = a as B;
if (b2 != null)
{
b2.Test(); // I'm B
}
// C# 7.0で導入されたis演算子の拡張
int? i = 1;
if (i is null) return; // iがnullのときのみtrueとなる。constant pattern "null"
if (i is int.MaxValue) return; // iがint.MaxValueと等しいときのみtrueとなる。constant pattern
if (i is 3) return;
if (!(i is int j)) // type pattern "int j"
{
//j.Dump(); // is演算子の評価結果がtrueのときのみ変数が割り当てられるので、ここでjを参照するとコンパイルエラー
return;
}
new string('*', j).Dump(); // 上のif文でis演算子の評価結果がfalseのときにreturnもしくは例外をスローせずにiを参照すると、確実に代入される保証がないのでコンパイルエラーになる
string str = null;
if (str is var k) // var patternはnullのときも含め、常にtrueとなり割り当てられる
{
k.Dump();
}
}
}
class A { }
class B : A { public void Test() { Console.WriteLine("I'm B."); } }
class C { }
|
is
演算子を使うと、「オブジェクトの実行時の型」が「指定した型」と互換性があるかどうかを動的にチェックできる。
as
演算子もしくはcast(キャスト)式を使うと、式を特定の型にキャストできる。cast式は、指定した型に式を明示的に変換し、その変換が存在しない場合は例外をスローする。as
演算子は、指定した参照型もしくはnull許容型に式を明示的に変換するが、キャストできないときは例外をスローせずnullを返す。
as
演算子は、is
演算子とcast式の組み合わせることでほぼ同値なコードに置き換え可能だが(式 as 型
= 式 is 型 ? (型)式 : (型)null
)、as
演算子を使えば、動的な型チェックが1回だけ実行されるようにコンパイラーによって最適化される。as
演算子で書けるときはas
演算子を優先的に使うのがよいだろう。
C# 7.0からis
演算子が拡張されて、constant pattern、type pattern、var patternの3通りの書き方が追加された。これらの書き方は後に紹介するswitch
文の拡張と同じである。
- constant pattern:
is
演算子の後ろに定数を記述し、定数と等しいときのみis
演算子の結果はtrueとなる - type pattern:
is
演算子の後ろに型と変数を記述する。型に対するis
演算子の評価結果がtrueのときのみ、その型にキャストした値が変数に代入される。この変数はtrueのときのみ代入されるため、falseのときにこの変数を参照しようとすると未定義変数への参照となりコンパイルエラーとなる - var pattern: nullの場合も含めて常にtrueとなり、変数に値が代入される
論理演算子(&&、||)
using System;
class Program
{
static void Main(string[] args)
{
var f1 = True() || False(); // True is called: True
var f2 = True() | False(); // True is called: False is called: True
var f3 = False() && True(); // False is called: False
var f4 = False() & True(); // False is called: True is called: False
}
static bool True()
{
Console.Write("True is called: ");
return true;
}
static bool False()
{
Console.Write("False is called: ");
return false;
}
}
|
論理演算子は、bool
値の論理演算結果を返す。
AND演算子&
および&&
は、左辺・右辺の値がともにTrue(真)のときのみtrueを返す。
OR演算子|
および||
は、左辺・右辺の値のどちらかがTrueのときにtrueを返す。
&&
および||
はショートサーキット(短絡評価)を行う点が、&
および|
と異なる。すなわち、&&
は左辺がFalse(偽)のときは右辺の評価を行わない。||
は左辺がTrueのときは右辺の評価を行わない。右辺の式が副作用を含むときは使い分けに注意が必要だ。一般的に多くのコードでは、より効率的に処理できるショートサーキット評価&&
および||
を使った上で、副作用を含む式を避けている。
条件演算子、null合算演算子
class Program
{
static void Main(string[] args)
{
var f1 = true;
var t1 = f1 ? "true" : "false"; // "true"
var f2 = false;
var t2 = f2 ? "true" : "false"; // "false"
string v1 = null;
var t3 = v1 ?? "default"; // "default"
string v2 = "text";
var t4 = v2 ?? "default"; // "text"
}
}
|
?:
は条件演算子もしくは三項演算子と呼ばれる。b ? x : y
の形式で利用し、条件b
を評価し、trueならばx
が評価され、falseならばy
が評価される。
null合体演算子??
は左辺の値がnullでない場合に左辺の値を返し、nullのときには右辺の値を返す。nullのときにデフォルト値を設定したい場合などによく利用される。
null条件演算子
using System;
class Program
{
static void Main(string[] args)
{
int[] array = null;
var length = array?.Length; // null
var i1 = array?[1]; // null。i1はint?型
Func<int> func = null;
var i2 = func?.Invoke(); // null。i2はint?型
}
}
|
null条件演算子は、メンバーアクセス?.
もしくはインデックス・アクセス?[
する際にnullチェックの記述を簡略化できる。すなわち、nullの場合はメンバーアクセスやインデックス・アクセスを行わずにnullを返し、nullでないときのみメンバーアクセスやインデックス・アクセスを行う。
これはデリゲートの呼び出しの際にnullチェックを行うことにも利用できる。デリゲートの関数をnull条件演算子でnullチェックして呼び出すには、?.Invoke()
で呼び出せばよい。関数の返り値がnull非許容の値型の場合、null条件演算子の返り値はnull許容型になる。
nameof演算子
using System;
class Program
{
static void Main(string[] args)
{
var s1 = nameof(args); // args
var s2 = nameof(DateTime.Now); // Now
var s3 = nameof(System.Linq); // Linq
}
}
|
nameof
はC# 6.0で導入された演算子で、変数やクラスといった式の名前を取得するためのものである。変数名やクラス名などを文字列で指定する必要がある場所に利用することで、例えばソースコードの変数名を変更するときに、IDEのリファクタリング機能などによる追随が容易になるといったメリットがある。
ステートメント(文)
if
using System;
class Program
{
static void Main(string[] args)
{
var flg = true;
// true
if (flg)
Console.WriteLine("true");
// else if
if (Check())
{
Console.WriteLine("if");
}
else if (flg)
{
Console.WriteLine("else if");
}
else
{
Console.WriteLine("else");
}
}
static bool Check()
{
return false;
}
}
|
if
ステートメントはbool
型の式の値を評価し、trueならばif
節のステートメントを、falseならばelse
節のステートメントを実行する。複数の式を実行する場合はブロック{ }
内を利用するが、1つの式のみを記述することもできる。else if
により、複数のbool
型の式で処理を分岐させることもできる。
switch
using System;
class Program
{
static void Main(string[] args)
{
var dayOfWeek = DayOfWeek.Thursday;
string text;
switch (dayOfWeek)
{
case DayOfWeek.Monday:
text = "月";
break;
case DayOfWeek.Tuesday:
text = "火";
break;
case DayOfWeek.Wednesday:
text = "水";
break;
case DayOfWeek.Thursday:
text = "木";
break;
case DayOfWeek.Friday:
text = "金";
break;
case DayOfWeek.Saturday:
text = "土";
break;
case DayOfWeek.Sunday:
text = "日";
break;
default:
throw new ArgumentOutOfRangeException();
}
// なお、日付を表すDateTime型のインスタンスからカレントカルチャの曜日の省略名を得る場合はこのように書ける
var s1 = $"{DateTime.Now:ddd}";
}
}
|
DateTime.DayOfWeek
プロパティから日本語の曜日の省略名を取得するコードを記述しているが、最後にある通り、DateTime
型のインスタンスから曜日の省略名を取得するには書式指定文字列を指定することでも記述できる。
switch
ステートメントは、キーワードswitch
の後に指定した式を評価して、case
で指定した定数と一致したステートメントを実行する制御ステートメントだ。C# 7.0の新機能により拡張されたswitch
ステートメントと従来のswitch
ステートメントでは挙動が異なる点がある。そこで、従来の挙動とC# 7.0新機能を分けて順番に説明しよう。
まず従来のswitch
ステートメント機能について説明すると、switch
に指定できる式は、数値型もしくはenum
/char
/string
型の式と、それらのnull許容型を返す式のみである。また、case
ラベルには定数のみが指定できる。
switch
ステートメントのcase
ラベルに記述するステートメントは、if
ステートメントなどと異なり複数のステートメントをブロック{ }
でまとめることなく記述できる。つまりcase
ラベルに続く複数のステートメントは、次のcase
もしくはdefault
ラベルまで実行される。しかし、ローカル変数のスコープを狭めるといった意図でブロック{ }
でまとめることもよく見かける。
あるcase
ラベルから後続のcase
ラベルに処理を続行するいわゆる「フォールスルー」はC#では禁止されているため、各case
ラベルの式はbreak
やreturn
で制御をswitch
ステートメントの外に移す必要がある。フォールスルーはgoto
ラベルステートメントで疑似的に記述することはできるが、goto
文は処理の制御を分かりづらくすることもあるので注意が必要だ。なお、複数のcase
ラベルを連続して併記している場合(=各case
ラベルの間にステートメントを含まない場合)に限り、フォールスルーのような形でそれら全てのcase
で後続のステートメントの処理が実行される(逆にいうと、「1つのswitch
セクションは、複数のcase
ラベルを持つことが可能」というわけだ)。
default
ラベルを記述することもでき、switch
で評価した値がどのcase
ラベルにも一致しない場合に、default
ラベルに指定したステートメントが実行される。
enum
型を指定した場合であっても、そのenum
の取り得る全ての値をcase
に列挙しているか検査する仕組みは、言語仕様としては存在しない。が、Roslyn拡張といったコンパイラーの拡張機能などで実現することは可能である。
case
ラベルとdefault
ラベルは、任意の順番で記述できる。「どの変数の値のときに、どのラベルが実行されるか」という実行順序は記述する順番によらない。このような挙動になる理由は、default
ラベルは必ずどのcase
の値にも一致しないときに実行されることと、case
には定数しか指定できないからだ。詳細な挙動内容はコンパイラーの実装依存ではあるが、基本的には、従来のswitch
ステートメントとif
~else
ステートメントの両方で同等の処理を記述した場合、switch
ステートメントによる分岐処理の方が(全てを上から順番に実行しない分だけ)高速になる。
次にC# 7.0で拡張されたswitch
ステートメント機能について説明すると、switch
には、従来のものに加えて、任意のオブジェクトを返す式も指定できるようになった(※従来形式のラベルと併用できる)。また、case
ラベルには、従来の定数(いわばis
演算子でいうconstant pattern)に加えて、変数宣言(いわばis
演算子でいうtype patternやvar pattern)や各patternに対してガード節と呼ばれる条件指定も指定できるようになっている。
case
ラベルにtype patternやvar patternによる変数宣言を指定した場合、そのpatternに一致したときのみ、宣言した変数に値が代入され、ラベルのステートメント内で参照できる。
また、各patternに対してガード節による条件を指定した場合、その条件がtrueのときのみ、そのラベルのステートメントが実行される。ガード節の評価結果がfalseの場合は、次のラベルの評価に移る。
では、どういったときに、従来とC# 7.0、どちらの機能で挙動することになるのだろうか? これについては、従来のswitch
に指定できないオブジェクトが指定されている場合のみ、そのswitch
ステートメントは拡張されたswitch
ステートメントとして振る舞うことになる。拡張されたswitch
ステートメントの挙動で注意すべき点は、前述した従来の挙動とは異なり、case
ラベルは上から順番に評価されるということだ。イメージとしては、上から順番にif
ステートメントが並んでいるのと同等と考えればいいだろう。そのため、分岐処理の速度もif
ステートメントの連続と同程度になり得る。
using System;
class Program
{
static void Run()
{
object shape = new Circle(4);
switch (shape)
{
// C# 6.0以前のcase節
case 0:
Console.WriteLine("should not be '0'");
break;
// C# 6.0以前のcaseにガード節追加
case 1 when IsDebug(shape):
Console.WriteLine("should not be '1' if debug is enabled");
break;
// type pattern
// case Circle: のようにプリミティブでない型をC# 6.0以前のように記述するとコンパイルエラー。変数宣言として記述しないといけない
case Circle c:
Console.WriteLine($"circle with radius {c.Radius}");
break;
// type patternにガード節
case Rectangle s when (s.Length == s.Height):
Console.WriteLine($"{s.Length} x {s.Height} square");
break;
// 上のガード節に一致しない場合のtype pattern
case Rectangle r:
Console.WriteLine($"{r.Length} x {r.Height} rectangle");
break;
// var pattternも利用可能
case var i when IsDebug(i):
Console.WriteLine("debug is enabled.");
break;
default:
Console.WriteLine("<unknown shape>");
break;
}
}
private static bool IsDebug(object i) => i is 1;
}
class Circle
{
public int Radius { get; set; }
public Circle(int r)
{
Radius = r;
}
}
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
}
class Rectangle
{
public int Height { get; }
public int Length { get; }
public Rectangle(int h, int l)
{
Height = h;
Length = l;
}
}
|
switch
ステートメントの拡張whileとdo
using System;
class Program
{
static void Main(string[] args)
{
int term = 0;
while (term <= 3)
{
Console.WriteLine(term);
++term;
}
term = 0;
do
{
Console.WriteLine(term);
++term;
} while (term == 0);
}
}
|
while
ステートメントとdo
ステートメントは繰り返しのための制御構文だ。どちらもwhile
キーワードの後ろの条件を評価した結果がtrueの間、繰り返し処理を行う。while
が繰り返し処理の前に条件を評価するのに対し、do
は繰り返し処理をした後に条件を評価する。
forとforeach
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
var array = new[] { 1, 3, 5 };
Console.WriteLine("for文");
var length = array.Length;
for (int i = 0; i < length; i++)
{
Console.WriteLine(array[i]);
}
Console.WriteLine("foreach文:配列");
foreach (var e in array)
{
Console.WriteLine(e);
}
Console.WriteLine("foreach文:Enumerator");
var enumerable = Enumerable.Range(1, 5);
foreach (var e in enumerable)
{
Console.WriteLine(e);
}
Console.WriteLine("foreach文:cast");
// int型とdouble型を混ぜた配列をobject[]型として宣言
var objects = new object[] { 1, 3, 4.5d };
// 要素型はvarだとobject型になるが、以下のようにint型で宣言するとキャストされるため、
// 要素がdouble型のときにキャストに失敗して実行時例外がスローされる
foreach (int e in objects)
{
Console.WriteLine(e);
}
}
}
|
for
ステートメントとforeach
ステートメントも繰り返しを制御する。
for
ステートメントは、for (初期化式; 条件式; 反復式)
という構文で、
- 一番最初に1回だけ初期化式を評価した後、
- 条件式を評価して、
- 結果がtrueの場合、以下のb~dの処理を実行。falseの場合はループを終了して抜ける。
- まず
for
の後の埋め込みステートメントを実行し、 - 次に反復式を評価する。
- 再び、2の条件式の評価から順に繰り返す。
for
ステートメントは、配列の要素を列挙する場合など、繰り返し回数が最初に分かっている場合などに使われることが多い。
foreach
ステートメントは、コレクションの要素を列挙し、コレクションの要素ごとに埋め込みステートメントを実行する。ここで利用できるコレクションとは、配列やIEnumerable<T>
/IEnumerable
インターフェースを実装したコレクションクラスなどである。なお、言語仕様上はforeach
に指定するコレクションはIEnumerable
インターフェースを実装する必要はなく、GetEnumerator
メソッドなどが定義されていればよいことになっている。
また、foreach
ステートメントの要素は、var
ではなく明示的に型を指定することもできる。これは要素に特定の型しか格納されていないが、定義の上ではobject
型が返り値になっている場合などに利用できるが、サンプルコードにあるように実行時に格納されている要素の型が、foreach
の要素の型と違っておりキャストできない場合は、実行時例外がスローされる。
breakとcontinue
using System;
class Program
{
static void Main(string[] args)
{
var array = new[] { 1, 2, 3, 4, 5 };
foreach (var e in array)
{
if (e % 2 == 1)
continue; // 奇数のときは次の繰り返しへ制御を移す(=以下のコードはスキップされる)
if (e == 4)
break; // 4のときにforeachを終了させる
Console.WriteLine(e); // 出力されるのは「2」のみ
}
}
}
|
break
ステートメントは、すでに説明したswitch
/while
/do
/for
/foreach
ステートメントの内部で利用し、すぐ外側のステートメントを終了する。
continue
ステートメントは、while
/do
/for
/foreach
ステートメント内部で利用し、すぐ外側のステートメントの新たな繰り返しに制御を移し、処理を継続する。
■
以上、今回前編ではC#言語の文法の中でも最もベーシックな「型」「変数」「演算子」「ステートメント(文)」の基本機能をできるだけコンパクトに説明した。次回中編では、オブジェクト指向言語であるC#の要となる「クラス」関連の機能として「名前空間」「クラス(メソッド/プロパティ/イベント/インデクサー/演算子オーバーロード/コンストラクターとデストラクター)」について説明する。
1. C# 基礎文法【6.0対応】 ― 1回完結の最速再入門!
項目を羅列するだけでもかなり長くなってしまうC# 6.0の主要な文法を、実利用者目線でできるだけコンパクトにまとめた。日々のコーディングの「あれ、どう書くんだっけ?」を素早く解決するためのリファレンス。
2. 【現在、表示中】≫ C# 基礎文法 最速再入門【7.0対応】
「あれ、どう書くんだっけ?」を素早く解決するための、C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。前編では、C#の歴史/開発ツール/プログラムの実行と制御/型と変数/演算子/ステートメントを説明する。
3. C# クラスの基本機能 最速再入門【7.0対応】
C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。中編では、名前空間/クラス/メソッド/プロパティ/イベント/インデクサー/演算子オーバーロード/コンストラクターとデストラクターを説明する。
4. C# タプル/ローカル関数/ラムダ式/非同期処理/例外処理 最速再入門【7.0対応】
C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。後編では、タプルと分解(デコンストラクション)/ローカル関数/構造体/継承とインターフェース/列挙型/イテレーター/例外処理/リソースの解放/ラムダ式/非同期処理(async/await)を説明する。