C# 初級講座 |
2023/12/28
この記事が対象とする製品・バージョン
Visual Studio 2022 | ◎ | 対象です。 | |
Visual Studio 2019 | ◎ | 対象です。 | |
Visual Studio 2017 | △ | 対象外ですが、参考になります。 | |
Visual Studio 2015 | △ | 対象外ですが、参考になります。 | |
Visual Studio 2013 | △ | 対象外ですが、参考になります。 | |
Visual Studio 2012 | △ | 対象外ですが、参考になります。 | |
Visual Studio 2010 | △ | 対象外ですが、参考になります。 | |
Visual Studio 2008 | △ | 対象外ですが、参考になります。 | |
Visual Studio 2005 | △ | 対象外ですが、参考になります。 | |
Visual Studio.NET 2003 | × | 対象外です。 | |
Visual Studio.NET (2002) | × | 対象外です。 | |
Visual Studio Code | △ | 対象外ですが、参考になります。 |
目次
int や string などの基本型は単一のデータ(値)を保持します。ある程度複雑なプログラムではデータを単一に扱うのではなく、意味のあるまとまりとして扱うシーンが多くなります。データの組み合わせに意味があったり、あるいは「住所」というもっと高度なデータを扱うこともよくあります。C#には住所というデータ型はありませんが、住所は分解すると都道府県・市区町村・番地・郵便番号など基本的な型で表現できる要素の組み合わせであることがわかります。
C#にはデータを構造化して扱う機能がいくつかあります。タプルや構造体や匿名型やレコードなどです。
コレクションも複数のデータを扱うことができるという点でデータの構造化と似ていますが、コレクションの場合は同種のデータを複数扱うことができるのに対し、タプルや構造体などは性質の違うデータをひとまとまりにします。
データの構造化を使用しなくても、基本型だけを使って注意深く大量の変数を使用してプログラムすれば、最終的に同じような機能を実現することは理屈のうえではできますが、あまりにもプログラムが煩雑になってしまうので好まれません。また、フレームワークの便利な機能などを使用しようとするとデータが構造化されていることが前提となっていることもしばしばです。
このようなわけですから、データをどのように構造化できるのか見ておくことにしましょう。
今回は第一弾で、タプルを扱います。
タプル(Tuple)を使うと1つの変数に複数の値(要素)の組み合わせを持つことができます。
次の例はタプルを使って1つの変数に文字と数値の値(要素)を持たせています。
var x = ("あいうえお", 12345);
System.Diagnostics.Debug.WriteLine(x.Item1); //あいうえお と出力されます。
System.Diagnostics.Debug.WriteLine(x.Item2); //12345 と出力されます。
このようにタプルは複数の値(要素)をカンマで区切ってカッコの中に入れて表現します。
それぞれの値(要素)を読み書きするには Item1フィールド、Item2フィールドを使用します。
同じように3つの値を表現することもできます。
var x = ("あいうえお", 12345, "ABC");
System.Diagnostics.Debug.WriteLine(x.Item1); //あいうえお と出力されます。
System.Diagnostics.Debug.WriteLine(x.Item2); //12345 と出力されます。
System.Diagnostics.Debug.WriteLine(x.Item3); //12345 と出力されます。
4つでも、5つでも同じことができます。
上記の例では変数xの型はvarを使って推論しています。タプルを使う場合は、通常、型推論機能を使って自動的に型を判定させます。
あえて型を記述すると、最初の例ではx は ValueTuple<string, int>型になり、2番目の例では、ValueTuple<string, int, string>型になります。つまり、タプルの実体はフレームワークの ValueTuple構造体(読み方:ValueTuple=バリュータプル)であり、このValueTuple構造体はどのような型の組み合わせでも表現できるように、(コレクションと同じように)ジェネリック機能を使った型パラメーターをとるというわけです。
既定ではタプルの要素を読み書きするにはItem1、Item2、・・・というわかりにくい名前のフィールドを使用することになります。
タプル作成時に要素に名前を付けることでもっとわかりやすい名前でアクセスさせることができます。
次のように : を使った少し変わった構文を使用します。
var ieyasu = (Name:"徳川家康", Age:23);
System.Diagnostics.Debug.WriteLine(ieyasu.Name); //徳川家康 と出力されます。
System.Diagnostics.Debug.WriteLine(ieyasu.Age); //23 と出力されます。
このプログラムでは変数ieyasuはValueTuple<string, int>型であり、フィールドはItem1、Item2、・・・という名前なのですが、C#の言語機能により、Nameという記述でItem1フィールドにアクセスし、Ageという名前でItem2フィールドにアクセスできるようになります。
Visual Studioは ieyasu. と入力するとヒントに Item1、Item2は表示せず代わりに Name、Ageを表示するようになるので、まるでそのようなオブジェクトが存在するかのような使いやすさでタプルを扱うことができます。
ブレークポイントをしかけて、変数ieyasuを右クリックしてクイックウォッチをするとどのように値を持っているかわかります。C#は Name や Age というフィールドがあるように見せかけていますが、未加工の生の値を表示する「列ビュー」を展開すると、実際にはItem1とItem2というフィールドがあるということがわかります。
さらに、上記の例のように明示的に名前を指定しなくても、C#は使用されている変数などからタプルの要素の名前を自動的に推論します。
次の例ではタプル作成時に使用している変数の名前から自動的にタプルの要素に名前が付きます。
string name = "徳川家康";
int age = 23;
var ieyasu = (name, age);
System.Diagnostics.Debug.WriteLine(ieyasu.name); //徳川家康 と出力されます。
System.Diagnostics.Debug.WriteLine(ieyasu.age); //23 と出力されます。
変数ではなく、タプル作成時にメソッドやプロパティを使用している場合、その名前から要素名を推論します。
var ieyasu = (Environment.UserName, DateTime.Now.Hour);
System.Diagnostics.Debug.WriteLine(ieyasu.UserName);
System.Diagnostics.Debug.WriteLine(ieyasu.Hour);
この例では、要素名は UserName と Hour になります。
ToStringやItem1など 名前が衝突する場合などうまく推論できない場合もあります。うまく推論できない場合は、名前がないときと同様に順番に付けられるItem1, Item2, ... というフィールド名で要素にアクセスすることになります。
ここまではC#のタプルの記述方法を紹介し、タプルの実体がフレームワークのValueTuple型であることも説明しました。
フレームワークにはもう1つ、その名もずばり Tuple (読み方:Tuple=タプル)というクラスがあります。このTupleとValueTupleはちょっと使い方が違って紛らわしいので、ここで注意喚起しておきます。結論から言うと微妙な違いを気にするには面倒なので Tupleクラスの方は使わないことをお勧めします。C#の言語機能としてのタプルの登場より前の2010年ごろから使えたので、そのころ作られたプログラムはこのTupleクラスの方が使われている可能性があります。
TupleはC#のタプルと同じような使い方ができるのですが、C#の言語の構文としてはサポートされておらず、このTupleクラスを使いたい場合は、一般のフレームワークのクラスとして呼び出す必要があります。
たとえば、次のようにしてTupleクラスを使用できます。
<string, int>は型パラメーターで、その後の("徳川家康", 23)はコンストラクターの呼び出しです。
var ieyasu = new Tuple<string, int>("徳川家康", 23);
System.Diagnostics.Debug.WriteLine(ieyasu.Item1); //徳川家康 と出力されます。
System.Diagnostics.Debug.WriteLine(ieyasu.Item2); //23 と出力されます。
わかりやすい違いはC#のタプルの方は後から要素の値を変更することができるのに、フレームワークのTupleクラスの方は後から要素を変更することができないことです。C#のタプルの実体であるValueTupleは構造体で、Tupleはクラスであり、ValueTupleの要素Item1、Item2はフィールドで、Tupleの方はプロパティであるという違いもあります。
タプルから値を取り出すときに、「分解」と呼ばれる機能を使ってまとめて取り出すことができます。
次のように直感的に書けます。
var myTuple = ("Apple", "Banana", 123);
string a, b;
int c;
(a, b, c) = myTuple;
この例では、変数a, b, cの値はそれぞれ、"Apple", "Banana", 123 になります。
変数を経由しないで直接タプルを分解することもできます。
string a, b;
int c;
(a, b, c) = ("Apple", "Banana", 123);
このような簡単な使い方ができるので、複数の変数に一度に値を割り当てる簡単な方法としてタプルを使用する人もいます。
分解と変数の宣言を同時に行うこともできます。次のように書きます。
(string a, string b, int c) = ("Apple", "Banana", 123);
varを使って型推論させることもできます。
(var a, var b, var c) = ("Apple", "Banana", 123);
このとき、左辺のかっこの前に一括してvarをつける簡単な書き方もできます。
var (a, b, c) = ("Apple", "Banana", 123);
この書き方でも型推論は変数ごとに行われるので、変数aと変数bはstringと推論され、変数cはintと推論されます。
もう1つちょっと面白い使い方を紹介します。タプルは変数の値を入れ替えるのにも使用できます。いわゆる変数のスワップ(swap)と呼ばれるものです。
int x = 123;
int y = 987;
//x と y の値が入れ替わります。つまり、xが987になり、yが123になります。
(x, y) = (y, x);
なお、分解はタプルに専用の機能というわけではなく、タプル以外のものでも分解できるものはあります。Deconstructというメソッドがルール通りに実装されているものはここで説明した分解が行えます。
メソッドの戻りをタプルにすることで、1つのメソッドから実質的に複数の戻り値を返すメソッドを作成することができます。2017年3月にC# 7.0でタプルが導入される前は、メソッドから複数の値を戻す必要がある場合、構造体やクラスを使って表現するのか、戻り値とは別に引数の値を書き換えて呼び出し元に戻すという方法が採られていました。しかし、これだとちょっと小さな機能を作成するときにもいちいち構造体やクラスを作成しなければならななかったり、プログラムがわかりにくくなったりと不便でした。タプルはこの悩みを解消します。
次のようにタプルをreturnすることで複数の値を戻り値にするメソッドを簡単に作れます。
public (int, int) Warizan(int x, int y)
{
int kotae = x / y;
int amari = x % y;
var result = (kotae, amari);
return result;
}
慣れない人のためにタプルを1回変数 result に代入してから return していますが、直接 return (kotae, amari) と書いても同じです。
最初の行のメソッドの定義で、戻り値の型が(int, int)になっている点に注意してください。これはintの要素を2つ持つタプルが戻り値にあるということを示しています。
呼び出し側は次のように書きます。
var x = Warizan(10, 3);
System.Diagnostics.Debug.WriteLine($"10÷3={x.Item1}…{x.Item2}");
タプルの各要素にアクセスするのに、Item1, Item2 という名前を使うのがわかりにくいポイントです。これをスマートにわかりやすくする方法が2つあります。
1つは分解を使う方法です。次の通りです。
var (kotae, amari) = Warizan(10, 3);
System.Diagnostics.Debug.WriteLine($"10÷3={kotae}…{amari}");
もう1つは戻り値を名前付きタプルにすることです。メソッドの定義を次のように書き換えます。
public (int quotient, int reminder) Warizan(int x, int y)
{
int kotae = x / y;
int amari = x % y;
var result = (kotae, amari);
return result;
}
こうすると呼び出し側ではこの名前がついた名前付きタプルを戻り値で受け取るので、この名前で値にアクセスできます。
var x = Warizan(10, 3);
System.Diagnostics.Debug.WriteLine($"10÷3={x.quotient}…{x.reminder}");
もちろん分解も使えますから、名前付きタプルでメソッドを定義してのが親切そうです。
フレームワークのメソッドの中にもタプルを返すものがあります。
たとえば、この例と同じわり算の答えとあまりを返す Math.DivRemメソッドはタプルを返します。
var (kotae, amari) = Math.DivRem(10, 3);
System.Diagnostics.Debug.WriteLine($"10÷3={kotae}…{amari}");
このメソッドは昔からありましたが、.NET 6(2021年11月)のときにタプルを返す機能が追加されました。それ以前の.NET5や.NET Frameworkではこのような使い方はできません。
.NET 5のときは次のようにこのメソッドを呼び出していました。戻り値はわり算の答えです。わり算のあまりは第3引数に指定した変数に代入されます。このように引数の値が呼び出し前後で変化する場合はoutなどのキーワードを付けることになっています。なお、この例で第3引数についている int は、ここで変数を宣言しているということです。別途 int amari と宣言された変数を第3引数に指定する場合は out amari となります。
int kotae = Math.DivRem(10, 3, out int amari);
System.Diagnostics.Debug.WriteLine($"10÷3={kotae}…{amari}");
昔はタプルがなかったので、このように引数の値を書き換えるなどちょっとわかりにくいプログラムが必要でした。互換性のため.NET6以降でもDivRemメソッドをこのように使用することは可能です。
ところで、タプルを分解するときに、すべての値が必要というわけではないのであれば、いらない値を無視することができます。これを破棄と呼びます。
たとえば、次のメソッドは天気と最高気温と最低気温をタプルで返します。
public (string tenki, int maxDegree, int minDegree) GetTenki()
{
return ("晴れ", 28, 19);
}
呼び出し側では最高気温と最低気温だけ知りたくて、天気は不要である場合、次のように天気の部分にアンダースコアを指定して分解できます。これが破棄です。天気はいらないですという意味です。
var (_, maxDegree, minDegree) = GetTenki();
もし、破棄がなければ、不要な天気のために変数を1つ用意しなければならないので、破棄を使用することで無駄を省けるというわけです。
複数の値を破棄することもできます。
興味深いことにすべての値を破棄することもできます。私には使いどころがわかりませんが。
Dictionaryから要素を取得するときにも分解が使えます。
次の例は、初級講座第23回で説明したDictionaryの要素を列挙する例です。(第23回を既に読まれている場合、同じ説明の繰り返しになってしまいます)
//惑星とその衛星の数。
var moons = new Dictionary<string, int>
{
{"水星",0 },
{"金星",0 },
{"地球",1 }, //地球は月だけ。
{"火星",2 }, //火星はフォボスとダイモス。
{"木星",79 },//イオ、エウロパ、ガニメデ、カリストを筆頭に多数
{"土星",82 },//タイタンなど多数
{"天王星",27 },
{"海王星", 14} //トリトンなど
};
//型推論により moon は KeyValuePair<string, int>型になります。
foreach (var moon in moons)
{
string planetName = moon.Key;
int moonCount = moon.Value;
System.Diagnostics.Debug.WriteLine($"{planetName}の衛星の数は{moonCount}個");
}
このプログラムではforeach の中で、moon.Keyを使ってキー(惑星の名前)を取り出し、moon.Valueを使って値(衛星の数)を取り出しています。
分解を使うともう少しスマートに要素を取り出せます。次のようにします。
//惑星とその衛星の数。
var moons = new Dictionary<string, int>
{
{"水星",0 },
{"金星",0 },
{"地球",1 }, //地球は月だけ。
{"火星",2 }, //火星はフォボスとダイモス。
{"木星",79 },//イオ、エウロパ、ガニメデ、カリストを筆頭に多数
{"土星",82 },//タイタンなど多数
{"天王星",27 },
{"海王星", 14} //トリトンなど
};
//型推論により planetName はstring型に、moonCountはint型になります。
foreach (var (planetName, moonCount) in moons)
{
System.Diagnostics.Debug.WriteLine($"{planetName}の衛星の数は{moonCount}個");
}
ここからは急に話が難しくなりますが、プログラムの幅が広がるので書いておきます。理解が追いつかなければとりあえず飛ばしていただいてもよいです。
本題に入る前にSelectメソッド(読み方:Select=セレクト)の説明をしておきます。
コレクションのSelectメソッドを使うとコレクションの各要素から値を抽出することができます。(数学の知識がある人はコレクションの写像を作る機能だと理解してください。)
たとえば、次の例では、まずC:\tempフォルダーのファイルの情報を格納するFileInfoクラスのコレクションを作成します。FileInfoクラスからはファイルの名前やサイズ、作成日時などさまざまな情報が取得できます。
//C:\tempフォルダー内のファイル(FileInfoクラス)のコレクションを作成
var files = new System.IO.DirectoryInfo(@"C:\temp").EnumerateFiles();
//filesコレクションから、ファイル名だけのコレクションを作成
var fileNames = files.Select(file => file.Name);
foreach(var fileName in fileNames)
{
System.Diagnostics.Debug.WriteLine(fileName);
}
次にSelectメソッドを呼び出して、この中から、ファイル名だけを抜粋して新しい fileNamesというコレクションを作成しています。
Selectメソッドでは新しいコレクションに何を採用するかをラムダ式で決定します。(ここが話が難しい点です)
1つの要素をもつコレクションを作成したいのであれば、上記のような例でよいのですが、Selectメソッドで複数の項目を持つコレクションを直接的に作成することはできません。たとえば、ファイル名とサイズの2つを持つコレクションを作ることはできません。次の例はエラーです。
//C:\tempフォルダー内のファイル(FileInfoクラス)のコレクションを作成
var files = new System.IO.DirectoryInfo(@"C:\temp").EnumerateFiles();
//filesコレクションから、ファイル名だけのコレクションを作成
var fileNames = files.Select(file => file.Name, file.Length); //←ここがエラー。複数の項目はSelectできません。
ラムダ式を簡易的なメソッドの記述方法であると考えればこれができないのは当たり前で、次のメソッドがエラーなのと同じ理屈です。
public string XXXXX (FileInfo file)
{
return file.Name, file.Length; //←ここがエラー。メソッドから複数の値をreturnできません。
}
メソッドであれば、今回説明したように、メソッドの戻りをタプルとすることで解決できますね。たとえば、次のような感じです。
public (string Name, long Length) XXXXX (FileInfo file)
{
return (file.Name, file.Length); //←これはタプルなのでOK
}
ということで、Selectメソッドのラムダ式もタプルにしてしまえば複数の値を返せます。
//C:\tempフォルダー内のファイル(FileInfoクラス)のコレクションを作成
var files = new System.IO.DirectoryInfo(@"C:\temp").EnumerateFiles();
//filesコレクションから、ファイル名だけのコレクションを作成
var fileNames = files.Select(file => (file.Name, file.Length)); //←タプルなのでOK
ダメな例と見比べると、( ) が増えているだけなのですが、この ( ) がタプルという意味ですので、大きな違いです。
これが私が言いたかった本題です。これができるとどんな良いことがあるのかはこの下で説明します。
これを活用して、コンピューターのドライブ(CドライブやDドライブなど)と、そのサイズおよび使用率を列挙するプログラムを作ってみましょう。次のようになります。
var drives = System.IO.DriveInfo.GetDrives()
.Select(drive => (drive.Name.TrimEnd(@":\".ToArray()),
drive.TotalSize / 1024 / 1024 / 1024,
(1 - drive.TotalFreeSpace / (double)drive.TotalSize) * 100));
foreach (var drive in drives)
{
var (name, totalSize, freeSize) = drive; //←タプルの分解
System.Diagnostics.Debug.WriteLine($"{name}ドライブ サイズ:{totalSize} GB 使用率:{freeSize:0.0}%");
}
DriveInfo.GetDrive()メソッドはコンピューターのすべてのドライブの情報のコレクションを返します。実体はDriveInfoクラスの配列です。DriveInfoクラスからはドライブの基本的な情報を取得できます。
そのコレクションに対して、Selectメソッドを使って、欲しい情報を3つ取り出した新しいコレクションを生成します。ここでタプルが登場します。赤い ( ) の部分です。
1つ目の情報はCやDなどのドライブ名です。 DriveInfo の Nameプロパティで取得できますが、このプロパティだと "C:\" や "D:\" のように :\ が後につくので、TrimEndメソッドを使って、後に":" と "\" があれば取り除きます。TrimEndのこの使い方では引数は Char型の配列で指定する必要があるので、文字列の@":\"から、ToArray() メソッドを呼び出しで Char型の配列に変換しています。なお、TrimEndは順番は見てくれないので、@"\:" と指定しても同じです。Windowsだけ考えればTrimEndを使う代わりにdrive.Name[0] などと書いて先頭の1文字だけを採用することもできますが、これをやってしまうとLinuxで実行した時にドライブ名が全部 / になってしまってわからないので、今回はLinuxなど他のOSも意識してTrimEndにしてみました。(そもそもドライブの考え方がちょっと違うのでここだけLinuxを意識しても中途半端なのすが)
2つ目の情報はドライブのサイズです。DriveInfo の TotalSizeプロパティで取得できますが、単位がバイトなので、ここでは、1024で3回割って、単位をギガバイト(GB)にしています。ここでは整数のわり算なので、C#は整数で答えを求め、小数は無視されます。
3つ目の情報はドライブの使用率です。空き容量(TotalFreeSpace)÷ドライブサイズ(TotalSize) で空き容量率を計算して、1からそれを引いて逆転させて使用率を求めています。途中 (double) のキャストがある理由は、これがないと整数同士のわり算になって小数部分が無視されてしまうからです。(double)で値を小数にすることで、C#のわり算の意味が変わって、結果の小数を保持するようになります。
変数driveはこの3つの要素をもつタプルのコレクションになります。
さて、結果を出力するときはタプルの分解機能を使っているので気になりませんが、このタプルでは要素に名前がついていないのでちょっと扱いにくいですね。要素に名前を付けてみましょう。
var drives = System.IO.DriveInfo.GetDrives()
.Select(drive => (DriveLetter: drive.Name.TrimEnd(@":\".ToArray()),
GBSize: drive.TotalSize / 1024 / 1024 / 1024,
Usage: (1 - drive.TotalFreeSpace / (double)drive.TotalSize) * 100));
foreach (var drive in drives)
{
System.Diagnostics.Debug.WriteLine($"{drive.DriveLetter}ドライブ サイズ:{drive.GBSize} GB 使用率:{drive.Usage:0.0}%");
}
foreach内でタプルを分解していた部分はそのままでも良かったのですが、折角なのでタプルの要素の名前を使うように書き換えてみました。
さらに、Whereメソッドを使って、使用率が80%以上のドライブだけを抜粋することにしてみましょう。
Whereメソッドはラムダ式で条件を指定して、コレクションの中から条件に当てはまるものだけのコレクションを生成します。
var drives = System.IO.DriveInfo.GetDrives()
.Select(drive => (DriveLetter: drive.Name.TrimEnd(@":\".ToArray()),
GBSize: drive.TotalSize / 1024 / 1024 / 1024,
Usage: (1 - drive.TotalFreeSpace / (double)drive.TotalSize) * 100))
.Where(drive => drive.Usage >= 80);
foreach (var drive in drives)
{
System.Diagnostics.Debug.WriteLine($"{drive.DriveLetter}ドライブ サイズ:{drive.GBSize} GB 使用率:{drive.Usage:0.0}%");
}
すばらしいですね。
タプル自体はC#のちょっとした機能のようにも見えますが、そのタプルを他の機能と連携させることでうまくプログラムを書けるようになります。
なお、最後の例ではSelectメソッドのラムダ式の引数のdriveは DriveInfo.GetDrivesメソッドの戻り値の要素なのでDriveInfo型で、Whereメソッドのラムダ式の引数のdriveは、Selectメソッドが生成するコレクションの要素なのでタプルである点に注意してください。だから、Selectメソッドで定義しているUsageという名前をWhereメソッドのラムダ内で使うことができているのです。Usageという名前を付けていなければ、drive.Item3 >= 80 と書くことになります。