M.Hiroi's Home Page

C# Programming

お気楽C#プログラミング超入門

[ Home | C# ]

C# の基礎知識 (新機能編)

●タプル (tuple)

「タプル (tuple)」は C# 7 から導入された新しいデータ型を定義する機能です。関数型言語 (SML/NJ, OCaml, Haskell など) では基本的な機能の一つです。今回は C# のタプルについて説明します。

●タプルの定義

C# の場合、タプルのデータ型はカッコ ( ) の中に型 type をカンマ ( , ) で区切って並べたものになります。

(type1, type2, ...) varName;
(type1, type2, ...) varName = (item1, item2, ...);

後者のように初期値を指定することもできます。また、次のように var を使って C# にデータ型を推論させることもできます。

var varName = (item1, item2, ...);

タプルの要素は varName.Item1 のように "Item + 番号" でアクセスすることができます。タプルの場合、番号は 1 から数えるので、先頭要素が Item1 になります。なお、C# のタプルは mutable なデータ構造なので、要素を書き換えることができます。関数型言語や Python のタプルのように immutable ではありません。ご注意くださいませ。

簡単な例を示しましょう。

$ dotnet script
> var a = (1, 2);
> a
(1, 2)
> a.Item1
1
> a.Item2
2
> a.Item1 = 10;
10
> a.Item2 = 20;
20
> a
(10, 20)
> a is (int, int)
true

> var b = (10, 20.5);
> b
(10, 20.5)
> b is (int, double)
true

> var c = (1, 2.5, "foo");
> c
(1, 2.5, foo)
> c is (int, double, string)
true
> var d = (1 + 2, 3 * 4);
> d
(3, 12)

変数 a のタプル (1, 2) は整数を 2 つ持っていて、型は (int, int) になります。変数 b のタプル (10, 20.5) は整数と実数なので (int, double) になります。変数 c のタプル (1, 2.5, "foo") は (int, double, string) になります。また、カッコの中に式を書くと、それを評価した値がタプルの要素になります。

タプルは入れ子にしてもかまいません。次の例を見てください。

> var e = ((1, 2), 3);
> e
((1, 2), 3)
> e.Item1
(1, 2)
> e.Item1.Item1
1
> e is ((int, int), int)
true

> var f = (1, (2, 3));
> f
(1, (2, 3))
> f.Item2
(2, 3)
> f.Item2.Item2
3
> f is (int, (int, int))
true

変数 e のタプルは、第 1 要素が (int, int) のタプルで、第 2 要素が int です。これを ((int, int), int) と表します。変数 f の組は、第 1 要素が int で第 2 要素が (int, int) の組になります。これを (int, (int, int)) と表します。どちらのタプルも 3 つの整数が含まれていますが、型は異なることに注意してください。

●タプルの代入と分解

タプルには「代入と分解」という機能があります。これを使うと、タプルの要素を変数に簡単に取り出すことができます。

(type1 name1, type2 name2, ...) = (item1, item2, ...);
var (name1, name2, ...) = (item1, item2, ...);

タプル内で変数を宣言し、そこにタプルを代入します。すると、タプルの要素が対応する変数にセットされます。var を使ってデータ型を推論させることもできます。既に定義されている変数に代入する場合は、データ型の宣言を省略して変数名だけをタプルに格納します。タプルの代入と分解は foreach でも行うことができます。

簡単な例を示しましょう。

> a
(10, 20)
> var (x, y) = a;
> x
10
> y
20

> e
((1, 2), 3)
> var ((x1, y1), z1) = e;
> x1
1
> y1
2
> z1
3

> f
(1, (2, 3))
> var (x2, (y2, z2)) = f;
> x2
1
> y2
2
> z2
3

> var xs = new (string, int)[] {("foo", 10), ("bar", 20), ("baz", 30), ("oops", 40)};
> xs
ValueTuple<string, int>[4] { (foo, 10), (bar, 20), (baz, 30), (oops, 40) }
> foreach (var (name, value) in xs) { Console.WriteLine("{0} {1}", name, value); }
foo 10
bar 20
baz 30
oops 40

●Deconstruct

クラスや構造体にメソッド Deconstruct を実装すると、フィールドの値をタプルに取り出すことができます。

public void Deconstruct(out type1 name1, out type2 name2, ...) {
  name1 = filedValue1;
  name2 = filedValue2;
  ...;
}

out が示す変数 name にフィールドの値 fieldValue を代入するだけです。Deconstruct は引数の個数が異なれば、いくつでも定義することができます。

簡単な例を示しましょう。

リスト : クラス Pair

class Pair<U, V> {
  U fst;
  V snd;
  public Pair(U a, V b) {
    fst = a;
    snd = b;
  }
  public U Fst() { return fst; }
  public V Snd() { return snd; }
  public void Deconstruct(out U a, out V b) {
    (a, b) = (fst, snd);
  }
}
$ dotnet script
> #load "pair.csx"
> var a = new Pair <int, int>(1, 2);
> a.Fst()
1
> a.Snd()
2
> var (x, y) = a;
> x
1
> y
2

Pair は 2 つの値を保持するクラスです。C# 7 からタプルが導入されたので、このようなクラスを作らなくても済むようになりました。Pair にメソッド Deconstruct を実装すると、var (x, y) = a; のように Pair のインスタンスをタプルに代入して、その要素にフィールドの値を取り出すことができます。

●タプルのフィールド名

タプルの要素には名前を付けることができます。これを「フィールド名」といいます。フィールド名を付けると、番号ではなく名前で要素にアクセスすることができます。

(type1 name1, type2 name2, ...) varName;
(type1 name1, type2 name2, ...) varName = (item1, item2, ...);
var varName = (name1: item1, name2: item2, ...);

データ型 type のあとにフィールド名 name を付けます。右辺の初期値でフィールド名を指定する場合は、値 item の前に name + ":" を付けます。簡単な例を示しましょう。

> var a = (x: 100, y: 200);
> a
(100, 200)
> a.x
100
> a.y
200
> a.x = -100;
> a.y = -200;
> a
(-100, -200)

> (int foo, int bar) b = a;
> b.foo
-100
> b.bar
-200
> b.x
=> エラー

> var c = a;
> c
(100, 200)
> c.x
100
> c.y
200

変数 a のタプルはフィールド名 x, y が定義されているので、a.x, a.y で要素にアクセスすることができます。フィールド名を定義したタプル (変数 b) に a のタプルを代入すると、元のタプルのフィールド名は使用できなくなります。変数 c のタプルのように、フィールド名を定義しなければ元のタプルのフィールド名を使用することができます。

●タプルの等値性

データ型が等しいタプル同士は演算子 ==, != で等値を判定することができます。演算子 == はタプルのデータ型が等しくて、要素の値が等しいとき真を返します。データ型が異なるタプルは演算子 ==. != で比較することはできません。エラーになります。

簡単な例を示しましょう。

> (1,2,3) == (1,2,3)
true
> (1,2,3) == (1,2,1)
false
> (1,(2,3)) == (1,(2,3))
true
> (1,(2,3)) == ((1,2),3)
=> エラー

最後の例は、左辺の第 1 要素が int で、右辺の第 1 要素が (int, int) です。データ型が異なるので比較することはできずエラーになります。なお、数のように型変換により等値を判定できる場合、データ型が異なっているタプルでも比較することができます。

> (1, 1.0) == (1, 1)
true
> (1, 1.2) == (1, 1)
false

●タプルで多値を返す

一般に、関数が返す値は一つだけですが、タプルを使うと複数の値を簡単に返すことができます。これを「多値」といいます。簡単な例を示しましょう。次のリストを見てください。

リスト : タプルの簡単な使用例 (sample20/Program.cs)

using System;

class Test {
  // 商と剰余を返す
  static (int, int) DivRem(int x, int y) {
    return (x / y, x % y);
  }

  // 最小値と添字を返す
  static (double, int) FindMin(double[] a) {
    var p = 0;
    var m = a[0];
    for (int i = 1; i < a.Length; i++) {
      if (a[i] < m) {
        p = i;
        m = a[i];
      }
    }
    return (m, p);
  }

  // 最大値と添字を返す
  static (double, int) FindMax(double[] a) {
    var p = 0;
    var m = a[0];
    for (int i = 1; i < a.Length; i++) {
      if (a[i] > m) {
        p = i;
        m = a[i];
      }
    }
    return (m, p);
  }

  static void Main() {
    double[] a = new double[] {5.0, 6.0, 4.0, 1.0, 2.0, 3.0, 9.0, 8.0, 7.0};
    var (m, p) = FindMin(a);
    Console.WriteLine("min {0}, {1}", m, p);
    (m, p) = FindMax(a);
    Console.WriteLine("max {0}, {1}", m, p);
    var (q, r) = DivRem(11, 4);
    Console.WriteLine("{0}, {1}", q, r);
  }
}
$ dotnet run --project sample20
min 1, 3
max 9, 6
2, 3

プログラムは簡単なので説明は不要でしょう。このように、タプルを使うと多値を返すことや、それを受け取ることも簡単にできます。

●マスターマインドの解法

それでは簡単な例題として、拙作のページ 簡単なプログラム : マスターマインド で作成した解法プログラムをタプルを使って書き直してみましょう。プログラムと実行結果を示します。

リスト : マスターマインドの解法 (master1.csx)

const int SIZE = 4;

// 質問結果をタプル (bulls, cows, code) に格納する
var query = new List<(int, int, int[])>();
var perms = new List<int[]>();

// 順列の生成
void MakePerm(int n, bool[] used, int[] ps) {
  if (n == SIZE)
    perms.Add((int[])ps.Clone());
  else
    for (int i = 0; i < 10; i++) {
      if (used[i]) continue;
      used[i] = true;
      ps[n] = i;
      MakePerm(n + 1, used, ps);
      used[i] = false;
    }
}

// Bulls を数える
int CountBulls(int[] xs, int[] ys) {
  int b = 0;
  for (int i = 0; i < SIZE; i++) {
    if (xs[i] == ys[i]) b++;
  }
  return b;
}

// 同じ数字を数える
int CountSameNumber(int[] xs, int[] ys) {
  int c = 0;
  foreach(int x in xs) {
    foreach(int y in ys) if (x == y) c++;
  }
  return c;
}

// 矛盾しないかチェックする
bool CheckQuery(int[] code) {
  // タプルの要素を変数 bulls, cows, xs に取り出す
  foreach(var (bulls, cows, xs) in query) {
    int b = CountBulls(xs, code);
    int c = CountSameNumber(xs, code) - b;
    if (b != bulls || c != cows) return false;
  }
  return true;
}

// コードの表示
void PrintCode(int[] code) {
  foreach(int x in code) Console.Write("{0} ", x);
}

// マスターマインドの解法
void Solver(int[] answer) {
  query.Clear();
  foreach(int[] code in perms) {
    if (CheckQuery(code)) {
      int b = CountBulls(answer, code);
      int c = CountSameNumber(answer, code) - b;
      query.Add((b, c, code));
      Console.Write("{0}: ", query.Count);
      PrintCode(code);
      Console.WriteLine(" bulls = {0}, cows = {1}", b, c);
      if (b == 4) {
        Console.WriteLine("Good Job!!");
        break;
      }
    }
  }
}

void main() {
  MakePerm(0, new bool[10], new int[SIZE]);
  Solver(new int[SIZE] {9,8,7,6});
  Solver(new int[SIZE] {9,4,3,1});
}

// 実行
main();
$ dotnet script master1.csx
1: 0 1 2 3  bulls = 0, cows = 0
2: 4 5 6 7  bulls = 0, cows = 2
3: 5 4 8 9  bulls = 0, cows = 2
4: 6 7 9 8  bulls = 0, cows = 4
5: 8 9 7 6  bulls = 2, cows = 2
6: 9 8 7 6  bulls = 4, cows = 0
Good Job!!
1: 0 1 2 3  bulls = 0, cows = 2
2: 1 0 4 5  bulls = 0, cows = 2
3: 2 3 5 4  bulls = 0, cows = 2
4: 3 4 0 6  bulls = 1, cows = 1
5: 3 5 6 1  bulls = 1, cows = 1
6: 6 5 0 2  bulls = 0, cows = 0
7: 7 4 3 1  bulls = 3, cows = 0
8: 8 4 3 1  bulls = 3, cows = 0
9: 9 4 3 1  bulls = 4, cows = 0
Good Job!!

●パターンマッチング

「パターンマッチング (pattern matching)」は C# 7 以降に追加された新しい機能です。パターンマッチングは関数型言語では基本的な機能の一つで、データと型を照合 (マッチング) し、マッチングに成功した節を選択して実行します。C# では is 演算子、switch 文、switch 式で使用することができます。

●switch 式

パターンマッチングは C# 8 から導入された switch 式で使うと便利です。switch 式の構文を以下に示します。

expr_0 switch {
  pattern_1 => expr_1,
  pattern_2 => expr_2,
  ...
  pattern_n => expr_n
}

switch 文とは逆に、switch 式は式 expr_0 の後ろに switch { ... } を記述します。{ ... } の中のカンマ ( , ) で区切られた部分をマッチング節 (matching clause) といいます。switch 式は式 expr_0 の評価結果とパターンを照合し、マッチングする節を選択して実行します。

たとえば、expr_0 の結果と pattern_1 がマッチングした場合、expr_1 を評価してその結果が switch 式の返り値になります。マッチングしない場合は次のパターンを調べます。マッチングするパターンが見つからない場合はエラーになります。なお、一度マッチング節が選択された場合、それ以降の節は選択されません。また、式 1 から式 n の結果は同じ型でなければいけません。ご注意ください。

簡単な例を示しましょう。switch 式を使って階乗を求める関数 fact を定義すると次のようになります。

> int fact(int n) => n switch { 0 => 1, var m => m * fact(m - 1) };
> for (int i = 0; i < 13; i++) { Console.WriteLine("{0}", fact(i)); }
1
1
2
6
24
120
720
5040
40320
362880
3628800
39916800
479001600

関数を定義するとき、本体が式だけの場合は次のように定義することができます。

返り値の型 関数名(仮引数の型 仮引数, ...) => 式

パターンが定数の場合、同じ値の変数とマッチングします。これを「定数パターン」といいます。最初の定義はパターンが 0 なので、n が 0 の場合にマッチングします。これは if (n == 0) return 1; と同じ処理です。

パターンが "var 変数" の場合はどんな値とでもマッチングします。これを「var パターン」といいます。この場合、変数にはその値が代入されます。したがって、n が 0 以外の場合は 2 番目のパターンと一致し、変数 m の値は n になります。ここで再帰呼び出しが行われます。

パターンマッチングを使うときは、マッチング節を定義する順番に気をつけてください。fact の場合、最初に変数パターンを定義すると、引数が 0 の場合でもマッチングするので、パターン 0 のマッチング節が実行されることはありません。特定のパターンから定義するように注意してください。

fact の場合、変数 n のデータ型は int なので、var のかわりに int を指定することができます。

int m => m * fact(m - 1)

"データ型 変数" を「宣言パターン」といいます。マッチングする値のデータ型がパターンで指定した型と一致したとき、マッチングは成功して変数にはその値が代入されます。変数を省略してデータ型だけ指定することもできます。これを「型パターン」といいます。これはデータ型によって処理を分岐するときに便利です。

ところで、変数 m は n と同じ値なので、パターンにアンダーバー ( _ ) を使って次のように定義してもかまいません。

_ => n * fact (n - 1)

_ を「破棄パターン」といいます。_ はどんな値ともマッチングしますが、マッチングした値は捨てられます。

●タプルパターン

パターンはタプルを使って記述することができます。これを「タプルパターン」といいます。タプルの要素にパターンを記述することができるので、いろいろなパターンを表現することができます。簡単な例を示しましょう。

$ dotnet script
> string rgb(bool r, bool g, bool b) => (r, g, b) switch {
*   (false, false, false) => "black",
*   (false, false, true)  => "blue",
*   (false, true,  false) => "green",
*   (false, true,  true)  => "cyan",
*   (true,  false, false) => "red",
*   (true,  false, true)  => "magenta",
*   (true,  true,  false) => "yellow",
*   (true,  true,  true)  => "white",
* };
> rgb(false, true, false)
"green"
> rgb(true, true, false)
"yellow"
> rgb(true, false, false)
"red"

関数 rgb は三原色 (赤、緑、青) の有無から 8 種類の色を文字列で返します。引数 r, g, b をタプルに格納し、switch 式でその値とパターンを照合します。パターンはタプルで、各要素は true と false の定数パターンになります。

もう一つ簡単な例として、二分木で表した数式 (構文木) を計算するプログラムを作りましょう。拙作のページ 簡単なプログラム 式の計算 (構文木の構築) を簡略化したものです。最初に、構文木を表すクラスを定義します。

リスト : 構文木 (expr.csx)

// 演算子
enum Op {Add, Sub, Mul, Div};

// 構文木
abstract class Expr { }

// 二項演算子
class Node : Expr {
  public Op Operator { get; set; }
  public Expr Left { get; set; }
  public Expr Right { get; set; }

  public Node(Op op, Expr eLeft, Expr eRight) {
    Operator = op;
    Left = eLeft;
    Right = eRight;
  }
}

// 数値 (木構造の葉に相当)
class Leaf : Expr {
  public double Value { get; set; }
  public Leaf(double val) { Value = val; }
}

式を計算する関数 calc0() は、宣言パターンを使うと次のようになります。

リスト : 式の計算

double calc0(Expr e) => e switch {
  Leaf l => l.Value,
  Node n => n.Operator switch {
    Op.Add => calc0(n.Left) + calc0(n.Right),
    Op.Sub => calc0(n.Left) - calc0(n.Right),
    Op.Mul => calc0(n.Left) * calc0(n.Right),
    Op.Div => calc0(n.Left) / calc0(n.Right),
    _      => 0    // ワーニングを消すため
  },
  _ => 0           // ワーニングを消すため
};

switch 式で引数 e の型をチェックします。Leaf であれば l.Value で数値を返します。Node であれば、n.Operator で処理を振り分けます。左部分木 n.Left と右部分木 n.Right の値を calc0 で求め、演算子 Operator に合わせた計算を行います。

簡単な実行例を示しましょう。

リスト : 簡単なテスト

var e1 = new Node(Op.Add, new Leaf(1), new Leaf(2));  // 1 + 2
var e2 = new Node(Op.Sub, new Leaf(3), new Leaf(4));  // 3 - 4
var e3 = new Node(Op.Mul, e1, e2);                    // (1 + 2) * (3 - 4)
var e4 = new Node(Op.Div, e2, e1);                    // (3 - 4) / (1 + 2)
Console.WriteLine("{0}", calc0(e1));
Console.WriteLine("{0}", calc0(e2));
Console.WriteLine("{0}", calc0(e3));
Console.WriteLine("{0}", calc0(e4));
$ dotnet script expr.csx
3
-1
-3
-0.3333333333333333

クラス Node に Deconstruct を追加して、タプルパターンを使うと次のようになります。

リスト : Node の Deconstruct

  public void Deconstruct(out Op o, out Expr l, out Expr r) {
    o = Operator;
    l = Left;
    r = Right;
  }
リスト : タプルパターン

double calc1(Expr e) => e switch {
  Leaf l => l.Value,
  Node (Op op, Expr l, Expr r) => op switch {
    Op.Add => calc1(l) + calc1(r),
    Op.Sub => calc1(l) - calc1(r),
    Op.Mul => calc1(l) * calc1(r),
    Op.Div => calc1(l) / calc1(r),
    _      => 0   // ワーニングを消すため
  },
  _ => 0          // ワーニングを消すため
};

引数 e の型が Node の場合、Deconstruct で e をタプルに分解してパターンと照合します。これで演算子と各部分木は変数 op, l, r にセットされます

●プロパティパターン

クラスや構造体にメソッド Deconstuct がなくても、プロパティが定義されていれば、それを使ってパターンマッチングを行うことができます。これを「プロパティパターン」といいます。プロパティパターンの構文を示します。

{propertyName1: pattern1, propertyName1: pattern1, ...}

プロパティパターンは { } の中にプロパティ名 propertyName とパターン pattern を記述します。プロパティとパターンはコロン ( : ) で区切ります。

簡単な例を示しましょう。構文木を計算する calc() をプロパティパターンを使って書き直すと次のようになります。

リスト : プロパティパターン

double calc2(Expr e) => e switch {
  Leaf {Value: var v} => v,
  Node {Operator: var op, Left: var l, Right: var r} => op switch {
    Op.Add => calc2(l) + calc2(r),
    Op.Sub => calc2(l) - calc2(r),
    Op.Mul => calc2(l) * calc2(r),
    Op.Div => calc2(l) / calc2(r),
    _ => 0 // ワーニングを消すため
  },
  _ => 0   // ワーニングを消すため
};

引数 e の型が Leaf の場合、プロパティ Value とマッチングして、その値は変数 v にセットされます。Node の場合、プロパティ Operator, Left, Right とマッチングして、それぞれの値は変数 op, l, r にセットされます。

●ケースガードとリレーショナルパターン

パターンマッチングだけでは選択する条件を表すことができない場合、キーワード when を使うと便利です。when の構文を示します。

pattern when condition => expr

このようなマッチング節を「ガード付き節」とか「ケースガード」といいます。パターン pattern との照合に成功して、かつ when の条件式 condition が真を返す場合に限り、その節が選択されます。条件を満たさない場合は、それ以降のマッチング節を調べます。

簡単な例題として、階乗を求める関数 fact において、引数 n が 0 より小さい場合はエラー送出する処理を追加してみましょう。

$ dotnet script
> long fact(long n) => n switch {
*   0 => 1,
*   _ when n > 0 => n * fact(n - 1),
*   _ => throw new Exception("fact: domain error")
* };
> fact(10)
3628800
> fact(20)
2432902008176640000
> fact(-1)
System.Exception: System.Exception: fact: domain error
  + Submission#12.fact(long) 場所  : 4

引数 n が 0 の場合は 1 を返します。次の節はケースガードがあるので、n > 0 のときに選択されます。n < 0 のときは最後の節が選択され、throw によってエラーが送出されます。

なお、数値の比較は C# 9 以降に導入された「リレーショナルパターン」を使うと簡単です。リレーショナルパターンの構文を示します。

operator const => expr

operator は演算子 <, >, <=, >= を指定します。左辺はマッチングした数値になり、右辺は定数式 const を指定します。演算子の条件を満たしたとき、その節が選択されます。

階乗 fact のプログラムを書き直すと次のようになります。

$ dotnet script
> long fact(long n) => n switch {
*   < 0 => throw new Exception("fact: domain error"),
*   0   => 1,
*   _   => n * fact(n - 1)
* };
> fact(10)
3628800
> fact(20)
2432902008176640000
> fact(-1)
System.Exception: System.Exception: fact: domain error
  + Submission#0.fact(long) 場所  : 2

最初の節で、引数 n が負かチェックします。そうであればエラーを送出します。次の節で、n が 0 ならば 1 を返します。最後の節で、関数 fact を再帰呼び出しします。

●論理パターン

パターンマッチングは、パターン連結子を使って複数のパターンを組み合わせることができます。これを「論理パターン」といい、C# 9 で追加された機能です。以下にパターン連結子を示します。

基本的には論理演算子 (!, &&, ||) と同じで、not, and, or を組み合わせることもできます。優先順位も同じく (高) not -> and -> or (低) で、カッコで優先順位を明示することができます。

数値の範囲チェックは、リレーショナルパターンと論理パターンを使うと簡単に行うことができます。たとえば、階乗を計算する関数 fact() において、引数 n と返り値のデータ型が long であれば、n が 20 を超えるとオーバーフローしてしまいます。そこで、n が負の場合と 20 を超える場合はエラーを送出することにします。

プログラムと実行結果を示します。

$ dotnet script
> long fact(long n) => n switch {
*   < 0 or > 20 => throw new Exception("fact: domain error"),
*   0 => 1,
*   _ => n * fact(n - 1)
* };
> fact(10)
3628800
> fact(20)
2432902008176640000
> fact(30)
System.Exception: System.Exception: fact: domain error
  + Submission#0.fact(long) 場所  : 2
> fact(-1)
System.Exception: System.Exception: fact: domain error
  + Submission#0.fact(long) 場所  : 2

●switch 文とパターン

C# 7 以降、switch 文では case の後ろにパターンを記述することができるようになりました。

switch(式) { case pattern [when condition]: 処理1; ...; break; ... default: 処理; ... }

ケースガードも指定することができます。簡単な例として、階乗を求める関数 fact() を switch 文を使って書き直して見ましょう。

リスト : 階乗

long fact(long n) {
  switch(n) {
    case < 0 or > 20:
      throw new Exception("fact: domain error");
    case 0:
      return 1;
    default:
      return n * fact(n - 1);
  }
}

実行結果は今までと同じなので省略します。

●is 演算子とパターン

is 演算子は左辺の値が右辺のデータ型と等しければ真を返し、そうでなければ偽を返します。C# 7 以降、is 演算子の右辺にはパターンを記述できるようになりました。

value is pattern

is 演算子はパターン pattern と照合に成功すると真を返します。たとえば、pattern が宣言パターンであれば、データ型と一致すると、変数に value がセットされます。ようするに、型チェックと型変換を同時に行ってくれるわけです。また、value is null や value is not null で、value の null 判定を行うことができます。

簡単な例を示します。

> int? a = null;
> a is null
true
> a is not null
false
> int? b = 1;
> b is null
false
> b is not null
true
> if (b is int c) {Console.WriteLine("int {0}", c);}
int 1

●レコード型

「レコード (record) 型」は immutable なデータ構造を扱うのに便利なデータ型です。レコード型は次の二種類があります。

C# は record をクラスに、record struct を構造体にコンパイルするので、record は参照型に、record struct は値型になります。けっきょく、レコード型はクラスや構造体と基本的には同じなのですが、レコード型を定義するとき immuutable なデータ型に適したプロパティやメソッドが自動的に生成されるところが異なります。

●レコード型の定義

レコード型はクラスや構造体と同じように定義することもできますが、次に示す構文を使うと簡単です。

record レコード名(型 引数名, ...);
record struct レコード名(型 引数名, ...);

以下に示すプロパティとメソッドが自動的に生成もしくはオーバーライドされます。

他のメソッドを定義する、あるいはメソッドをオーバーライドしたいときは、次のように { ... } の中で定義します。

record [struct] レコード名(型 引数名, ...) {
  // メソッドなどの定義
  ...
}

簡単な例を示しましょう。

> record Foo(int A, int B) {
*   public int Add() { return A + B; }
*   public int Sub() { return A - B; }
* }
> var a = new Foo(10, 5);
> a
[Foo { A = 10, B = 5 }]
> a.A
10
> a.B
5
> a.Add()
15
> a.Sub()
5

レコード Foo のインスタンスはクラスと同様に new を使って生成します。プロパティ A, B が自動的に生成され、独自に定義したメソッド Add(), Sub() も呼び出すことができます。

●レコード型の等値判定

レコード型は等値の判定がクラスとは異なります。演算子 == で右辺と左辺のデータを比較するとき、左右ともに参照型データであれば、そのデータのアドレスを比較するのがデフォルトの動作です。つまり、インスタンスのフィールド変数の値が等しくても、異なるインスタンスであれば false になります。

等値の判定にはメソッド obj1.Equals(obj2) で行うこともできます。メソッド Equals のデフォルトの動作は、obj1, obj2 が参照型データであればそのアドレスを比較し、値型データであればその値 (もしくはフィールド変数の値) を比較します。レコード型の場合、演算子 == とメソッド Equals の動作は、どちらもフィールド変数の値で比較するように変更されます。

簡単な例を示しましょう。

リスト : クラス、構造体、レコード型の簡単な例 (pair.csx)

class PairC<U, V> {
  public U Fst { get; }
  public V Snd { get; }
  public PairC(U a, V b) {
    Fst = a;
    Snd = b;
  }
}

struct PairS<U, V> {
  public U Fst { get; }
  public V Snd { get; }
  public PairS(U a, V b) {
    Fst = a;
    Snd = b;
  }
}

record PairRC<U, V>(U Fst, V Snd);
record struct PairRS<U, V>(U Fst, V Snd);
$ dotnet script
> #load "pair.csx"
> var a1 = new PairC<int,int>(1, 2);
> var b1 = new PairC<int,int>(1, 2);
> a1 == b1
false
> a1 == a1
true
> a1.Equals(b1)
false
> a1.Equals(a1)
true

> var a2 = new PairS<int,int>(1,2);
> var b2 = new PairS<int,int>(1,2);
> var c2 = new PairS(1,3);
> a2 == b2
=> エラー
> a2.Equals(b2)
true
> a2.Equals(a2)
true
> a2.Equals(c2)
false

> var a3 = new PairRC<int,int>(1,2);
> a3
[PairRC { Fst = 1, Snd = 2 }]
> var b3 = new PairRC<int,int>(1,2);
> var c3 = new PairRC<int,int>(1,3);
> a3 == b3
true
> a3 == a3
true
> a3 == c3
false
> a3.Equals(b3)
true
> a3.Equals(a3)
true
> a3.Equals(c3)
false

> var a4 = new PairRS<int,int>(1,2);
> a4
[PairRS { Fst = 1, Snd = 2 }]
> var b4 = new PairRS<int,int>(1,2);
> var c4 = new PairRS<int,int>(1,3);
> a4 == b4
true
> a4 == a4
true
> a4 == c4
false
> a4.Equals(b4)
true
> a4.Equals(a4)
true
> a4.Equals(c4)
false

クラス PairC の場合、演算子 == とメソッド Equals は 2 つのインスタンスが同じとき true を返します。構造体 PairS の場合、Equals で比較するとフィールド変数の値が等しいときに true を返します。PairRC と PairRS の場合、フィールド変数の値が同じであれば、演算子 == とメソッド Equals は true を返します。

●レコード型とパターンマッチング

レコード型はプロパティとメソッド Deconstruct が実装されるので、タプルの「代入と分解」や「パターンマッチング」を使用することができます。簡単な例を示しましょう。

> var a = new PairRC<int,double>(1, 1.2345);
> a
[PairRC { Fst = 1, Snd = 1.2345 }]
> var (b, c) = a;
> b
1
> c
1.2345
> if (a is PairRC<int,double> {Fst: var x, Snd: var y}) { Console.WriteLine("{0} {1}", x, y); }
1 1.2345

●with 式

C# はフィールド変数の値を変更した新しいレコードを with 式で生成することができます。

recordObj with {Pname1 = newValue, ...} => newRecordObj

with 式はレコード recordObj のプロパティ Pname の値を newValue に変更した新しいレコード newRecordObj を返します。{ ... } の中を空にすると、recordObj をコピーした新しいレコードを返します。

簡単な例を示しましょう。

> a
[PairRC { Fst = 1, Snd = 1.2345 }]
> var b = a with { Fst = 10 };
> b
[PairRC { Fst = 10, Snd = 1.2345 }]
> a
[PairRC { Fst = 1, Snd = 1.2345 }]

> var c = a with { Fst = 10, Snd = 12.345 };
> c
[PairRC { Fst = 10, Snd = 12.345 }]
> var d = a with {};
> d
[PairRC { Fst = 1, Snd = 1.2345 }]
> a == d
true

元のレコード a の値を書き換えることはありません。

●レコード型の継承

レコード (record class) はクラスにコンパイルされるので、継承を利用することができます。

record 名前(型 引数名, ...) : superRecord(...);

レコード定義のあとコロン ( : ) で区切り、その後ろでスーパークラスのコンストラクタ superRecord を呼び出します。ただし、レコードが継承できるのはレコードだけです。クラスを継承することはできません。同様に、クラスはレコードを継承することはできません。

簡単な例を示しましょう。

$ dotnet script
> record Foo(int A);
> record Bar(int A, double B) : Foo(A);
> record Baz(int A, double B, string C) : Bar(A, B);
> var a = new Foo(123);
> a
[Foo { A = 123 }]
> var b = new Bar(456, 7.89);
> b
[Bar { A = 456, B = 7.89 }]
> var c = new Baz(789, 1.2345, "hello");
> c
[Baz { A = 789, B = 1.2345, C = hello }]
> a.A
123
> b.A
456
> c.A
789
> c.B
1.2345
> c.C
"hello"

このように、レコード (record class) でも簡単に継承を利用することができます。

●構文木の計算

最後に簡単な例題として、数式 (構文木) を計算するプログラムを作りましょう。プログラムは次のようになります。

リスト : 数式 (構文木) の計算 (expr1.csx)

// 演算子
enum Op {Add, Sub, Mul, Div};

// 構文木
abstract record Expr;

// 二項演算子
record Node(Op Operator, Expr Left, Expr Right) : Expr;

// 数値 (木構造の葉に相当)
record Leaf(double Value) : Expr;

// 式の計算
double calc(Expr e) => e switch {
  Leaf {Value: var v} => v,
  Node {Operator: var op, Left: var l, Right: var r} => op switch {
    Op.Add => calc(l) + calc(r),
    Op.Sub => calc(l) - calc(r),
    Op.Mul => calc(l) * calc(r),
    Op.Div => calc(l) / calc(r),
    _ => 0 // ワーニングを消すため
  },
  _ => 0   // ワーニングを消すため
};

var e1 = new Node(Op.Add, new Leaf(1), new Leaf(2));
var e2 = new Node(Op.Sub, new Leaf(3), new Leaf(4));
var e3 = new Node(Op.Mul, e1, e2);
var e4 = new Node(Op.Div, e2, e1);
Console.WriteLine("{0}", calc(e1));
Console.WriteLine("{0}", calc(e2));
Console.WriteLine("{0}", calc(e3));
Console.WriteLine("{0}", calc(e4));
$ dotnet script expr1.csx
3
-1
-3
-0.3333333333333333

クラスと同様に、abstract を付けると仮想レコードを定義することができます。クラスを定義するよりもレコードのほうが簡潔にプログラムできました。immutable なデータ構造を扱う場合、レコードはとても便利な機能だと思います。興味のある方はいろいろ試してみてください。


Copyright (C) 2022 Makoto Hiroi
All rights reserved.

[ Home | C# ]