●イテレータ
- インターフェース IEnumerable のメソッド GetEnumerator() を実装すると foreach を使用することができる
- GetEnumerator() はインターフェース IEnumerator を継承したクラスのインスタンスを返す
- これがいわゆる「イテレータ (iterator)」として動作する
- IEnumerator を継承する場合、次に示すプロパティとメソッドを実装する
- Current イテレータが示す要素を取得する
- bool MoveNext() イテレータを次の要素へ進める
- void Reset() イテレータを初期値に戻す
- C# は ver 2.0 以降になると、yield return を使ってイテレータを簡単に作ることができる
- なお、C# では yield return による機能を「イテレータ」と呼んでいる
- GetEnumerator() の中で yiled return 要素; を実行するだけ
- イテレータを途中で中断するときは yield break を使う
- ジェネリックの場合は IEnumerable<T> を継承する
- 実装するメソッドは IEnumerator<T> GetEnumerator()
- もうひとつ IEnumerator IEnumerable.GetEnumerator() も実装すること
- これを実装すると後述する LINQ のメソッドや高階関数を利用できるようになる
リスト : イテレータの簡単な例題 (sample12/Program.cs)
using System;
using System.Collections;
using System.Collections.Generic;
class Foo : IEnumerable {
int a, b, c;
public Foo(int x, int y, int z) {
a = x;
b = y;
c = z;
}
public IEnumerator GetEnumerator() {
yield return a;
yield return b;
yield return c;
}
}
class Bar<T> : IEnumerable<T> {
T a, b, c;
public Bar(T x, T y, T z) {
a = x;
b = y;
c = z;
}
public IEnumerator<T> GetEnumerator() {
yield return a;
yield return b;
yield return c;
}
// public は付けない
IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator();
}
}
class Test {
static void Main() {
var a = new Foo(1, 2, 3);
foreach(int n in a) {
Console.WriteLine("{0}", n);
}
var b = new Bar<int>(10, 20, 30);
foreach(int n in b) {
Console.WriteLine("{0}", n);
}
}
}
$ dotnet run --project sample12
1
2
3
10
20
30
●インデクサー
リスト : インデクサーの簡単な例題 (sample13/Program.cs)
using System;
class Foo {
int a, b, c;
public Foo(int x, int y, int z) {
a = x;
b = y;
c = z;
}
public int this[int n] {
set {
switch(n) {
case 0: a = value; break;
case 1: b = value; break;
default: c = value; break;
}
}
get {
switch(n) {
case 0: return a;
case 1: return b;
default: return c;
}
}
}
}
class Test {
static void Main() {
var a = new Foo(1, 2, 3);
for (int i = 0; i < 3; i++) {
a[i] *= 10;
Console.WriteLine("{0}", a[i]);
}
}
}
$ dotnet run --project sample13
10
20
30
●LINQ
- LINQ (Language Integrated Query) は、C# のデータだけではなく、データベースの問い合わせや XML の操作などを統一して行うための仕組み
- LINQ には SQL 同様のクエリ式を使う方法と、標準的なメソッドや演算子を使う方法がある
- 前者の構文は後者のメソッドや演算子にコンパイルされる
- LINQ には便利なメソッドや高階関数が多数用意されている
- LINQ を使うときは using System.Linq;
- マッピングは Select()
- フィルターは Where()
- 畳み込みは Aggregate()
- OrderBy() は昇順に、OrderByDescending() は逆順にソートする
- 先頭からの探索が First(), 末尾からの探索が Last()
- ElementAt() は n 番目の要素を返す
- Cotains() は要素が含まれていれば真を返す
- All() は引数の関数 (述語) を要素に適用し、すべて真ならば真を返す
- Any() は引数の関数 (述語) を要素に適用し、ひとつでも真ならば真を返す
- GroupBy() はキーを指定して、キーの値が等しいもの集める (グループ化)
- その他の主なメソッド
- Take, Skip, TakeWhilte, SkipWhile
- Concat, Reverse
- Distinct, Union, Intersect, Except
- AsEnumerable, ToArray, ToList, ToDictionary
- SequenceEqual
- Range, Repeat, Empty
- Count, Sum, Min, Max, Average
$ dotnet script
> var a = new int[8] {1,2,3,4,5,6,7,8};
> a.Select(n => n * n)
Enumerable.SelectArrayIterator<int, int>(Count = 8)
> a.Select(n => n * n).ToList()
List<int>(8) { 1, 4, 9, 16, 25, 36, 49, 64 }
> a.Select(n => n * 1.5).ToList()
List<double>(8) { 1.5, 3, 4.5, 6, 7.5, 9, 10.5, 12 }
> a.Where(n => n % 2 == 0).ToList()
List<int>(4) { 2, 4, 6, 8 }
> a.Where(n => n % 2 != 0).ToList()
List<int>(4) { 1, 3, 5, 7 }
> a.Aggregate((sum, n) => sum + n)
36
> a.Aggregate(100, (sum, n) => sum + n)
136
> var b = new int[] {5,6,4,7,3,8,2,9,1,0};
> b.OrderBy(n => n)
OrderedEnumerable<int, int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
> b.OrderBy(n => n).ToList()
List<int>(10) { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }
> b.OrderByDescending(n => n).ToList()
List<int>(10) { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }
> b.First(n => n % 2 == 0)
6
> b.Last(n => n % 2 != 0)
1
> var c = new int[] {2,4,6,8,10};
> c.All(n => n % 2 == 0)
true
> c.Any(n => n % 2 != 0)
false
> var d = new [] { new {name="foo", gr=1}, new {name="bar", gr=2},
* new {name="baz", gr=1}, new {name="oops", gr=2}};
> d
<>f__AnonymousType0#19<string, int>[4] { \{ name = "foo", gr = 1 },
\{ name = "bar", gr = 2 }, \{ name = "baz", gr = 1 }, \{ name = "oops", gr = 2 } }
> d.GroupBy(n => n.gr)
GroupedEnumerable<<>f__AnonymousType0#19<string, int>, int>
{ Key = 1 { Key=1, =\{ name = "foo", gr = 1 },
=\{ name = "baz", gr = 1 } }, Key = 2 { Key=2, =\{ name = "bar", gr = 2 },
=\{ name = "oops", gr = 2 } } }
> foreach (var xs in d.GroupBy(n => n.gr)) {
* Console.WriteLine(xs.Key);
* foreach (var item in xs) {
* Console.WriteLine(item.name);
* }
* }
1
foo
baz
2
bar
oops
- { ... } は匿名型で、コンパイラが型名を決める
- new [] { new { ... }, ... } で匿名型の配列を生成することができる
●LINQ (クエリ式)
- SQL の場合、データの抽出は select 文を使う
- select カラム名 form テーブル名 where 条件式;
- C# の場合、クエリ式を使うと次のようになる
- from 変数名 in コレクション where 条件式 select 要素;
- 昇順にソートする場合は select の前に orderby 要素 を指定する
- 逆順にソートする場合は orderby 要素 descending とする
- group 要素 by キー は指定したキーでグループ化する
- select と group の後ろにクエリ式を書くことはできない
- into でクエリ式を連結することができる
- ... into 変数 ...
- 前のクエリ式の結果が into で指定した変数に格納される
> b
int[10] { 5, 6, 4, 7, 3, 8, 2, 9, 1, 0 }
> from x in b where x % 2 == 0 select x
Enumerable.WhereArrayIterator<int> { 6, 4, 8, 2, 0 }
> from x in b where x % 2 == 0 orderby x select x
OrderedEnumerable<int, int> { 0, 2, 4, 6, 8 }
> var xs = new[] { new {name="foo", score=10}, new {name="bar", score=20},
* new {name="baz", score=40}, new {name="oops", score=30}};
> from x in xs where x.score > 20 select x.name
Enumerable.WhereSelectArrayIterator
<<>f__AnonymousType0#35<string, int>, string> { "baz", "oops" }
> from x in xs where x.score > 20 select x
Enumerable.WhereArrayIterator<<>
f__AnonymousType0#35<string, int>> { \{ name = "baz", score = 40 },
\{ name = "oops", score = 30 } }
- select で複数のフィールドを指定するときは匿名型で指定する
- この場合、クエリ式で抽出された複数の値は匿名型に格納されて返される
リスト : クエリ式の簡単な使用例 (sample14/Program.cs)
using System;
using System.Linq;
class Test {
static void Main() {
var heightTable = new [] {
new {id=1, name="Ada", height=148.7, rank=1},
new {id=2, name="Alice", height=149.5, rank=2},
new {id=3, name="Carey", height=133.7, rank=3},
new {id=4, name="Ellen", height=157.9, rank=4},
new {id=5, name="Hanna", height=154.2, rank=1},
new {id=6, name="Janet", height=147.8, rank=2},
new {id=7, name="Linda", height=154.6, rank=3},
new {id=8, name="Maria", height=159.1, rank=4},
new {id=9, name="Miranda",height=148.2, rank=1},
new {id=10, name="Sara", height=153.1, rank=2},
new {id=11, name="Tracy", height=138.2, rank=3},
new {id=12, name="Violet", height=138.7, rank=4},
};
var xs = from x in heightTable where x.height < 140
orderby x.height select new {x.name, x.height};
foreach(var x in xs) {
Console.WriteLine("{0}, {1}", x.name, x.height);
}
var ys = from x in heightTable where x.height > 150
orderby x.height descending select new {x.name, x.height};
foreach(var y in ys) {
Console.WriteLine("{0}, {1}", y.name, y.height);
}
var gs = from x in heightTable
group new { x.name, x.height } by x.rank;
foreach(var g in gs) {
Console.Write("{0}: ", g.Key); // プロパティ Key でキーの値を取得できる
foreach(var p in g) {
Console.Write("({0}, {1}) ", p.name, p.height);
}
Console.WriteLine("");
}
}
}
$ dotnet run --project sample14
Carey, 133.7
Tracy, 138.2
Violet, 138.7
Maria, 159.1
Ellen, 157.9
Linda, 154.6
Hanna, 154.2
Sara, 153.1
1: (Ada, 148.7) (Hanna, 154.2) (Miranda, 148.2)
2: (Alice, 149.5) (Janet, 147.8) (Sara, 153.1)
3: (Carey, 133.7) (Linda, 154.6) (Tracy, 138.2)
4: (Ellen, 157.9) (Maria, 159.1) (Violet, 138.7)
●ファイル入出力
- テキストファイルの入出力は System.IO に定義されているクラス StreamReader, StreamWriter を使う
- テキストの読み込み
- int Read(); 1 文字 (char) 読み込み
- int Read(char[] buff, int index, int size); 配列 buff の index 番目以降に size 個の文字を読み込む
- string ReadLine(); 1 行読み込み
- string ReadToEnd(); 最後まで読み込む
- テキストの書き込み
- Write(), WriteLine() は Cosole.Write(), Console.WriteLine() と同じ
- Flush(); バッファに残っているデータを出力する
リスト : テキストファイルの連結 (cat/Program.cs)
using System;
using System.IO;
class Test {
static void Main(string[] args) {
foreach(string name in args) {
using (var s = new StreamReader(name)) {
string? buff;
while ((buff = s.ReadLine()) != null) {
Console.WriteLine(buff);
}
}
}
}
}
- コマンドライン引数は Main() の引数 string[] args に格納される
- ReadLine() はファイルの終了を検出すると null を返す
$ ls
Program.cs cat.csproj obj
$ dotnet run Program.cs
using System;
using System.IO;
class Test {
static void Main(string[] args) {
foreach(string name in args) {
using (var s = new StreamReader(name)) {
string? buff;
while ((buff = s.ReadLine()) != null) {
Console.WriteLine(buff);
}
}
}
}
}
リスト : テキストファイルのコピー (cp/Program.cs)
using System;
using System.IO;
class Test {
static void Main(string[] args) {
if (args.Length < 2) {
Console.WriteLine("arguments error");
} else {
using (var sIn = new StreamReader(args[0])) {
using (var sOut = new StreamWriter(args[1])) {
string? buff;
while ((buff = sIn.ReadLine()) != null) {
sOut.WriteLine(buff);
}
}
}
}
}
}
$ ls
Program.cs cp.csproj obj
$ dotnet run Program.cs test.txt
$ ls
Program.cs bin cp.csproj obj test.txt
$ cat test.txt
using System;
using System.IO;
class Test {
static void Main(string[] args) {
if (args.Length < 2) {
Console.WriteLine("arguments error");
} else {
using (var sIn = new StreamReader(args[0])) {
using (var sOut = new StreamWriter(args[1])) {
string? buff;
while ((buff = sIn.ReadLine()) != null) {
sOut.WriteLine(buff);
}
}
}
}
}
}
●バイナリファイルの入出力
- バイナリファイルの入出力は System.IO に定義されているクラス FileStream を使う
- FileStream はバイト単位で入出力を行う基本的なクラス
- 使い方はC言語の標準ライブラリ stdio.h に定義されている関数と似ている
- コンストラクタ
- FileStream 変数名 = new FileStream(string filename, FileMode mode);
- FIleMode
- Open, 既存のファイルをオープンする (存在しない場合は例外を送出)
- CreateNew, 新しいファイルを作成する (存在する場合は例外を送出)
- Truncate, 既存のファイルを 0 に切り詰めてオープンする (存在しない場合は例外を送出)
- Create, ファイルが存在する場合は Truncate と同じ、存在しない場合は CreateNew と同じ
- OpenOrCreate, ファイルが存在する場合はそれをオープンし、存在しない場合は新しいファイルを作成する
- Append, ファイルが存在する場合はデータを末尾に追加し、存在しない場合は新しいファイルを作成する
- 一般的な使い方では、データの読み込みに Open を、書き込みに Create を指定すればよい
- 基本的なアクセスメソッド
- int ReadByte(), 1 バイト読み込む (EOF に到達した場合は -1 を返す)
- int Read(byte [] buff, int offset, int cnt), cnt バイト読み込んで、buff + offset 以降に格納する
- void WriteByte(byte c), 1 バイト書き込む
- void Write(byte [] buff, int offset, int cnt), buff + offset からデータを cnt バイト書き込む
- バイト以外の単位で入出力を行うにはクラス BinaryReader, BinaryWriter を使う
- コンストラクタ
- BinaryReader 変数名 = new BinaryReader(Stream input);
- BinaryWriter 変数名 = new BinaryWriter(Stream output);
- 通常は FileStream で生成したストリームを引数に渡せばよい
- BiaryReader, BinaryWriter には様々な型でデータを読み書きするメソッドが用意されている
- このほかにも便利なクラスやメソッドが用意されている
- 詳細は C# のリファレンスを参照
リスト : バイナリファイルのコピー (copy/Program.cs)
using System;
using System.IO;
class Test {
static void Main(string[] args) {
if (args.Length < 2) {
Console.WriteLine("arguments error");
} else {
// using 宣言, C# 8.0 から利用可能
using var sIn = new FileStream(args[0], FileMode.Open);
using var sOut = new FileStream(args[1], FileMode.Create);
int c;
while ((c = sIn.ReadByte()) != -1) {
sOut.WriteByte((byte)c);
}
}
}
}
$ ls copy
Program.cs copy.csproj obj
$ cd copy
$ dotnet run Program.cs test.txt
$ ls
Program.cs bin copy.csproj obj test.txt
$ cat test.txt
using System;
using System.IO;
class Test {
static void Main(string[] args) {
if (args.Length < 2) {
Console.WriteLine("arguments error");
} else {
using var sIn = new FileStream(args[0], FileMode.Open);
using var sOut = new FileStream(args[1], FileMode.Create);
int c;
while ((c = sIn.ReadByte()) != -1) {
sOut.WriteByte((byte)c);
}
}
}
}
リスト : 乱数 (double) データの読み書き (sample23a/Program.cs)
using System;
using System.IO;
class Test {
static void Main(string[] args) {
{
using var s1 = new FileStream("test.dat", FileMode.Create);
using var s2 = new BinaryWriter(s1);
var r = new Random();
Console.WriteLine("----- write random data -----");
for (int i = 0; i < 10; i++) {
double x = r.NextDouble();
Console.WriteLine(x);
s2.Write(x);
}
}
using var s3 = new FileStream("test.dat", FileMode.Open);
using var s4 = new BinaryReader(s3);
Console.WriteLine("----- read random data -----");
for (int i = 0; i < 10; i++) {
double x = s4.ReadDouble();
Console.WriteLine(x);
}
}
}
$ dotnet run --project sample23a
----- write random data -----
0.004021309139920826
0.1083248895192791
0.1777151122192152
0.48411280991540695
0.49638943134720204
0.13615221317311488
0.6281558086366923
0.928947103389078
0.599753063548463
0.5356192240569689
----- read random data -----
0.004021309139920826
0.1083248895192791
0.1777151122192152
0.48411280991540695
0.49638943134720204
0.13615221317311488
0.6281558086366923
0.928947103389078
0.599753063548463
0.5356192240569689
●名前空間
- 名前空間は namespace 名前 { ... } で定義する
- 名前空間の中では複数のクラスを定義できる
- 名前空間は入れ子にすることもできる
- Name1 の中に Name2 を定義する場合、namespace Name1.Name2 { ... } と書くこともできる
リスト : 名前空間の使用例 (sample15/Program.cs)
using System;
using Foo;
using Foo.Bar;
namespace Foo {
namespace Bar {
class Baz {}
}
class Oops {}
}
class Test {
static void Main() {
var a = new Foo.Bar.Baz();
var b = new Foo.Oops();
Console.WriteLine("{0}", a);
Console.WriteLine("{0}", b);
var c = new Baz();
var d = new Oops();
Console.WriteLine("{0}", c);
Console.WriteLine("{0}", d);
}
}
$ dotnet run --project sample15
Foo.Bar.Baz
Foo.Oops
Foo.Bar.Baz
Foo.Oops
- 名前空間の中で定義したクラスは Name.ClassName でアクセスできる
- using Name; や using Name1.Name2; とすることで Name や Name1.Name2 を省略できる
- using で別名を付けることができる
- using 別名 = Name1.Name2.ClassName; // クラス名の別名
- using 別名 = Name1.Name2.Name3; // 名前空間の別名
- 別名を使う場合、ドット ( . ) だけではなく :: 演算子 (エイリアス修飾子) を使うことができる
- using System = Sys; Sys::Console.WriteLine("") のように使う
- 名前空間 global はルートを表す (System は global::System のこと)
●ライブラリの作成
$ dotnet new console -o sample16
テンプレート "コンソール アプリ" が正常に作成されました。
作成後の操作を処理しています...
/home/mhiroi/csharp/sample16/sample16.csproj を復元しています:
Determining projects to restore...
Restored /home/mhiroi/csharp/sample16/sample16.csproj (in 265 ms).
正常に復元されました。
$ cd sample16
$ dotnet new classlib -o hello
テンプレート "クラス ライブラリ" が正常に作成されました。
作成後の操作を処理しています...
/home/mhiroi/csharp/sample16/hello/hello.csproj を復元しています:
Determining projects to restore...
Restored /home/mhiroi/csharp/sample16/hello/hello.csproj (in 272 ms).
正常に復元されました。
$ ls hello
Class1.cs hello.csproj obj
リスト : 簡単なライブラリの作成 (sample16/hello/Class1.cs)
using System;
namespace hello;
public class HelloWorld
{
public static void Greeting() {
Console.WriteLine("Hello World");
}
}
- ライブラリを呼び出すプロジェクトでソリューションファイルを作成する
dotnet new sln
- ソリューションファイルにライブラリのパスを追加する
dotnet sln add path-name
- プログラムファイル Program.cs を書き換えて、プログラムを実行する
$ dotnet new sln
テンプレート "ソリューション ファイル" が正常に作成されました。
$ ls
Program.cs hello obj sample16.csproj sample16.sln
$ dotnet sln add ./hello/hello.csproj
プロジェクト `hello/hello.csproj` をソリューションに追加しました。
リスト : ライブラリの使用例 (sample16/Program.cs)
using hello;
class Test {
static void Main() {
HelloWorld.Greeting();
}
}
$ dotnet run
Hello World
●値呼びと参照呼び
- 一般に、関数呼び出しには「値呼び (call by value)」と「参照呼び (call by reference)」がある
- 近代的なプログラミング言語では「値呼び」が主流で、C# も値呼びである
- 受け取るデータを格納する変数 (仮引数) を用意する
- データを引数に代入する (データのコピーが行われる)
- 関数の実行終了後、引数を廃棄する
- 仮引数に対する更新が直ちに実引数にも及ぶような呼び出し方が「参照呼び」
- 参照呼びは、呼び出し先 (caller) から呼び出し元 (callee) の局所変数にアクセスできる
- C# の場合、仮引数と実引数にキーワード ref を付けると参照呼びと同様の動作になる
リスト : 値の交換 (sample17/Program.cs)
using System;
class Test {
static void Swap(ref int a, ref int b) {
int c = a;
a = b;
b = c;
}
static void Main() {
int a = 1, b = 2;
Console.WriteLine("a = {0}, b = {1}", a, b);
Swap(ref a, ref b);
Console.WriteLine("a = {0}, b = {1}", a, b);
}
}
$ dotnet run --project sample17
a = 1, b = 2
a = 2, b = 1
- ref を使う場合、変数は初期化しておく必要がある
- 基本的に C# の関数は一つの値しか返せない
- 複数の値 (多値) を返したい場合は参照呼びを使う
- このとき ref ではなくキーワード out を使うと変数の初期化が不要になる
リスト : out の使い方 (sample18/Program.cs)
using System;
class Test {
// 商と剰余を返す (Math.DivRem() と同じ)
static int DivRem(int x, int y, out int z) {
z = x % y;
return x / y;
}
static void Main() {
int r; // 初期化しなくてもよい
int q = DivRem(11, 4, out r);
Console.WriteLine("{0}, {1}", q, r);
}
}
$ dotnet run --project sample18
2, 3
- なお、C# 7 以降では「タプル (tuple)」が導入されたので、多値を返す場合はタプルを使ったほうが簡単
- タプルはあとで説明する
●データ型の判定
$ dotnet script
> class Foo {}
> class Bar : Foo {}
> class Baz : Bar {}
> Foo a = new Foo();
> Foo b = new Bar();
> Foo c = new Baz();
> a is Foo
true
> a is Bar
false
> b is Foo
true
> b is Bar
true
> b is Baz
false
> c is Foo
true
> c is Bar
true
> c is Baz
true
> a as Foo
Submission#0.Foo { }
> a as Bar // null
> b as Bar
Submission#1.Bar { }
> b as Baz // null
> c as Baz
Submission#2.Baz { }
- 演算子 typeof(データ型) は System.Type のインスタンスを返す
- メソッド GetType() でも System.Type のインスタンスを取得できる
- プロパティ Name でデータ型の名前 (文字列) を取得できる
> typeof(Foo).Name
"Foo"
> b.GetType()
[Submission#1+Bar]
> b.GetType().Name
"Bar"
> c.GetType().Name
"Baz"
●演算子の多重定義
//
// ratio/Program.cs : 有理数
//
// Copyright (C) 2016-2022 Makoto Hiroi
//
using System;
using System.Numerics;
struct Ratio : IComparable {
public BigInteger Numer { get; } // 分子
public BigInteger Denom { get; } // 分母
public Ratio(BigInteger p, BigInteger q) {
BigInteger a = BigInteger.GreatestCommonDivisor(p, q);
Numer = p / a;
Denom = q / a;
if (Denom < 0) {
Numer = - Numer;
Denom = - Denom;
}
}
public static Ratio operator +(Ratio a, Ratio b) {
return new Ratio(a.Numer * b.Denom + b.Numer * a.Denom, a.Denom * b.Denom);
}
public static Ratio operator -(Ratio a, Ratio b) {
return new Ratio(a.Numer * b.Denom - b.Numer * a.Denom, a.Denom * b.Denom);
}
public static Ratio operator *(Ratio a, Ratio b) {
return new Ratio(a.Numer * b.Numer, a.Denom * b.Denom);
}
public static Ratio operator /(Ratio a, Ratio b) {
return new Ratio(a.Numer * b.Denom, a.Denom * b.Numer);
}
// IComparable 用のメソッド
public int CompareTo(Object? obj) {
if (!(obj is Ratio)) return 1;
Ratio r = (Ratio)obj;
BigInteger a = Numer * r.Denom;
BigInteger b = r.Numer * Denom;
if (a == b) return 0;
else if (a < b) return -1;
else return 1;
}
public override bool Equals(object? obj) {
if (!(obj is Ratio)) return false;
Ratio r = (Ratio)obj;
return Numer == r.Numer && Denom == r.Denom;
}
// とても適当なので実際に使ってはいけない
public override int GetHashCode() {
return ((Numer << 7) ^ Denom).GetHashCode();
}
public static bool operator ==(Ratio a, Ratio b) {
return a.Equals(b);
}
public static bool operator !=(Ratio a, Ratio b) {
return !a.Equals(b);
}
public static bool operator <(Ratio a, Ratio b) {
return a.CompareTo(b) < 0;
}
public static bool operator <=(Ratio a, Ratio b) {
return a.CompareTo(b) <= 0;
}
public static bool operator >(Ratio a, Ratio b) {
return a.CompareTo(b) > 0;
}
public static bool operator >=(Ratio a, Ratio b) {
return a.CompareTo(b) >= 0;
}
// 表示
public override string ToString() {
return Numer.ToString() + "/" + Denom.ToString();
}
}
class Test {
static void Main() {
Ratio a = new Ratio(1, 2);
Ratio b = new Ratio(1, 3);
Ratio c = new Ratio(1, 4);
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine(a + b);
Console.WriteLine(a - b);
Console.WriteLine(a * b);
Console.WriteLine(a / b);
Console.WriteLine(c + b);
Console.WriteLine(c - b);
Console.WriteLine(c * b);
Console.WriteLine(c / b);
Console.WriteLine(a == c + c);
Console.WriteLine(a != c + c);
Console.WriteLine(a < b);
Console.WriteLine(a > b);
Console.WriteLine(a <= b);
Console.WriteLine(a >= b);
Console.WriteLine(b < c);
Console.WriteLine(b > c);
Console.WriteLine(b <= c);
Console.WriteLine(b >= c);
var buff = new Ratio[] {b, a, c};
Array.Sort(buff);
foreach(Ratio x in buff) {
Console.Write("{0} ", x);
}
Console.WriteLine("");
}
}
- 配列のメソッド Sort() や BinarySearch() などを使用するときは IComparable を継承する
- 実装するメソッドは x.CompareTo(y) だけ
- x < y, 負の整数 (int) を返す
- x == y, 0 を返す
- x > y, 正の整数を返す
- IComparable を継承しても比較演算子は自動的に定義されないので、自分で演算子を多重定義する必要がある
- 演算子 ==, != を多重定義するとき、メソッド Equals() をオーバーライドしないとワーニングがでる
- メソッド Equals() をオーバーライドするとき、メソッド GetHashCode() をオーバーライドしないとワーニングがでる
$ dotnet run --project ratio
1/2
1/3
1/4
5/6
1/6
1/6
3/2
7/12
-1/12
1/12
3/4
True
False
False
True
False
True
False
True
False
True
1/4 1/3 1/2
●null 許容型
- 通常、null は値型の変数に代入することはできないが、null も代入できる値型を「null 許容型」という
- 値型を T とすると、null 許容型は T? と記述する
- T? は System.Nullable<T> の省略形
- null 許容型は通常の値型と同様の計算を行うことができる
- 計算する値に null が含まれていると結果は null になる
- null の判定は演算子 ==, != のほかにプロパティ HasValue もある
- プロパティ Value は値を返すが、null であれば例外を送出する
- null 許容型の値を値型の変数に代入するときは演算子 ?? を使うと便利
- x が null 許容型の場合、int y = x ?? -1; は x に値があればその値を、null であれば -1 を y に代入する
$ dotnet script
> int? a = 10;
> int? b = null;
> a
10
> b // null
> int c = a ?? -1;
> c
10
> int d = b ?? -1;
> d
-1
> a + a
20
> a + b // null
>
●日付と時刻
- C# の場合、日付と時刻の操作は主に構造体 DateTime を使う
- 現在の日付と時刻は静的なプロパティ Now で取得できる
- 現在の日付だけでよければ静的なプロパティ Today を使う
- 主なプロパティ
- Year, Month, Day, Hour, Minute, Second, Millsecond
- Date : 日付だけを取得
- TimeOfDay : 時刻だけを取得
- DayOfWeek : 曜日を取得
- Ticks : タイマー刻み数を取得
- new DateTime() で新しいインスタンスを生成できる
- 構造体 TimeSpan は時間間隔を表す
- DateTime と DateTime の引き算は TimeSpan を返す
- 主なプロパティ
- Years, Months, Days, Hours, Minutes, Seconds, Millseconds, Ticks
- これらのプロパティは TimeSpane の該当する要素の値を返すだけ
- トータルでの値は次のプロパティを使う
- TotalDays, TotalHours, TotalMinutes, TotalSeconds, TotalMillseconds
$ dotnet script
> var a = new DateTime(2022, 1, 1);
> var b = new DateTime(2022, 2, 1);
> a
[2022/01/01 0:00:00]
> b
[2022/02/01 0:00:00]
> var c = b - a;
> c
[31.00:00:00]
> c.TotalDays
31
> c.TotalHours
744
> c.TotalSeconds
2678400
リスト : 簡単な実行時間の計測 (sample19/Program.cs)
using System;
class Test {
// 二重再帰
static long Fibo(long n) {
if (n < 2) return n;
return Fibo(n - 2) + Fibo(n - 1);
}
static void Main() {
var s = DateTime.Now;
Console.WriteLine(Fibo(42));
Console.WriteLine((DateTime.Now - s).TotalSeconds);
}
}
$ dotnet run --project sample19
267914296
4.3644275
$ dotnet run -c Release --project sample19
267914296
2.0055968
実行環境 : Ubunts 22.04 (WSL2), Intel Core i5-6200U 2.30GHz
●書式指定文字列
- メソッド ToString() はオブジェクトを文字列に変換する
- 自作のクラスで ToString() をオーバーライドすると、Console.Write() などの出力関数でオブジェクトを表示できる
- public overirde string ToString() { ... }
- データ型によっては ToSting() の引数に書式指定文字列を指定することができる
- 数値の主な書式指定子
- c. C : 通貨単位を表示
- d, D : 10 進数 (整数専用)
- x, X : 16 進数 (整数専用, x は a - f, X は A - F で表示)
- f, F : 固定小数点
- e, E : 指数
- g, G : 数値 (F, E のどちらか簡潔な方で表示)
- n, N : 桁区切り記号付き
- p, P : % 記号付き
- 詳細は 標準の数値書式指定文字列 | Microsoft Docs を参照
$ dotnet script
> 255.ToString("d")
"255"
> 255.ToString("x")
"ff"
> 255.ToString("X")
"FF"
> double x = 1.23456789;
> x.ToString("f")
"1.235"
> x.ToString("e")
"1.234568e+000"
> x.ToString("g")
"1.23456789"
> 123456789.ToString("n")
"123,456,789.000"
- 日付の主な書式指定子
- 小文字は短い形式、大文字は長い形式
- d, D : 日付
- t, T : 時刻
- f, F : 完全な日付と時刻
- g, G : 一般的な日付と時刻
- y, Y : 年月
- m, M : 月日
- 詳細は 標準の日時書式指定文字列 | Microsoft Docs を参照
> var a = new DateTime(2022, 2, 1, 12, 34, 56);
> a
[2022/02/01 12:34:56]
> a.ToString("d")
"2022/02/01"
> a.ToString("D")
"2022年2月1日火曜日"
> a.ToString("t")
"12:34"
> a.ToString("T")
"12:34:56"
> a.ToString("f")
"2022年2月1日火曜日 12:34"
> a.ToString("F")
"2022年2月1日火曜日 12:34:56"
> a.ToString("g")
"2022/02/01 12:34"
> a.ToString("G")
"2022/02/01 12:34:56"
- String.Format() や Console.Write() などでは複数のデータの書式を指定することができる
- これを「復号書式指定」という
- 復号書式指定の構文
{ index[,w][:指定子] }
- index は書式文字列のあとの引数の位置
- w はフィールド幅で、負数を指定すると右詰めされる
- 指定子のあとの数字を指定することもできる
- 整数の場合はフィールド幅 (右詰めで空欄には 0 が充填)
- 浮動小数点数の場合は精度 (小数点以降の桁数)
> Console.WriteLine("[{0,8}]", "hello")
[ hello]
> Console.WriteLine("[{0,-8}]", "hello")
[hello ]
> Console.WriteLine("[{0,-8:d}]", 123)
[123 ]
> Console.WriteLine("[{0,8:d}]", 1234)
[ 1234]
> Console.WriteLine("[{0,-8:d}]", 1234)
[1234 ]
> Console.WriteLine("[{0:d8}]", 1234)
[00001234]
> Console.WriteLine("{0:f}", 1.2345678)
1.23
> Console.WriteLine("{0:f7}", 1.2345678)
1.2345678
- 数値は 0 や # などを使って書式を作ることができる
- これを「カスタム数値書式指定文字列」という
- 詳細は カスタム数値書式指定文字列 | Microsoft Docs を参照
- カスタム日時書式指定文字列 | Microsoft Docs も用意されている
> Console.WriteLine("{0:###.###}", 123.45)
123.45
> Console.WriteLine("{0:###.000}", 123.45)
123.450
> Console.WriteLine("{0:0000.000}", 123.45)
0123.450