Quantcast
Channel: ++C++; // 未確認飛行 C
Viewing all 75 articles
Browse latest View live

タプル

$
0
0

概要

Ver. 7

名前のない複合型」で説明したように、 型には常によい名前が付くわけではなく、名無しにしておきたいことがあります。 そういう場合に使うもののうちの1つがC# 7で導入されたタプルです。

タプルの最大の用途は多値戻り値です。 関数の戻り値は引数と対になるものなので、タプルの書き心地は引数に近くなるように設計されています。

ポイント

  • (int x, int y)というような、引数みたいな書き方で「名前のない型」を作れます
  • この書き方をタプルと呼びます

タプル

C# 7で導入されたタプル(tuple)は、 (int x, int y)というような、引数みたいな書き方で「名前のない型」を作る機能です。

タプルという名前

前節の戻り値は「最小値と最大値と平均値を並べたもの」なわけですが、こういう「データを複数並べたもの」を意味する単語がタプルです。

英語では倍数を「double, triple, quadruple, ...」などという単語で表しますが、これを一般化して n-tuple (nは0以上の任意の整数)と書くことがあり、これがタプルの語源です。 n倍、n重、n連結というような意味しかなく、まさに「名前のない複合型」にピッタリの単語です。

型の明示

(int x, int y)みたいな書き方で、1つの型を表します。 タプルの型の書き方はメソッドの仮引数リスト(引数を受け取る側の書き方)に似ていて、()の中に「型名 メンバー名」を , 区切りで並べます。

これは、型を書ける場所であれば大体どこにでもこの「型」を書けます。 まず、以下のように、フィールドや戻り値などの型にできます。

class Sample
{
    private (int x, int y) value;
    public (int x, int y) GetValue() => value;
}

以下のように、ローカル変数の型としても明示できます。

var s = new Sample();
(int x, int y) t = s.GetValue();

もちろん、varを使った型推論も効きます。

varで型推論

また、ジェネリックな型の型引数にも使えます。

var dic = new Dictionary<(string s, string t), (int x, int y)>
{
    { ("a", "b"), (1, 2) },
    { ("x", "y"), (4, 8) },
};

Console.WriteLine(dic[("a", "b")]); // (1, 2)

次節の「タプル リテラル」という書き方もあるのであまり使わないとは思いますが、 一応、new演算子を使った初期化もできます。

// var t = new T(1, 2); みたいなのと同じノリ
var t1 = new (int x, int y) (1, 2);

// var t = new T { x = 1, y = 2}; みたいなのと同じノリ
var t2 = new (int x, int y) { x = 1, y = 2 };

タプル リテラル

タプルは(1, 2)というような書き方でリテラルを書くことができます。 タプル リテラルは実引数リスト(引数を渡す側の書き方)に似ています。

// メソッド呼び出し時の F(1, 2); みたいなノリ
(int x, int y) t1 = (1, 2);

// メソッド呼び出し時の F(x: 1, y: 2); みたいなノリ
var t2 = (x: 1, y: 2);

nullのように単体では型が決まらないものも、左辺に型があれば推論が効きます。 一方で、左辺もvar等になっていて型が決まらない場合、コンパイル エラーになります。

// これは左辺から型推論が聞くので、null も書ける
(string s, int i) t1 = (null, 1);

// これはダメ。null の型が決まらない。
var t2 = (null, 1); // コンパイル エラー

メンバー参照

メンバーの参照の仕方は普通の型と変わりません。(int x, int y)であれば、xyという名前でアクセスできます。 ちなみに、タプルのメンバーは書き換え可能です。

var t = (x: 1, y: 2);
Console.WriteLine(t.x); // 1
Console.WriteLine(t.y); // 2

// メンバーごとに書き換え可能
t.x = 10;
t.y = 20;
Console.WriteLine(t.x); // 10
Console.WriteLine(t.y); // 20

// タプル自身も書き換え可能
t = (100, 200);
Console.WriteLine(t.x); // 100
Console.WriteLine(t.y); // 200

ちなみに、タプルのメンバーはフィールドになっています (プロパティではない)。 フィールドになっているということは、例えば、参照引数(ref)に直接渡せます (これが、プロパティだと無理)。

例えば以下のようなメソッドがあったとします。

static void Swap<T>(ref T x, ref T y)
{
    var t = x;
    x = y;
    y = t;
}

このとき、以下のようにタプルのメンバーを渡せます。

var t = (x: 1, y: 2);
Swap(ref t.x, ref t.y);
Console.WriteLine(t.x); // 2
Console.WriteLine(t.y); // 1

タプルの分解

タプルは、各メンバーを分解して、それぞれ別の変数に受けて使うことができます。

var t = (x: 1, y: 2);

// 分解宣言1
(int x1, int y1) = t; // x1, y1 を宣言しつつ、t を分解
// 分解宣言2
var (x2, y2) = t; // 分解宣言の簡易記法

// 分解代入
int x, y;
(x, y) = t; // 分解結果を既存の変数に代入

この分解は、タプル以外の型に対しても使えるものです。 詳しくは「複合型の分解」で説明します。

タプル間の変換

タプル間の代入は、一定の条件下では暗黙的変換が掛かります。

名前違いのタプル

タプル間の代入は、メンバーの宣言位置に基づいて行われます。 逆に言うと、名前は無関係で、メンバーの型の並びだけ一致していれば代入できます。

例えば以下のように書くと、1番目同士(xs)、2番目同士(yt)で値が代入されます。

(int s, int t) t1 = (x: 1, y: 2);
Console.WriteLine(t1.s); // 1
Console.WriteLine(t1.t); // 2

同名であっても、位置が優先です。以下のような書き方をすると、xyが入れ替わります。

(int y, int x) t2 = (x: 1, y: 2);
Console.WriteLine(t2.x); // 2
Console.WriteLine(t2.y); // 1

型違いのタプル

タプルのメンバーの型が違う場合、メンバーごとに暗黙的な変換がかかる場合に限り、 タプル間の暗黙的変換ができます。

例えば以下の場合、xyzも、それぞれが型変換できるので、タプルの暗黙的型変換が掛かります。

object x = "abc"; // string → object は OK
long y = 1; // int → long は OK
int? z = 2; // int → int? は OK
// ↓
(object x, long y, int? z) t = ("abc", 1, 2); // OK

逆に、以下の場合はコンパイル エラーになります。この例では全部のメンバーが変換不能ですが、全部でなくても、どれか一つでも変換できないと、タプル自体の変換もエラーになります。

string x = 1; // int → string は NG
int y = 1L; // long → int は NG
int z = default(int?); // int? → int は NG
// ↓
(string x, int y, int z) t = (1, 1L, default(int?)); // NG

タプルの入れ子

タプルは入れ子にできます。

// タプルの入れ子
(string a, (int x, int y) b) t1 = ("abc", (1, 2));
Console.WriteLine(t1.a);   // abc
Console.WriteLine(t1.b.x); // 1
Console.WriteLine(t1.b.y); // 2

// 型推論も可能
var t2 = (a: "abc", b: (x: 1, y: 2));

メンバー名も匿名

タプルは、メンバー名もなくして、完全に匿名(名無し)にすることもできます。 この場合、メンバーを使う際にはItem1Item2、…というような名前で参照します。

var t1 = (1, 2);
Console.WriteLine(t1.Item1); // 1
Console.WriteLine(t1.Item2); // 2

Item1Item2、… という名前は、後述するValueTuple構造体のメンバー名です。

冒頭や「名前のない複合型」で説明したように、 「メンバー名だけ見れば十分」だから型名を省略するのであって、 メンバー名まで省略するのとさすがにプログラムが読みづらくなります。 メンバー名も持っていない完全な匿名タプルは、おそらくかなり短い寿命でしか使わないでしょう。 例えば、すぐに別の(メンバー名のある)タプル型に代入したり、分解して変数に受けて使うことになります。

予定

(書きかけ)

改ページする

実装寄りの話

ValueTuple構造体

TupleElementNames属性

実行時にはメンバー名は紛失する


複合型の分解

$
0
0

概要

Ver. 7

タプルから値を取り出す際には、メンバーを直接、それぞれバラバラに受け取りたくなることがあります。

名前のない複合型」で説明したように、 メンバー名だけ見ればその型が何を意味するか分かるからこそ型に名前が付かないわけです。 このとき、その型を受け取る変数にも、よい名前が浮かばなくなるはずです。

そこでC# 7では、タプルと同時に、分解(deconstruction)のための構文が追加されました。

分解

以下のような、整数列の個数(count)と和(sum)を同時に計算するメソッドがあったとします。 「名前のない複合型」で説明したように、 戻り値の型として「個数と和」みたいな名前(CountAndSumとか)しか思い浮かばないようなものです。

static (int count, int sum) Tally(IEnumerable<int> items)
{
    var count = 0;
    var sum = 0;
    foreach (var x in items)
    {
        sum += x;
        count++;
    }

    return (count, sum);
}

そうなると、この結果を受け取る変数名も、「個数と和」以上の名前はつかないでしょう。 通常、ローカル変数であれば適当な名前でもそこまで問題ではないので、 xとかyとか、本当に意味がない名前を付けることになると思います。

var x = Tally(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(x.count);
Console.WriteLine(x.sum);

実際にほしい名前はcountsumだけです。 であれば、最初からcount変数とsum変数に分解して受け取りたいと思うでしょう。 要するに、以下のようなことを1行で書ける構文がほしいです。

// この3行に相当する構文がほしい
var x = Tally(new[] { 1, 2, 3, 4, 5 });
var count = x.count;
var sum = x.sum;
// 以後、もう x は使わない

Console.WriteLine(count);
Console.WriteLine(sum);

タプルのような名前の決まらない型は、この例のように分解して使うのが前提と言えます。

そこで、C# 7では、タプルと一緒に、以下のような分解のための構文を追加しました。

(var count, var sum) = Tally(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(count);
Console.WriteLine(sum);

導入された分解構文には、 分解と同時に変数宣言もする「分解宣言」と、 既存の変数に分解する「分解代入」の2種類あります。

ちなみに、この分解構文は、タプルか、後述するDeconstructメソッドを持つ任意の型に対して使えます。

分解宣言

以下のような書き方で、分解と同時に変数を宣言できます。 これを分解宣言(deconstruction declaration)と言います。

// count, sum を宣言しつつ、タプルを分解
(int count, int sum) = Tally(items);

// ↓こう書くとタプル型の変数の宣言
// (int count, int sum) t = Tally(items);

この例の後半のコメントのように、分解宣言はタプルの型宣言の書き方によく似ています。 ただ、タプルの型宣言と違って、型推論のvarが使えます。

// 型推論で count, sum を宣言しつつ、タプルを分解
(var count, var sum) = Tally(items);

// ↓タプルだと var は使えない。これはコンパイル エラー
// (var count, var sum) t = Tally(items);

このとき、部分的に型推論(var)を使うこともできます。

// 部分的に var を使う
(var count, long sum) = Tally(items);

一方で、宣言したいすべての変数を型推論する場合であれば、先頭に1つだけ var キーワードを書く以下のような書き方もできます。

// 「var + 変数リスト」でタプルを分解
var (count, sum) = Tally(items);

この書き方は、foreachforなどでの変数宣言でも使えます。

(int x, int y)[] array = new[] { (1, 2), (3, 4) };

foreach (var (x, y) in array)
{
    Console.WriteLine($"{x}, {y}");
}

for ((int i, int j) = (0, 0); i < 10; i++, j--)
{
    Console.WriteLine($"{i}, {j}");
}

(仕様書状はクエリ式のletfrom でも使えることになっているものの、プレビュー版である現在は未実装。)

分解代入

既存の変数を使って分解することもできます。 こちらは分解代入(deconstruction assignment)といいます。

int x, y;

// 既存の変数を使って分解
(x, y) = Tally(items);

文法説明のために簡素化したものとはいえ、この例では分解宣言で十分で、 再代入(既存の変数xyの書き換え)の必要性があまりありません。 実際は、以下の例のように、ループで書き換えたりすることになるでしょう。

var x = 1.0;
var y = 5.0;

for (int i = 0; i < 100; i++)
{
    (x, y) = ((x + y) / 2, Math.Sqrt(x * y));
}

分解代入の左辺には、書き換え可能なものであれば何でも書けます。 例えば、配列アクセスや参照戻り値などを分解代入の左辺に書けます。

private static void DeconstractionAssingment()
{
    var a = new[] { 1, 2 };

    // 配列アクセス
    var b = new int[a.Length];
    (b[1], b[0]) = (a[0], a[1]);

    // 参照戻り値
    var c = new int[a.Length];
    (Mod(c, 5), Mod(c, 8)) = (a[0], a[1]);

    Console.WriteLine(string.Join(", ", b));
    Console.WriteLine(string.Join(", ", c));
}

static ref T Mod<T>(T[] array, int index) => ref array[index % array.Length];

分解時の型変換

分解時には、タプル間の型変換と同じルールで暗黙の型変換が働きます。 すなわち、宣言位置で分解されます(メンバー名は見ない)し、メンバーごとに暗黙的型変換が効くなら分解でも暗黙的型変換が効きます。

// Tally の戻り値は (count, sum) の順
var t = Tally(new[] { 1, 2, 3, 4, 5 });

// sum = t.count, count = t.sum の意味になるので注意が必要
(int sum, int count) = t;
Console.WriteLine(sum);   // 5
Console.WriteLine(count); // 15

// int → object も int → long も暗黙的に変換可能
// なので、分解もでもこの変換が暗黙的に可能
(object x, long y) = t;

任意の型を分解

C#の言語機能としてのタプルの他にも、 タプルに類する型はあります。 すなわち、意味のある変数が作れず、分解して使う前提の型です。

代表例はKeyValuePair構造体(System.Collections.Generic名前空間)でしょう。 keyvalueという変数で分解して受け取りたいです。

また、C#の構文としてタプルが導入される以前に使っていた型ですが、 Tupleクラス(System名前空間)というものがあります。 メンバー名まで紛失してしまうので使い勝手はよくありませんが、 「型名がうまく付けられない時に使う型」です。

これらの型に対しても分解構文を使いたいです。 そこで、C# 7では、Deconstructという名前のインスタンス メソッド、もしくは、拡張メソッドさえ持っていれば、 どんな型でも分解構文使えるようにしました。 例としてKeyValuePairTupleに対するDeconstructの書き方を示しましょう。 以下のような拡張メソッドがあれば分解できます。

static class Extensions
{
    public static void Deconstruct<T, U>(this KeyValuePair<T, U> pair, out T key, out U value)
    {
        key = pair.Key;
        value = pair.Value;
    }

    public static void Deconstruct<T1, T2>(this Tuple<T1, T2> x, out T1 item1, out T2 item2)
    {
        item1 = x.Item1;
        item2 = x.Item2;
    }
}

(ちなみに、将来的には、KeyValuePairTuple自体に手が入って、インスタンス メソッドとしてDeconstructメソッドが追加される可能性もあります。)

これで、KeyValuePairTupleに対して分解構文が使えます。以下のようなコードが書けます。

var pair = new KeyValuePair<string, int>("one", 1);
var (k, v) = pair;
// 以下のようなコードに展開される
// string k;
// int v;
// pair.Deconstruct(out k, out v);

var tuple = Tuple.Create("abc", 100);
var (x, y) = tuple;
// 以下のようなコードに展開される
// string x;
// int y;
// tuple.Deconstruct(out x, out y);

分解の評価のされ方

分解構文では、メンバーごとにそれぞれ代入するような結果を生みます。 このとき、以下のようなルールが働きます。

  • メンバーの評価は左から順
  • メンバーの書き換えは同時に起こる

単純な場合、例えば(a, b) = (x, y);のような時にはこんなルールを気にするまでもなく、a = x; b = y;と同じ結果になります。 ここで、もう少し複雑な場合を考えてみましょう。

まず、左右で同じ変数が出てくる場合についてです。 分解構文では、各メンバーへの代入が同時に行われるかのような結果を生みます。 例えば、xyという2つの変数の値を入れ替え(swap)ようとするとき、逐次実行であれば、以下のような書き方は間違いです。

var x = 1;
var y = 2;

y = x;
x = y; // 上の行で y が書き換わっているので、値の入れ替えにはならない

Console.WriteLine(x); // 1
Console.WriteLine(y); // 1

// 正しくは以下のように書く
// var temp = y;
// y = x;
// x = temp;

これが、分解代入を使って以下のように書くと、正しく値が入れ替わります。

var x = 1;
var y = 2;

// 分解代入であれば、値の書き換えは同時に起こる
(y, x) = (x, y);

Console.WriteLine(x); // 2
Console.WriteLine(y); // 1

値が並行して同時に書き換わっているように見えます。 実際には、以下のように、一時的な変数が挟まったように解釈されています。

// 実際には、同時に書き換わったように見えるように、一時変数が挟まる
// (y, x) = (x, y) であれば、以下のように評価されてる
var t = (x, y); // 一時的にタプルが作られる
y = t.Item1;
x = t.Item2;

タプルが作られる分、var temp = y; y = x; x = temp; というような書き方よりは若干効率が悪くなります。 といっても、タプルは構造体なのでヒープ(「値型と参照型の違い」、「ヒープ」参照)を圧迫したりはしません。 おそらくJITコンパイル時の最適化で消える程度の差です。

さらに複雑になるのは、式が副作用を持つ場合です。 例として、分解代入の両辺に、悪名高いインクリメント演算を混ぜてみましょう。 各メンバーは、左から順に評価されます。

var a = new[] { 0, 1, 2, 3 };
var i = 0;

(a[i++], a[i++]) = (a[i++], a[i++]);

Console.WriteLine(string.Join(", ", a)); // 2, 3, 2, 3
// つまり、以下の評価を受けてる
// (a[0], a[1]) = (a[2], a[3]);

これと同じ動作をタプルと分解なしで書くと、以下のようなコードになります。

var a = new[] { 0, 1, 2, 3 };
var i = 0;

ref var l1 = ref a[i++];
ref var l2 = ref a[i++];
var r1 = a[i++];
var r2 = a[i++];

l1 = r1;
l2 = r2;

[雑記] 小さな機能の組み合わせ

$
0
0

この記事はソフトウェアデザインに寄稿した内容が元になっています。

初出: 技術評論社刊『ソフトウェアデザイン 2016 年 4 月 号
    今すぐ実践できる良いプログラムの書き方
    C#編 言語機能の進化から学ぶ「良いコードの書き方」

概要

LINQ」で説明した通り、C#にはLINQ(Language Integrated Queryの略語。リンクと読む)と呼ばれるデータ処理用の機能があります。 LINQは、正確に言うとデータ処理に関連する複数の構文やライブラリの組み合わせを指す言葉です。

「LINQとは何か」については他のページでで説明しますが、ここで重要なのは、「組み合わせ」という部分です。小さな機能を組み合わせて大きな目的を実現したり、汎用的な処理を組み合わせて複雑な処理を組み合わせたり、それぞれ別の担当者が書いた小さな部品を組み合わせてシステム全体を構築したり、様々な組み合わせが考えられます。

ここでは、C#でデータ処理を行う上で、「組み合わせ」がどう活きているかという話をしていきましょう。

入力、加工、出力

1つ目は、データ列の入力元と出力先の組み合わせです。少し恣意的な例になりますが、「入力した整数列のうち、奇数のものだけ抜き出して、二乗したものを出力する」という処理を考えましょう。入力元・出力先が固定でいいならそう難しい話ではありません。例えば、コンソールからの入出力で考えると、以下のようになります。

while (true)
{
    // コンソールから入力
    var line = Console.ReadLine();
    if (string.IsNullOrEmpty(line)) break;
    var x = int.Parse(line);

    // 条件選択
    if ((x % 2) == 1)
    {
        // 値の変換
        var y = x * x;

        // コンソールに出力
        Console.WriteLine(y);
    }
}

問題は、入力元/出力先はコンソールとは限らないことです。ファイルの読み書きであったり、ネット越しの受け渡しであったり、様々な入出力が考えられます。そのたびに、この例のような類のコードを書くのは非効率で、「奇数のものだけ抜き出して、二乗」という加工する部分だけを切り出して、様々な入出力と組み合わせて使えるようにすべきです。

これは、IEnumerable<T>(System.Collections.Generic名前空間)を受け取り、IEnumerable<T>を返すメソッドを作れば実現できます。イテレーターを使えばそう難しくはありません。以下のような書き方ができます。

// コンソールから入力
static IEnumerable<int> Read()
{
    while (true)
    {
        var line = Console.ReadLine();
        if (string.IsNullOrEmpty(line)) break;
        yield return int.Parse(line);
    }
}

// 加工: 条件選択 + 変換
static IEnumerable<int> Filter(IEnumerable<int> source)
{
    foreach (var x in source)
        if ((x % 2) == 1)
            yield return x * x;
}

// コンソールに出力
static void Write(IEnumerable<int> source)
{
    foreach (var x in source)
        Console.WriteLine(x);
}

これで、下図に示すように、様々な入出力の組み合わせが使えるようになります。

入力、加工、出力の組み合わせ

汎用処理の組み合わせ

続いては小さな汎用処理の組み合わせで所望の処理を実現することについて考えます。前節の加工処理(サンプル コードのFilterメソッド)には、さらに細かく分けると以下の処理が含まれています。

  • 条件選択: 奇数だけ取り出す
  • 変換: 二乗を計算する

そして、一般に、多くのデータ処理がこの類型に当てはまります。すなわち、何らかの条件を与えて選択を行い、何らかの式に従って変換を行います。

実は、.NETには標準で、条件選択や変換のためのライブラリが含まれています。WhereメソッドとSelectメソッド(いずれもSystem.Linq名前空間のEnumerableクラスで定義されている静的メソッド)です。

  • Where: 条件を与えてデータを選択する
  • Select: 式を与えてデータを変換する

これらの名前は、SQLのキーワードに由来します。この他にも、Enumerableクラスには、データ加工用の様々なメソッドが用意されています。

これらを使って前節のコードと同じ処理を書き直すと、(コード中のRead, Writeに対して)以下のような書き方ができます。

Write(Read()
    .Where(x => (x % 2) == 1)
    .Select(x => x * x)
    );

ちなみに、Where, Selectは、インスタンス メソッドと同じようにx.Where(...)というような書き方をしていますが、実際に呼ばれるのはEnumerableクラスのWhere静的メソッドです。これは、拡張メソッドと呼ばれる機能を 使っています。

これで、下図に示すように、汎用処理の組み合わせで所望の処理を実現できます。

汎用処理の組み合わせ

契約、実装、処理

前節で説明したようなIEnumerable<T>を中心とした汎用処理には、下図に示すような3つの立場が絡みます。

規約、実装、処理

規約(contract)は、型が持つべきメンバーが何かを定めます。IEnumerable<T>の例でいうと、「データ列を得るためにはCurrentプロパティやMoveNextメソッドが必要」というようなものです。これを定めるのがインターフェイスです。

実装(implementation)は、規約が定めるメンバーをどう実現するかです。同様の例でいうと、「配列やリストなどのクラスはIEnumerable<T>を実装しているのでデータ列を列挙できる。列挙の仕方はそれぞれのクラスによって異なる」となります。

そして最後に、この規約に沿えば実現できる処理(process)があります。今回の例でいうと、「WhereSelectなど、IEnumerable<T>から得られるデータ列を加工して、別のIEnumerable<T>を返すメソッドを作る」といったものです。

重要なのは、規約、実装、処理の3つは、それぞれ別の担当者が書く(ということがあるし、そうできるべき)ということです。これに対してありがちなミスは、実装クラス(ここでいう配列やリスト)に処理(ここでいうWhereSelect)を含めてしまうというものです。そうやってしまうと、どんな実装にでも使えそうな汎用的な処理が特定の実装にだけ含まれることになって、組み合わせて使うことができなくなります。組み合わせを増やすために、規約、実装、処理の分離を意識しましょう。

文法の組み合わせ

この章の冒頭で「LINQとはデータ処理に関連する複数の構文の組み合わせ」という話をしました。データ処理はプログラミングにおいて重要なテーマの1つですが、それでも、汎用プログラミング言語にデータ処理専用の構文を導入するのはやりすぎでしょう(「汎用」でなくなる)。しかし、それぞれ汎用に使える小さな構文の組み合わせで実現できるなら話は別で、汎用プログラミング言語に導入する価値が高くなります。

詳細はそれぞれのリンク先を見てもらうとして、LINQは以下のような構文の組み合わせで実現されています。これらはすべて、C# 3.0で追加され、データ処理以外のことに対しても有用です。

タプル

$
0
0

概要

Ver. 7

名前のない複合型」で説明したように、 型には常によい名前が付くわけではなく、名無しにしておきたいことがあります。 そういう場合に使うもののうちの1つがC# 7で導入されたタプルです。

タプルの最大の用途は多値戻り値です。 関数の戻り値は引数と対になるものなので、タプルの書き心地は引数に近くなるように設計されています。

ポイント

  • (int x, int y)というような、引数みたいな書き方で「名前のない型」を作れます
  • この書き方をタプルと呼びます

タプル

C# 7で導入されたタプル(tuple)は、 (int x, int y)というような、引数みたいな書き方で「名前のない型」を作る機能です。

タプルという名前

前節の戻り値は「最小値と最大値と平均値を並べたもの」なわけですが、こういう「データを複数並べたもの」を意味する単語がタプルです。

英語では倍数を「double, triple, quadruple, ...」などという単語で表しますが、これを一般化して n-tuple (nは0以上の任意の整数)と書くことがあり、これがタプルの語源です。 n倍、n重、n連結というような意味しかなく、まさに「名前のない複合型」にピッタリの単語です。

型の明示

(int x, int y)みたいな書き方で、1つの型を表します。 タプルの型の書き方はメソッドの仮引数リスト(引数を受け取る側の書き方)に似ていて、()の中に「型名 メンバー名」を , 区切りで並べます。

これは、型を書ける場所であれば大体どこにでもこの「型」を書けます。 まず、以下のように、フィールドや戻り値などの型にできます。

class Sample
{
    private (int x, int y) value;
    public (int x, int y) GetValue() => value;
}

以下のように、ローカル変数の型としても明示できます。

var s = new Sample();
(int x, int y) t = s.GetValue();

もちろん、varを使った型推論も効きます。

varで型推論

また、ジェネリックな型の型引数にも使えます。

var dic = new Dictionary<(string s, string t), (int x, int y)>
{
    { ("a", "b"), (1, 2) },
    { ("x", "y"), (4, 8) },
};

Console.WriteLine(dic[("a", "b")]); // (1, 2)

次節の「タプル リテラル」という書き方もあるのであまり使わないとは思いますが、 一応、new演算子を使った初期化もできます。

// var t = new T(1, 2); みたいなのと同じノリ
var t1 = new (int x, int y) (1, 2);

// var t = new T { x = 1, y = 2}; みたいなのと同じノリ
var t2 = new (int x, int y) { x = 1, y = 2 };

ちなみに、タプルのメンバーは2つ以上な必要があります。()(int x)というようなタプルは現在の仕様では作れません。

タプル リテラル

タプルは(1, 2)というような書き方でリテラルを書くことができます。 タプル リテラルは実引数リスト(引数を渡す側の書き方)に似ています。

// メソッド呼び出し時の F(1, 2); みたいなノリ
(int x, int y) t1 = (1, 2);

// メソッド呼び出し時の F(x: 1, y: 2); みたいなノリ
var t2 = (x: 1, y: 2);

nullのように単体では型が決まらないものも、左辺に型があれば推論が効きます。 一方で、左辺もvar等になっていて型が決まらない場合、コンパイル エラーになります。

// これは左辺から型推論が聞くので、null も書ける
(string s, int i) t1 = (null, 1);

// これはダメ。null の型が決まらない。
var t2 = (null, 1); // コンパイル エラー

メンバー参照

メンバーの参照の仕方は普通の型と変わりません。(int x, int y)であれば、xyという名前でアクセスできます。 ちなみに、タプルのメンバーは書き換え可能です。

var t = (x: 1, y: 2);
Console.WriteLine(t.x); // 1
Console.WriteLine(t.y); // 2

// メンバーごとに書き換え可能
t.x = 10;
t.y = 20;
Console.WriteLine(t.x); // 10
Console.WriteLine(t.y); // 20

// タプル自身も書き換え可能
t = (100, 200);
Console.WriteLine(t.x); // 100
Console.WriteLine(t.y); // 200

ちなみに、タプルのメンバーはフィールドになっています (プロパティではない)。 フィールドになっているということは、例えば、参照引数(ref)に直接渡せます (これが、プロパティだと無理)。

例えば以下のようなメソッドがあったとします。

static void Swap<T>(ref T x, ref T y)
{
    var t = x;
    x = y;
    y = t;
}

このとき、以下のようにタプルのメンバーを渡せます。

var t = (x: 1, y: 2);
Swap(ref t.x, ref t.y);
Console.WriteLine(t.x); // 2
Console.WriteLine(t.y); // 1

タプルの分解

タプルは、各メンバーを分解して、それぞれ別の変数に受けて使うことができます。

var t = (x: 1, y: 2);

// 分解宣言1
(int x1, int y1) = t; // x1, y1 を宣言しつつ、t を分解
// 分解宣言2
var (x2, y2) = t; // 分解宣言の簡易記法

// 分解代入
int x, y;
(x, y) = t; // 分解結果を既存の変数に代入

この分解は、タプル以外の型に対しても使えるものです。 詳しくは「複合型の分解」で説明します。

タプル間の変換

タプル間の代入は、一定の条件下では暗黙的変換が掛かります。

名前違いのタプル

タプル間の代入は、メンバーの宣言位置に基づいて行われます。 逆に言うと、名前は無関係で、メンバーの型の並びだけ一致していれば代入できます。

例えば以下のように書くと、1番目同士(xs)、2番目同士(yt)で値が代入されます。

(int s, int t) t1 = (x: 1, y: 2);
Console.WriteLine(t1.s); // 1
Console.WriteLine(t1.t); // 2

同名であっても、位置が優先です。以下のような書き方をすると、xyが入れ替わります。

(int y, int x) t2 = (x: 1, y: 2);
Console.WriteLine(t2.x); // 2
Console.WriteLine(t2.y); // 1

型違いのタプル

タプルのメンバーの型が違う場合、メンバーごとに暗黙的な変換がかかる場合に限り、 タプル間の暗黙的変換ができます。

例えば以下の場合、xyzも、それぞれが型変換できるので、タプルの暗黙的型変換が掛かります。

object x = "abc"; // string → object は OK
long y = 1; // int → long は OK
int? z = 2; // int → int? は OK
// ↓
(object x, long y, int? z) t = ("abc", 1, 2); // OK

逆に、以下の場合はコンパイル エラーになります。この例では全部のメンバーが変換不能ですが、全部でなくても、どれか一つでも変換できないと、タプル自体の変換もエラーになります。

string x = 1; // int → string は NG
int y = 1L; // long → int は NG
int z = default(int?); // int? → int は NG
// ↓
(string x, int y, int z) t = (1, 1L, default(int?)); // NG

タプルの入れ子

タプルは入れ子にできます。

// タプルの入れ子
(string a, (int x, int y) b) t1 = ("abc", (1, 2));
Console.WriteLine(t1.a);   // abc
Console.WriteLine(t1.b.x); // 1
Console.WriteLine(t1.b.y); // 2

// 型推論も可能
var t2 = (a: "abc", b: (x: 1, y: 2));

メンバー名も匿名

タプルは、メンバー名もなくして、完全に匿名(名無し)にすることもできます。 この場合、メンバーを使う際にはItem1Item2、…というような名前で参照します。

var t1 = (1, 2);
Console.WriteLine(t1.Item1); // 1
Console.WriteLine(t1.Item2); // 2

Item1Item2、… という名前は、後述するValueTuple構造体のメンバー名です。

冒頭や「名前のない複合型」で説明したように、 「メンバー名だけ見れば十分」だから型名を省略するのであって、 メンバー名まで省略するのとさすがにプログラムが読みづらくなります。 メンバー名も持っていない完全な匿名タプルは、おそらくかなり短い寿命でしか使わないでしょう。 例えば、すぐに別の(メンバー名のある)タプル型に代入したり、分解して変数に受けて使うことになります。

タプルの内部実装

タプルがどういうコードに展開されるかについても話しておきましょう。

タプルを使ったコードを古いバージョンの.NET上で動かしたり、 タプルを使ったライブラリを古いバージョンのC#から参照したり、 別のプログラミング言語から参照したい場合もあります。 そのために、タプルは、ValueTupleという構造体に展開されます。

ValueTuple構造体への展開

タプルは、コンパイルの結果としてはValueTuple構造体(System名前空間)に展開されます。

例えば、以下のようなコードを考えます。

var t = (x: 3, y: 5);
var p = t.x * t.y;
var (x, y) = t;
Console.WriteLine($"{x} × {y} = {p}");

以下のようなコードに展開されます。

var t = new ValueTuple<int, int>(3, 5); // (x: 3, y: 5)
var p = t.Item1 * t.Item2; // t.x * t.y
var x = t.Item1;
var y = t.Item2;
Console.WriteLine($"{x} × {y} = {p}");

元々のxyという名前は、内部的には残っていません。ValueTuple構造体のメンバーであるItem1Item2に展開されます。

特に、一度objectdynamicを経由すると、名前を完全に紛失します。 以下のコードでは、xyが見つからず、実行時エラーを起こします。

private static void Dynamic()
{
    // 匿名型は名前が残る
    var a = new { x = 3, y = 5 };
    var s1 = Sum(a); // 大丈夫
    Console.WriteLine(s1);

    // タプル型は名前を紛失する
    var t = (x: 3, y: 5);
    var s2 = Sum(t); // x, yという名前が実行時になくてエラーに
    Console.WriteLine(s2);
}

private static dynamic Sum(dynamic d) => d.x + d.y;

TupleElementNames属性

とはいえ、名前をどこにも残さないと、ライブラリをまたいだ時にxyなどの名前が使えなくて困ります。 そこで、クラスのメンバーにタプルを使う場合には、TupleElementNames属性(System.Runtime.CompilerServices名前空間)を付けて、 C#コンパイラーには名前がわかるようにしています。

例えば、以下のような引数も戻り値もタプルなメソッドを書いたとします。

public (int x, int y) F((int a, int b) t) => (t.a + t.b, t.a - t.b);

このメソッドは、以下のように展開されます。タプルがValueTuple構造体に化けますが、TupleElementNames属性を付けて名前を残します。

[return: TupleElementNames(new[] { "x", "y" })]
public ValueTuple<int, int> F([TupleElementNames(new[] { "a", "b" })] ValueTuple<int, int> t)
    => new ValueTuple<int, int>(t.Item1 + t.Item2, t.Item1 - t.Item2);

C#コンパイラーは、この情報を元に、タプルの名前を復元します。

ValueTuple構造体の中身

タプルの展開結果にあたるValueTupleは、型引数が0~8個の合計9個の構造体があります。 例えば、型引数2個のものは以下のような定義になっています。

[StructLayout(LayoutKind.Auto)]
public struct ValueTuple<T1, T2>
    : IEquatable<ValueTuple<T1, T2>>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<ValueTuple<T1, T2>>
{
    public T1 Item1;
    public T2 Item2;

    public ValueTuple(T1 item1, T2 item2)
    {
        Item1 = item1;
        Item2 = item2;
    }

    // 後略、インターフェイスのメンバー定義
}

基本的には、publicなフィールドだけを持つ構造体です。 それに、値の比較用の各種インターフェイスが実装されています。

メンバーが9個以上のタプル

最初に言った通り、ValueTuple構造体の型引数は、最大のものでも8個です。 では、メンバーが9個以上のタプルを作るとどうなるかというと、入れ子のValueTuple構造体が作られます。

例えば、以下のようなコードを書いたとします。 メンバー名も匿名で作ったので、9番目のメンバーの参照にItem9という名前が使われています。

var t = (1, 2, 3, 4, 5, 6, 7, 8, 9);
Console.WriteLine(t.Item9);

このコードは、以下のように展開されます。

var t = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int>>(
    1, 2, 3, 4, 5, 6, 7, new ValueTuple<int, int>(8, 9));
Console.WriteLine(t.Rest.Item2);

ValueTuple構造体の定義場所

C# 7のリリースに合わせて、ValueTuple構造体は標準ライブラリに取り込まれる予定です。

一方で、古い.NET (.NET Framework 4.6.2以前、.NET Standard 1.6以前)上でタプルを使いたい場合は、 以下のライブラリを参照します。この中にValueTuple構造体や、TupleElementNames属性が定義されています。

型引数0、1のValueTuple

前述の通り、タプルのメンバーは2つ以上な必要で、()(int x)というようなタプルは作れません。 一方で、ValueTuple構造体には、型引数0個と1個のものが存在します。

var noneple = new ValueTuple();
var oneple = new ValueTuple<int>(1);

// メンバー2個以上はタプル構文を使える
var twople = (1, 2); // new ValueTuple<int, int>(1, 2);
var threeple = (1, 2, 3); // new ValueTuple<int, int, int>(1, 2, 3);

型引数0個のValueTuple(0-tuple)は、いわゆるUnit型です。 voidの代わりにこの型を使うことで、戻り値がある場合とない場合のコードを統一的に書けてうれしい場合があります。 一方、型引数1個のもの(1-tuple)も、用途としては0-tupleと同じです。 型引数2個以上のものと並べて、戻り値や引数の個数違いを統一的に書けます。

例えば、以下の2つのコードはどちらの方が統一性があっていいかという話になります。

// タプルでは0、1は書けない
async Task F0() { }
async Task<int> F1() => 1;
async Task<(int x1, int x2)> F2() => (1, 2);
async Task<(int x1, int x2, int x3)> F3() => (1, 2, 3);
// こう書けると統一性があってきれい(C# 7では書けない)
async Task<()> F0() { }
async Task<(int x1)> F1() => (1);
async Task<(int x1, int x2)> F2() => (1, 2);
async Task<(int x1, int x2, int x3)> F3() => (1, 2, 3);

特に、ソースコード生成などでまとめて、個数違いのメソッドを生成したい場合などには、0-tupleや1-tupleがほしくなります。 0個と1個の時だけ特別扱いが必要になるかどうかという問題です。 0-tupleと1-tupleがあれば、特別扱いなしでソースコード生成ができて楽です。

ということで、0-tuple、1-tupleの需要はあるんですが、問題があって構文を提供できていません。 1-tupleになるであろう構文は(1)というような形になるはずですが、 これが、C#の既存の構文ですでに、単に1と同じ意味で解釈されるため、1-tupleを作れません。 0-tupleの方の()は、これまでは書けなかった書き方なので別にC# 7で追加できますが、 1-tupleだけ飛ばして「0か2以上のみ」とするのも変な話です。

関連

タプルには、毛色の似た機能が2つあります。

  • 匿名型 … タプルと同様に、名前がない型
  • 出力引数 … 複数の戻り値を返すのに使える

匿名型との比較

タプルは、名前がない型という観点で言うと、匿名型と似ています。 しかし、「名前のない複合型」で説明したように、 出自・用途の違いから、内部実装は結構異なります。

以下の表のようになります。

タプル 匿名型
主な用途 多値戻り値 部分的なメンバー抜き出し
展開結果 ValueTuple構造体+属性 クラスの生成
型の種類 値型 参照型
見た目 引数の書き方に似ている オブジェクト初期化子の書き方に似ている

展開結果の差は用途の差から来ています。 タプルは戻り値として使います。publicなメンバーの型にも使うことになるので、ライブラリ間をまたげる必要があります。 ValueTuple構造体に展開することで、ライブラリをまたいでも同じ構造体を参照する状態になります。

一方、匿名型は、ライブラリごとにそれぞれクラスを生成します(「匿名型」参照)。 同じ型に見えて、ライブラリをまたぐと別クラスになってしまいます。 このことから、匿名型は、メソッドの戻り値など、publicになりうる場所には書けません。 メソッド内のローカルな部分で完結して使う必要があります。

とはいえ、ValueTuple構造体に展開では、前節での説明の通り、実行時に名前を紛失します。 dynamicや、式木での利用にはタプルは向きません。匿名型を使います。

値型か参照型かも実装が異なりますが、これも、戻り値として使う、その後すぐに分解して使うという想定だと、値型の方が実行性能的に有利だからです。 用途が変われば最適な実装は変わります。

出力引数との比較

多値戻り値という用途だと、出力引数という手段もあります。 一般的に言うと、多値戻り値には今後タプルを使うのがおすすめです。 出力引数の方が煩雑な書き方になりがちだからです。

比較のために簡単な例を挙げてみましょう。まず、C# 6以前の出力引数を使ったものです。

static void F(Point p)
{
    // 事前に変数を用意しないといけない/var 不可
    int x, y;
    // 1個1個 out を付けないといけない
    Deconstruct(p, out x, out y);
    Console.WriteLine($"{x}, {y}");

    //非同期メソッドには使えない
}

// 1個1個 out を付けないといけない
static void Deconstruct(Point p, out int x, out int y)
{
    // 1個1個代入
    x = p.X;
    y = p.Y;
}

1個1個out修飾子を付けて回るのは結構な煩雑さです。 呼び出す前に別途変数宣言が必要なのも面倒です。 これらは単に煩雑なだけなので我慢すれば何とかなりますが、 致命的なのは非同期メソッドで使えないことです。

ちなみに、煩雑さはC# 7で多少マシになりました。出力変数という構文(説明ページ準備中。でき次第リンク)が追加されて、以下のように書けます。

static void F(Point p)
{
    // 変数の事前準備は不要に
    // でも1個1個 out を付けないといけない
    Deconstruct(p, out var x, out var y);
    Console.WriteLine($"{x}, {y}");

    //非同期メソッドには相変わらず使えない
}

// 1個1個 out を付けないといけない
static void Deconstruct(Point p, out int x, out int y) => (x, y) = (p.X, p.Y);

でも、相変わらず長くなりがちです。 また、非同期メソッドで使えない点は変わりません。

タプルを使えばこの問題は解決です。

static async Task F(Point p)
{
    // 1個の var で受け取れる
    var t1 = Deconstruct(p);
    Console.WriteLine($"{t1.x}, {t1.y}");

    // 何なら分解と併せればもっと書き心地よく書ける
    var (x, y) = Deconstruct(p);
    Console.WriteLine($"{x}, {y}");

    // 非同期メソッドで使えるのはタプルだけ
    var t2 = await DeconstructAsync(p);
    Console.WriteLine($"{t2.x}, {t2.y}");
}

static (int x, int y) Deconstruct(Point p) => (p.X, p.Y); // 1個の式で書けて楽
static async Task<(int x, int y)> DeconstructAsync(Point p) => (p.X, p.Y);

一方で、出力引数を使いたくなる場面も残っています。

  • TryParseのように、bool値を返してifステートメントなどの条件式内で使いたい場合
  • オーバーロードを呼び分けたい場合

if内で使いたい場合は、例えば以下のようなコードになります。

static void TryPattern()
{
    var s = Console.ReadLine();
    if (int.TryParse(s, out var x)) Console.WriteLine(x);
}

これはさすがにタプルを使う方が煩雑です。

static void TuplePattern()
{
    var s = Console.ReadLine();
    var (success, x) = Parse(s);
    if (success) Console.WriteLine(x);
}

static (bool success, int value) Parse(string s) => int.TryParse(s, out var x) ? (true, x) : (false, 0);

もっとも、C# 7では、以下のような is 演算子を使ったnullチェックで同様のことをすると言う手もあります。 この書き方を型スイッチと呼びます(説明ページ準備中。でき次第リンク)。

static void NullCheckPattern()
{
    var s = Console.ReadLine();
    if (ParseOrDefault(s) is int x) Console.WriteLine(x);
}

static int? ParseOrDefault(string s) => int.TryParse(s, out var x) ? x : default(int?);

もう1つ、オーバーロードですが、C#では(というか.NETでは)、引数でのオーバーロードはできますが、戻り値でのオーバーロードはできません。 そこで、以下のように、オーバーロードに関しては出力引数の方が有利になります。

// これはオーバーロード可能
static void F(out int x, out int y) => (x, y) = (1, 2);
static void F(out int id, out string name) => (id, name) = (1, "abc");

// 戻り値でのオーバーロードはできない
// コンパイル エラーに
static (int x, int y) F() => (1, 2);
static (int id, string name) F() => (1, "abc");

複合型の分解

$
0
0

概要

Ver. 7

タプルから値を取り出す際には、メンバーを直接、それぞれバラバラに受け取りたくなることがあります。

名前のない複合型」で説明したように、 メンバー名だけ見ればその型が何を意味するか分かるからこそ型に名前が付かないわけです。 このとき、その型を受け取る変数にも、よい名前が浮かばなくなるはずです。

そこでC# 7では、タプルと同時に、分解(deconstruction)のための構文が追加されました。

分解

以下のような、整数列の個数(count)と和(sum)を同時に計算するメソッドがあったとします。 「名前のない複合型」で説明したように、 戻り値の型として「個数と和」みたいな名前(CountAndSumとか)しか思い浮かばないようなものです。

static (int count, int sum) Tally(IEnumerable<int> items)
{
    var count = 0;
    var sum = 0;
    foreach (var x in items)
    {
        sum += x;
        count++;
    }

    return (count, sum);
}

そうなると、この結果を受け取る変数名も、「個数と和」以上の名前はつかないでしょう。 通常、ローカル変数であれば適当な名前でもそこまで問題ではないので、 xとかyとか、本当に意味がない名前を付けることになると思います。

var x = Tally(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(x.count);
Console.WriteLine(x.sum);

実際にほしい名前はcountsumだけです。 であれば、最初からcount変数とsum変数に分解して受け取りたいと思うでしょう。 要するに、以下のようなことを1行で書ける構文がほしいです。

// この3行に相当する構文がほしい
var x = Tally(new[] { 1, 2, 3, 4, 5 });
var count = x.count;
var sum = x.sum;
// 以後、もう x は使わない

Console.WriteLine(count);
Console.WriteLine(sum);

タプルのような名前の決まらない型は、この例のように分解して使うのが前提と言えます。

そこで、C# 7では、タプルと一緒に、以下のような分解のための構文を追加しました。

(var count, var sum) = Tally(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(count);
Console.WriteLine(sum);

導入された分解構文には、 分解と同時に変数宣言もする「分解宣言」と、 既存の変数に分解する「分解代入」の2種類あります。

ちなみに、この分解構文は、タプルか、後述するDeconstructメソッドを持つ任意の型に対して使えます。

分解宣言

以下のような書き方で、分解と同時に変数を宣言できます。 これを分解宣言(deconstruction declaration)と言います。

// count, sum を宣言しつつ、タプルを分解
(int count, int sum) = Tally(items);

// ↓こう書くとタプル型の変数の宣言
// (int count, int sum) t = Tally(items);

この例の後半のコメントのように、分解宣言はタプルの型宣言の書き方によく似ています。 ただ、タプルの型宣言と違って、型推論のvarが使えます。

// 型推論で count, sum を宣言しつつ、タプルを分解
(var count, var sum) = Tally(items);

// ↓タプルだと var は使えない。これはコンパイル エラー
// (var count, var sum) t = Tally(items);

このとき、部分的に型推論(var)を使うこともできます。

// 部分的に var を使う
(var count, long sum) = Tally(items);

一方で、宣言したいすべての変数を型推論する場合であれば、先頭に1つだけ var キーワードを書く以下のような書き方もできます。

// 「var + 変数リスト」でタプルを分解
var (count, sum) = Tally(items);

この書き方は、foreachforなどでの変数宣言でも使えます。

(int x, int y)[] array = new[] { (1, 2), (3, 4) };

foreach (var (x, y) in array)
{
    Console.WriteLine($"{x}, {y}");
}

for ((int i, int j) = (0, 0); i < 10; i++, j--)
{
    Console.WriteLine($"{i}, {j}");
}

(仕様書状はクエリ式のletfrom でも使えることになっているものの、プレビュー版である現在は未実装。)

分解代入

既存の変数を使って分解することもできます。 こちらは分解代入(deconstruction assignment)といいます。

int x, y;

// 既存の変数を使って分解
(x, y) = Tally(items);

文法説明のために簡素化したものとはいえ、この例では分解宣言で十分で、 再代入(既存の変数xyの書き換え)の必要性があまりありません。 実際は、以下の例のように、ループで書き換えたりすることになるでしょう。

var x = 1.0;
var y = 5.0;

for (int i = 0; i < 100; i++)
{
    (x, y) = ((x + y) / 2, Math.Sqrt(x * y));
}

分解代入の左辺には、書き換え可能なものであれば何でも書けます。 例えば、配列アクセスや参照戻り値などを分解代入の左辺に書けます。

private static void DeconstractionAssingment()
{
    var a = new[] { 1, 2 };

    // 配列アクセス
    var b = new int[a.Length];
    (b[1], b[0]) = (a[0], a[1]);

    // 参照戻り値
    var c = new int[a.Length];
    (Mod(c, 5), Mod(c, 8)) = (a[0], a[1]);

    Console.WriteLine(string.Join(", ", b));
    Console.WriteLine(string.Join(", ", c));
}

static ref T Mod<T>(T[] array, int index) => ref array[index % array.Length];

分解時の型変換

分解時には、タプル間の型変換と同じルールで暗黙の型変換が働きます。 すなわち、宣言位置で分解されます(メンバー名は見ない)し、メンバーごとに暗黙的型変換が効くなら分解でも暗黙的型変換が効きます。

// Tally の戻り値は (count, sum) の順
var t = Tally(new[] { 1, 2, 3, 4, 5 });

// sum = t.count, count = t.sum の意味になるので注意が必要
(int sum, int count) = t;
Console.WriteLine(sum);   // 5
Console.WriteLine(count); // 15

// int → object も int → long も暗黙的に変換可能
// なので、分解もでもこの変換が暗黙的に可能
(object x, long y) = t;

任意の型を分解

C#の言語機能としてのタプルの他にも、 タプルに類する型はあります。 すなわち、意味のある変数が作れず、分解して使う前提の型です。

代表例はKeyValuePair構造体(System.Collections.Generic名前空間)でしょう。 keyvalueという変数で分解して受け取りたいです。

また、C#の構文としてタプルが導入される以前に使っていた型ですが、 Tupleクラス(System名前空間)というものがあります。 メンバー名まで紛失してしまうので使い勝手はよくありませんが、 「型名がうまく付けられない時に使う型」です。

これらの型に対しても分解構文を使いたいです。 そこで、C# 7では、Deconstructという名前のインスタンス メソッド、もしくは、拡張メソッドさえ持っていれば、 どんな型でも分解構文使えるようにしました。 例としてKeyValuePairTupleに対するDeconstructの書き方を示しましょう。 以下のような拡張メソッドがあれば分解できます。

static class Extensions
{
    public static void Deconstruct<T, U>(this KeyValuePair<T, U> pair, out T key, out U value)
    {
        key = pair.Key;
        value = pair.Value;
    }

    public static void Deconstruct<T1, T2>(this Tuple<T1, T2> x, out T1 item1, out T2 item2)
    {
        item1 = x.Item1;
        item2 = x.Item2;
    }
}

(ちなみに、将来的には、KeyValuePairTuple自体に手が入って、インスタンス メソッドとしてDeconstructメソッドが追加される可能性もあります。)

これで、KeyValuePairTupleに対して分解構文が使えます。以下のようなコードが書けます。

var pair = new KeyValuePair<string, int>("one", 1);
var (k, v) = pair;
// 以下のようなコードに展開される
// string k;
// int v;
// pair.Deconstruct(out k, out v);

var tuple = Tuple.Create("abc", 100);
var (x, y) = tuple;
// 以下のようなコードに展開される
// string x;
// int y;
// tuple.Deconstruct(out x, out y);

分解の評価のされ方

分解構文では、メンバーごとにそれぞれ代入するような結果を生みます。 このとき、以下のようなルールが働きます。

  • メンバーの評価は左から順
  • メンバーの書き換えは同時に起こる

単純な場合、例えば(a, b) = (x, y);のような時にはこんなルールを気にするまでもなく、a = x; b = y;と同じ結果になります。 ここで、もう少し複雑な場合を考えてみましょう。

まず、左右で同じ変数が出てくる場合についてです。 分解構文では、各メンバーへの代入が同時に行われるかのような結果を生みます。 例えば、xyという2つの変数の値を入れ替え(swap)ようとするとき、逐次実行であれば、以下のような書き方は間違いです。

var x = 1;
var y = 2;

y = x;
x = y; // 上の行で y が書き換わっているので、値の入れ替えにはならない

Console.WriteLine(x); // 1
Console.WriteLine(y); // 1

// 正しくは以下のように書く
// var temp = y;
// y = x;
// x = temp;

これが、分解代入を使って以下のように書くと、正しく値が入れ替わります。

var x = 1;
var y = 2;

// 分解代入であれば、値の書き換えは同時に起こる
(y, x) = (x, y);

Console.WriteLine(x); // 2
Console.WriteLine(y); // 1

値が並行して同時に書き換わっているように見えます。 実際には、以下のように、一時的な変数が挟まったように解釈されています。

// 実際には、同時に書き換わったように見えるように、一時変数が挟まる
// (y, x) = (x, y) であれば、以下のように評価されてる
var t = (x, y); // 一時的にタプルが作られる
y = t.Item1;
x = t.Item2;

タプルが作られる分、var temp = y; y = x; x = temp; というような書き方よりは若干効率が悪くなります。 といっても、タプルは構造体なのでヒープ(「値型と参照型の違い」、「ヒープ」参照)を圧迫したりはしません。 おそらくJITコンパイル時の最適化で消える程度の差です。

さらに複雑になるのは、式が副作用を持つ場合です。 例として、分解代入の両辺に、悪名高いインクリメント演算を混ぜてみましょう。 各メンバーは、左から順に評価されます。

var a = new[] { 0, 1, 2, 3 };
var i = 0;

(a[i++], a[i++]) = (a[i++], a[i++]);

Console.WriteLine(string.Join(", ", a)); // 2, 3, 2, 3
// つまり、以下の評価を受けてる
// (a[0], a[1]) = (a[2], a[3]);

これと同じ動作をタプルと分解なしで書くと、以下のようなコードになります。

var a = new[] { 0, 1, 2, 3 };
var i = 0;

ref var l1 = ref a[i++];
ref var l2 = ref a[i++];
var r1 = a[i++];
var r2 = a[i++];

l1 = r1;
l2 = r2;

型スイッチ

$
0
0

概要

Ver. 7

C# 7で、is演算子switchステートメントcaseが拡張されて、以下のような機能が入りました。

  • caseでも、is演算子と同じように、インスタンスの型を見ての分岐ができるようになった
  • x is T tや、case T tというように、型を調べつつ、型が一致してたらキャスト結果を変数tで受け取れるようになった

この機能を型スイッチ(type switch)と呼びます。

この機能は、C# 7よりもさらに先のバージョンで入る予定の「パターン マッチング(pattern matching)」という機能のうち、一番初歩的な部分だけを先に実装したものです。C# 7の時点では、型を見ての分岐(型スイッチ)だけが実装されていますが、将来的にはもっと複雑な条件を書けるようになります。

is演算子の拡張

C# 7では、is演算子で以下のような書き方ができるようになりました(型の後ろに新しい変数を書けるようになりました) 。

型を調べたい変数 is  新しい変数

C# 6以前のis演算子は少し使い勝手が悪い面がありました。型の一致を判定するだけならいいんですが、 型変換も絡むといまいちです。

例えば、以下のように型を判定するだけならis演算子の出番です。

// 型判定のみなら、これまでの is 演算子でも十分
if (obj is string) Console.WriteLine("string");

ところが、型を判定したうえでダウンキャストしたいという場面では、以下のように、「2度手間」になって、コード量的にも実行効率的にもよくないです。

// 型変換もしたい
if (obj is string)
{
    var s = (string)obj;
    //↑ isとキャストで2つの別命令を使う。二重処理になってるだけで無駄
    Console.WriteLine("string #" + s.Length);
}

結局、以下のように、as演算子を使うことが推奨されます。

// 結局、as 演算子 + null チェックを使うことになる
var s = obj as string;
if (s != null)
{
    Console.WriteLine("string #" + s.Length);
}

これに対して、C# 7では、is演算子で以下のような書き方ができるようになりました。

// C# 7での新しい書き方
if (obj is string s)
{
    Console.WriteLine("string #" + s.Length);
}

挙動的には、先ほどのas演算子を使ったものとまったく同じ挙動になります。 is演算子で型を判定しつつ(boolの戻り値を返しつつ)、その型への変換結果を新しい変数で受け取れます。

is演算子によるnullチェック

元々のis演算子の仕様でもあるんですが、nullには型がなくて常にisに失敗します(falseを返す)。

string x = null;

if (x is string)
{
    // x の変数の型は string なのに、is string は false
    // is 演算子は変数の実行時の中身を見る & null には型がない
    Console.WriteLine("ここは絶対通らない");
}

この仕様は、C# 7からの新しい構文でも引き継いでいて、nullじゃないときだけだけ何かの処理をしたいときに使えます。 と言っても、参照型の場合にはあまり使い道はありませんが、以下のような書き方ができます。

static void F(string nullable)
{
    if (nullable is string nonNull)
    {
        // nonNull には絶対に null が入らない
        // nullable をそのまま使っても、if の結果、null じゃない保証があるのであまり意味はないけども
        Console.WriteLine(nonNull.Length);
    }
}

この書き方が役に立つのは、値型とnull許容型を使う場合でしょう。 例えばC# 6以前だと、以下のような書き方になります。

static void F(int? x)
{
    // C# 6以前の書き方
    if (x.HasValue)
    {
        // この「.Value」が結構うっとおしい
        Console.WriteLine(x.Value * x.Value);
    }
}

これが、C# 7で以下のように書けるようになります。

static void F(int? x)
{
    if (x is int n)
    {
        Console.WriteLine(n * n);
    }
}

余談: 変数の意味を変えない

プログラミング言語によっては、以下のように、is演算子で型を判定した後には、自動的にその型扱いしてくれる言語もあります。

static void F(object obj)
{
    if (obj is string)
    {
        // この中では obj を string 扱いできる言語がある
        // C# ではコンパイル エラー
        Console.WriteLine("string #" + obj.Length);
    }
    else if (obj is int)
    {
        // 同上、int 扱いできる言語がある
        // C# ではコンパイル エラー
        Console.WriteLine("int " + (obj * obj));
    }
}

C# では、こういう、「objectだと思っていたものが一定範囲でだけ別の型になる」というようなことはやらない方針です。

また、以下のように、同名の別変数を導入できる言語もありますが、こちらもC#では認めていません。

static void F(object x)
{
    if (x is string x)
    {
        // 引数の x とは別に、is 演算子で別の「x」を導入できる言語もある
        // C# ではコンパイル エラー
        Console.WriteLine("string #" + x.Length);
    }
}

C#では、変数はスコープ内で意味不変(invariant meaning)であるべきという方針を持っています。 上記の2つの例では、objxが部分的に(ifの中でだけ)別の意味になるので、C#としては認めたくないものになります。

switchステートメントの拡張

C# 7では、switchステートメントのcase句に、値だけでなく、型を書けるようになりました。 型による条件の書き方は、前節のis演算子と同様です。 また、型による条件に加えて、when句というものを付けて追加の条件式を書くこともできます。

switch(変数)
{
    case  変数:
        // 型が一致しているときにここに来る
        // その型に変換した結果が変数に入っている
        break;
    case  変数 when 条件式:
        // 型が一致していて、かつ、条件式満たしているときにここに来る
        break;
    case :
        // 通常の値による条件との混在も可能
        break;
      ・
      ・
      ・
    default:
        // どの条件も満たさない時に実行される
        break;
}

例えば以下のような書き方ができます。

static void F(object obj)
{
    switch (obj)
    {
        case string s:
            Console.WriteLine("string #" + s.Length);
            break;
        case 7:
            Console.WriteLine("7の時だけここに来る");
            break;
        case int n when n > 0:
            Console.WriteLine("正の数の時にここに来る " + n);
            // ただし、上から順に判定するので、7 の時には来なくなる
            break;
        case int n:
            Console.WriteLine("整数の時にここに来る" + n);
            // 同上、0 以下の時にしか来ない
            break;
        default:
            Console.WriteLine("その他");
            break;
    }
}

上から逐次判定

C# 6までの、値による分岐しかなかったswitchステートメントとはちょっと違う部分があります。 以下の点に気を付けてください。

  • 条件の範囲が被る場合がある
    • 値による分岐の場合は、各 case がそれぞれ排他だった
    • 型による分岐が入ったことで、上記の例でいう 7intかつ正の数 ⊃ int のように、被りが起こり得る
  • 条件は上から順に判定する
    • 被りがない場合なら順序を気にする必要はなかった
      • なので、「ジャンプ テーブル化」(後述)という最適化手法が使えていた
    • 型による分岐を1つでも含むと、この前提が崩れて、ジャンプ テーブル化できない(逐次判定しかしない)

ジャンプ テーブル化の説明のために、以下のようなswitchを考えましょう。

switch(n)
{
    case 0: return "zero";
    case 1: return "one";
    case 2: return "two";
    case 3: return "three";
    case 4: return "four";
    case 5: return "five";
    case 6: return "six";
    case 7: return "seven";
    case 8: return "eight";
    case 9: return "nine";
    default: return "other";
}

こういうswitchであれば、以下のように、辞書を引いて結果を得ることもできるはずです。

var map = new Dictionary<int, string>
{
    { 0, "zero" },
    { 1, "one" },
    { 2, "two" },
    { 3, "three" },
    { 4, "four" },
    { 5, "five" },
    { 6, "six" },
    { 7, "seven" },
    { 8, "eight" },
    { 9, "nine" },
};

string s;
if (map.TryGetValue(n, out s)) return s;
else return "other";

caseの個数が少ないうちは普通に上から順に等値判定していく方が軽いんですが、 case数が増えれば増えるほど、辞書化した方が有利になります。

そこで、C# のswitchステートメント(というか、.NETの中間言語のswitch命令)では、caseの数が多い場合にこういう辞書を使った最適化を行うようになっています。 正確にいうと、辞書の値は条件分岐によるジャンプ先が入っていて、goto的な命令との組み合わせで実現されます。 そこで、「ジャンプ先のテーブルを引く」という意味で「ジャンプ テーブル化」と呼ばれます。

繰り返しになりますが、caseに型による条件を書いてしまうと、こういうジャンプ テーブル化ができなくなります。 というより、コンパイル結果的にはswitch命令が使えず、if-elseを繰り返すようなコードにコンパイルされます。 上から順に逐次判定になるので、case数があまりにも多いと実行性能的にあまりよくないので注意してください。

型スイッチの用途

型によって分岐する方法としては、仮想メソッドを使う方法があります。 オブジェクト指向プログラミング言語としては、仮想メソッドが相当に便利で、実行性能もよく、こちらが好まれます。 極端な意見では、「型を調べたら負け」、「ダウンキャストが必要なのは筋が悪い」という人すらいます。

ここでは、この仮想メソッドと、本稿の主題である型スイッチの使い分けについて説明します。

例として、以下のようなクラス階層を考えます。

public abstract class Node { }

public class Var : Node { }

public class Const : Node
{
    public int Value { get; }
    public Const(int value) { Value = value; }
}

public class Add : Node
{
    public Node Left { get; }
    public Node Right { get; }
    public Add(Node left, Node right)
    {
        Left = left;
        Right = right;
    }
}

public class Mul : Node
{
    public Node Left { get; }
    public Node Right { get; }
    public Mul(Node left, Node right)
    {
        Left = left;
        Right = right;
    }
}

説明都合で簡素化していますが、数式を扱うようなクラスです。 要するに、例えば、「x×x+1」というような式を、以下のようなコードで表すためのクラスです。

var expression = new Add(
    new Mul(
        new Var(),
        new Var()),
    new Const(1));

式を扱いためのクラス

これに対して、「変数xの値を与えて、式の計算結果を得る」というようなメソッドを、仮想メソッドと型スイッチの両方で作ってみましょう。

まず、仮想メソッドなら以下のようになるでしょう(必要な部分だけを抜き出してあります)。

abstract class Node
{
    public abstract int Calculate(int x);
}

class Var
{
    public override int Calculate(int x) => x;
}

class Const
{
    public override int Calculate(int x) => Value;
}

class Add
{
    public override int Calculate(int x) => Left.Calculate(x) + Right.Calculate(x);
}

class Mul
{
    public override int Calculate(int x) => Left.Calculate(x) * Right.Calculate(x);
}

一方、型スイッチを使って書くなら以下のようになります。

public static class NodeExtensions
{
    public static int Calculate(this Node n, int x)
    {
        switch (n)
        {
            case Var v: return x;
            case Const c: return c.Value;
            case Add a: return Calculate(a.Left, x) + Calculate(a.Right, x);
            case Mul m: return Calculate(m.Left, x) * Calculate(m.Right, x);
        }
        throw new ArgumentOutOfRangeException();
    }
}

それぞれ、以下のような特徴があります。

  • 性能:
    • 〇 仮想メソッドはかなり実行性能がいい
    • × 型スイッチでは性能面はかなわない
  • 実装の強制
    • 〇 仮想メソッドなら、抽象メソッドにしておけば派生クラスでの実装漏れがあり得なくなる
    • × 型スイッチの場合、caseへの追加忘れがあり得る
  • 実装を書ける場所
    • × 仮想メソッドはクラスの中にないとダメ
    • 〇 型スイッチなら拡張メソッドなど、クラスの外でも使える

基本的にはやっぱり仮想メソッドの方が性能・使い勝手の面で良かったりします。 ただ、仮想メソッド最大の問題は、クラスの中に書くのが必須ということです。 どうしてもクラスの中には書けない(クラスの作者自身が書けず、第三者が書く必要がある)場合というのはあって、 この場合は型スイッチを使う必要があります。

クラスの中に書くということは、そのクラスを使いたい人なら誰でも使うような汎用的な機能なはずです。 仮想メソッドはそういう汎用的な機能にしか使えないということになります。

一方で、使う人それぞれの固有の事情であれば、使う人の側が自分で書くことになるでしょう。

例えば、表示要件を考えてみます。 あるアプリでは、「x * x + 1」というように、プログラミング言語によくあるように、掛け算を*で表して文字列化したいかもしれません。 またあるアプリでは、「x×x+1」というように、ちゃんと数式フォントで、掛け算には×記号を使って表示したいかもしれません。 数式表示のためには、自前でレンダリングを行うべきかもしれませんし、 「<math><mi>x</mi><mo>×</mo><mi>x</mi><mo>+</mo><mn>1</mn></math>」というようなMathML文字列を作って、これを何らかのライブラリに解釈してもらうのがいいかもしれません。

数式データを使う用途もアプリごとに変わってくるでしょう。 あるアプリでは、数式を組版して表示すること自体が目的かもしれません。 またあるアプリでは、数式を微分したり方程式の解を求めたり、数学計算のために使うかもしれません。 あるいは、プログラミング言語を作っていて、式を計算するCPU命令を出力するための中間形式として使うかもしれません。

こういう、クラス作者側では用途が見えないものは、型スイッチを使って書くことになります。

補足: 型スイッチの性能

仮想メソッドと比べると遅いという話をしましたが、これは、仮想メソッドが性能よすぎるだけで、 型スイッチもそこまでひどい性能ではありません。 先ほどのCalculateの例でいうと、大まかに計測したところ4倍程度の差でした。

「型を見る」というと、リフレクションを想像する人がいるようです。 リフレクションを使う場合、確かに、2・3桁(2・3倍じゃなくて、桁が変わる)遅くなる場合があります。 しかし、型スイッチに必要なのは「その型に代入できるかどうか」だけで、これはそこそこ高速な処理です。 リフレクションで遅いのは、「ある型がどういうメンバーを持っているか調べる」であるとか、 「メソッド名を文字列で渡してメソッドを探して、そのメソッドを実行する」であるとかです。

要するに、リフレクションで取れる型情報や、それの使い方には何段階かあって、それぞれ負荷の度合いも変わります。

型情報の使い方と実行速度

型識別だけなら大したコストは掛かりません。そして、型スイッチが使うのはこの型識別情報だけです。

むしろ、型スイッチの遅さの原因は、 前項で説明したような、逐次判定のせいです。 上から1つ1つcaseの条件判定しているので、平均的にはcaseの数に比例した処理量が必要になります。

式とステートメント

$
0
0

概要

変数と式」で少し言葉としては出しましたが、 プログラミング言語の構文には大きく分けて式(expression)とステートメント(statement: 文、平叙文)という2種類のものがあります。

最近のプログラミング言語ほど式の比率が高くなっています。 C#でも、バージョンを重ねるごとに、式になっている構文が増えています。

基礎」と「構造化」のセクションで、C#の式とステートメントの結構な割合を紹介しました。ここで1度、この式とステートメントの区別についての話をしておきます。

式とステートメント

式とステートメントは、大まかに言うと以下のようなものです。

  • (expression): 割とどこにでも書ける代わりに戻り値が必須
  • ステートメント(statement): 戻り値がなくてもいい代わりに書ける場所がブロック内に限られている

式

ステートメント

ステートメント(文)
数式っぽい構文が多い 制御構文が多い
どこにでも書ける ブロック内(関数本体の中など)にしか書けない
いろいろ組み合わせて書ける そんなに組み合わせの幅はない
戻り値が必須 戻り値がない

式やステートメント(文)の一覧は、「C# の式と文の一覧」にまとめてあります。

書くことの予定

https://github.com/ufcpp/UfcppSample/issues/100

変数宣言式

$
0
0

概要

C# 7のいくつかの機能は、将来的にパターン マッチング(pattern matching)という機能に発展する予定です。

パターン マッチングは結構大きな言語機能ですが、 小さく切り出していった結果が型スイッチや分解になります。 これらは元々の大きな目標から切り出されたものだけあって、以下のような共通する性質があったりします。

  • 変数を宣言しつつ受け取れる
    • 式の途中でも変数宣言できる
  • 複数の値のうち一部だけを受け取り、残りを破棄したいことがある

変数宣言式

これらに共通する点の1つは、式の途中で変数を宣言できるようになるということです。

ここから発展して、任意の式の中で変数を宣言できるようにする予定があって、この機能を変数宣言式(variable declaration expression)といいます。 例えば以下のように書けます。

// (予定。C# 7では書けない) C# 8?
static int X(string s) => (int x = int.Parse(s)) * x;

(int x = int.Parse(s)) の部分の戻り値は、xに代入された値です。結局、以下のコードと同じ意味ですが、これが「式」として書けます。

static int X(string s)
{
    int x = int.Parse(s);
    return x * x;
}

値の破棄

型スイッチや分解では、変数を宣言しつつ何らかの値を受け取るわけですが、 特に受け取る必要のない余剰の値が生まれたりします。

例えば、分解の場合、複数の値のうち、1つだけを受け取りたい場合があったとします。 そういう場面が複数並んでしまった場合、以下のようなコードになりがちです。

static void Deconstruct()
{
    // 商と余りを計算するメソッドがあるけども、ここでは商しか要らない
    // 要らないので適当な変数 x とかで受ける
    var (q, x) = DivRem(123, 11);

    // 逆に、余りしか要らない
    // 要らないから再び適当な変数 x で受けたいけども、x はもう使ってる
    // しょうがないから x1 とかにしとくか…
    var (x1, r) = DivRem(123, 11);
}

static (int quotient, int remainder) DivRem(int dividend, int divisor)
    => (Math.DivRem(dividend, divisor, out var remainder), remainder);

「しょうがないから」感がひどく、どう見ても不格好です。

こういう時に使うのが、値の破棄(discard)です。 以下のように、_を書くことで値を無視できます。

{
    // _ を書いたところでは、値を受け取らずに無視する
    var (q, _) = DivRem(123, 11);

    // _ は変数にはならないので、スコープを汚さない。別の場所でも再び _ を書ける
    // また、本来「var x」とか変数宣言を書くべき場所にも _ だけを書ける
    (_, var r) = DivRem(123, 11);
}

1つ目の例では一見、_という名前の変数を定義しているようにも見えますが、別の挙動になります。 変数は作らず、スコープ内の別の場所でも再び_を使うことができます(先ほどの例みたいに_1みたいな変な名前を作らなくて済む)。

また、2つ目の例のように、「型名 変数名」みたいに書くべき場所でも、var _ではなく、_だけでOKです。

同様に、出力変数宣言でも_を破棄の意味で使えます。

// 欲しいのは戻り値だけであって、out 引数で受け取った値は要らない
static bool CanParse(string s) => int.TryParse(s, out _);

型スイッチでも同様です。

static int TypeSwitch(object obj)
{
    switch (obj)
    {
        case int[] x: return x.Length;
        case long[] x: return 2 * x.Length;
        // int でさえあれば値は問わない
        case int _: return 1;
        // 同、long
        case long _: return 2;
        case null: return 0;
        // 以下の行をコメントアウトするとエラーに
        // 今のところ、case _ は未実装(将来的に予定はあり)
        //case _:
        default: throw new ArgumentOutOfRangeException();
    }
}

_ が破棄の意味になる場合

_という記号は、元々のC#では識別子として有効な名前です。 すなわち、以下のコードは有効なC#コードです。

var _ = 10;
Console.WriteLine(_); // 10 が表示される

_を破棄の意味で使うということは、_の使い方を変えるということになります。

  • C# 7から導入される新しい構文の中では、_が常に破棄の意味になる
  • それ以前の構文では、1つも参照がなかった場合だけ_を破棄の意味で扱う(予定)

分解、出力引数宣言、型スイッチなど、C# 7から導入された構文の中では、 _が常に破棄の意味になります。 _という名前の変数は作られません。

static void Deconstruct1()
{
    // 要らないので適当な変数 x とかで受ける
    var (q, x) = DivRem(123, 11);

    // 要らないと言いつつ、参照できてしまう
    Console.WriteLine(x);

    // 要らないものは _ で破棄
    var (_, r) = DivRem(123, 11);

    // 分解の中に書いた _ は変数にはならない
    // 以下の行でコンパイル エラーになる(_ は存在しない)
    Console.WriteLine(_);
}

現状(C# 7のリリース候補版の時点)では、既存の構文に対しては破棄は使えません。 _は普通に変数扱いされます。

既存の構文で破棄を使いたいものの代表例は、ラムダ式の引数でしょう。 残念ながら今のところは破棄の意味で_を使えず、「_1」みたいな名前がいまだ必要になります。 例えば、以下のコードはコンパイル エラーになります。

static void Subscribe(INotifyPropertyChanged source)
{
    // 2個目の _ が「同じ名前被ってる」エラーになる
    source.PropertyChanged += (_, _) => Console.WriteLine("property changed");
}

ただし、今後の計画としては、既存の文法に対しても破棄の意味で _ を使えるようにしたいようです。 この場合、既存のC#コードを壊さないように、以下のような方針を取ります。

  • 宣言だけして1回も _ を使わなかったら破棄扱い
  • 1回でも使っていたら普通の変数・引数扱い

すなわち、以下のようなコードが書ける予定です。

static void Subscribe(INotifyPropertyChanged source)
{
    // (予定)C# 8?
    // 1回も _ を使わなかったら破棄扱い
    source.PropertyChanged += (_, _) => { };

    // _ を使っているのでこれは引数扱い。同名の別引数は作れない
    source.PropertyChanged += (_, _1) => Console.WriteLine(_);
}

[雑記] エントリーポイント

$
0
0

概要

C# では通常、1つのプログラムは複数の C# ソースコードからなり、そのソースコード中には複数の関数が含まれています。 その、多数ある関数の中で、プログラム起動時に最初に呼ばれるものをエントリーポイント(entry point: 入場地点)と呼びます。

C# のプログラムの基本構造」で例を出したように、 C# では、Mainという名前の関数が自動的にエントリーポイントになります。

(「関数」内でも補足していますが、 正確にいうと、Mainという名前のメソッドがエントリーポイントになります。)

[補足] C# スクリプト

スクリプト実行の場合は関数で囲わなくてもどこにでも処理を書けます。 Main関数も不要です。

Main の引数、戻り値

Mainの引数と戻り値は、以下のいずれかである必要があります。 これ以外のオーバーロードはエントリーポイントになりません。

static int Main()
static int Main(string[] args)
static void Main()
static void Main(string[] args)

(ただし、後述しますが、C# 7.1 からは戻り値としてTaskクラスが使えるようになりました。)

引数を持っている場合、引数にはコマンドライン引数が渡ってきます。 (引数なし版は、コマンドライン引数を受け取る必要がない時に使います。)

また、戻り値はプログラムの終了コードを返します。 Windows の場合は0が正常終了、1が部分的な成功、…などの意味があるようです。 戻り値なし版の場合は常に0(正常終了)扱いです。

Main がないタイプのプロジェクト

GUI アプリや Web アプリでは、Main関数を書かない場合があります。 この場合、以下のいずれかです。

  • 他のプログラムから呼び出される。どの関数から呼ぶから、呼び出し元次第
  • 開発者に見えないところで自動的にMainが作られている

例えば、ASP.NETの場合は前者、WPF アプリの場合は後者になります。

エントリーポイントの指定

1つのプログラムの中に複数のクラスがあって、 複数のクラスの中にMain関数がある場合、そのままではエントリーポイントを決定できず、コンパイル エラーになります。

この場合、どのMain関数を使うかをオプション指定できます。

参考:

非同期 Main

Ver. 7.1

C# 7.1で、以下のように、Main関数の戻り値にTaskクラス(System.Threading.Tasks名前空間)を使えるようになりました。

static Task<int> Main()
static Task<int> Main(string[] args)
static Task Main()
static Task Main(string[] args)

もちろん、非同期メソッドを使えるようにするためです。 例えば以下のようなMain関数が、ちゃんとエントリーポイントとして認識されます。

static async Task Main()
{
    for (int i = 10; i > 0; i--)
    {
        Console.WriteLine(i);
        await Task.Delay(1000);
    }

    Console.WriteLine("done.");
}

非同期 Main の仕組み

ちなみに、この機能は、コンパイラーが通常の(void/int戻り値の)エントリーポイントを別途自動生成することで実現しています。 例えば、先ほどの例のように、Task Main()を書くと、追加で以下のような関数が作られ、これが実際のエントリーポイントとして機能します。

// 実際には <Main> というような、C# で本来使えない名前で生成される
static void _Main_(string[] args)
{
    Main().GetAwaiter().GetResult();
}

中身はGetAwaiter().GetResult()を呼んでいるだけです。

通常の Main がすでにある場合

非同期 Main の仕様は C# 7.1 で追加されたものです。 そのため、これまでに書いたコードの中にすでに、エントリーポイントにするつもりがない Task Main() が含まれている場合に対する考慮が必要です。

C# 7.1 では、通常の(void/int戻り値の)Main関数がある場合、そちらだけをエントリーポイント扱いします。

static void Main(string[] args)
{
    Console.WriteLine("こちらがエントリーポイント扱い");
}

static async Task Main()
{
    Console.WriteLine("void Main(string[]) がある限り、こちらは呼ばれない");
}

C# 7.1 の新機能

$
0
0

※2017年6月現在、プレビュー版の情報です。正式リリースまでに、機能の増減や、細かい仕様の変更がありえます。

C# 7.1

Ver. 7.1
リリース時期 2017/?
同世代技術
  • Visual Studio 2017 Update 3
要約・目玉機能
  • C# 7.0のちょっとした改善

2017年6月時点で「Preview 2」の状態ですが、C# 7.0のリリース(2017年2月)から半年程度で C# 7.1 がリリースされそうです。

C# 7.0の頃から、目標としては C# のリリース サイクルの短縮を考えていました。 多くの機能を2・3年に1度一気にリリースするよりも、細かく出せるものに関しては短いリリース サイクルで出したいという意図です。 今回、(実質的に)初の「小数点リリース」となる C# 7.1 が誕生しそうです。

( 一応、C# 1.1があったんですが、ほとんど使われない機能が2つ追加されただけなので、1.1があったこと自体あまり認知されていないものです。)

C# 7.1 は、Visual Studio 2017のリリース時期に間に合わなかった C# 7.0 の積み残しと言った感じの、小さい機能が4つほど追加されています。

非同期Main

Mainメソッドの戻り値にTaskクラス(System.Threading.Tasks名前空間)を使えるようになりました。 以下のいずれかのオーバーロードであればエントリーポイントとして認識されます。

static Task<int> Main()
static Task<int> Main(string[] args)
static Task Main()
static Task Main(string[] args)

詳しくは、「非同期Main」で説明します。

default 式

これまでも既定値を作るために、default(T)という構文がありましたが、 型名Tの指定が煩雑でした。 特に、名前の長い型に対してdefault(T)を使うと、かなりのうっとおしさがあります。

既定値を結構使って、かつ、名前が長い型というと、例えばCancellationToken構造体(System.Threading名前空間)とかです。 以下のようなコードを書いたりします。

static async Task DefaultExpression(CancellationToken c = default(CancellationToken))
{
    while (c != default(CancellationToken) && !c.IsCancellationRequested)
    {
        await Task.Delay(1000);
        Console.WriteLine(".");
    }
}

これに対して、C# 7.1では、左辺(代入先)から推論できる場合に、(T)を省略してdefaultだけで既定値を作れるようになりました。 例えば先ほどのコードは以下のように書き直せます。

static async Task DefaultExpression(CancellationToken c = default)
{
    while (c != default && !c.IsCancellationRequested)
    {
        await Task.Delay(1000);
        Console.WriteLine(".");
    }
}

詳しくは「既定値」に追記するか、 推論がらみで1ページ足すかも。

タプル要素名の推論

タプルの要素名が、タプル構築時に渡した変数から推論できるようになりました。 例えば以下のように、(x, y) と書くだけで、1要素目にx、2要素目に y という名前が付きます。 (これまでだと、(x: x, y: y) と書く必要があった。)

var x = 1;
var y = 2;
var t = (x, y);

// C# 7.0。t の要素には名前が付かない
Console.WriteLine(t.Item1);
Console.WriteLine(t.Item2);

// C# 7.1。(x, y) で (x: x, y: y) 扱い
// t の要素に x, y という名前が付く
Console.WriteLine(t.x);
Console.WriteLine(t.y);

詳しくは「タプル」に追記予定。

ジェネリック型に対するパターン マッチング(型スイッチ)

C# 7.0でisswitchで型を見ての分岐ができるようになりました。 しかし、ジェネリクスが絡む場合、 例えば以下のようなコードはC# 7.0ではコンパイル エラーになっていました。

static void M<T>(T x)
{
    switch (x)
    {
        case int i:
            break;
        case string s:
            break;
    }
}

Tintstringとして処理できない」と言った旨のコンパイル エラーが出ます。

さらにいうと、以下のような需要が結構ありそうな場面でも、C# 7.0ではコンパイル エラーになりました。

class Base { }
class Derived1 : Base { }
class Derived2 : Base { }
class Derived3 : Base { }

// こういう、型制約付きのやつですら 7.0 ではダメだった
static void N<T>(T x)
    where T : Base
{
    switch (x)
    {
        case Derived1 d:
            break;
        case Derived2 d:
            break;
        case Derived3 d:
            break;
    }
}

C# 7.0でも、以下のように、as演算子を使った場合にはちゃんとコンパイルできます。 型スイッチは、内部的にはas演算子に展開される機能で、as演算子にできて型スイッチにできないことがあるのは不自然です。

static void N<T>(T x)
    where T : Base
{
    { var d = x as Derived1; if (d != null) { return; } }
    { var d = x as Derived2; if (d != null) { return; } }
    { var d = x as Derived3; if (d != null) { return; } }
}

そこで、C# 7.1では、上記コードのような、ジェネリックな型に対する型スイッチを使えるようになりました。 (新機能というよりは、仕様漏れ・バグ修正の類です。)

詳しくは「型スイッチ」に追記予定です。

C# 7.2 の新機能

$
0
0

C# 7.2

Ver. 7.2
リリース時期 未定
同世代技術
  • Visual Studio 2017 15.5
要約・目玉機能
  • 構造体と参照の活用

※ 現在、C# 7.2 はプレビュー版です。正式リリースまでに変更があり得ます。

少しずつ説明を埋めている最中です。

C# 7.2で追加された機能の多くは「構造体と参照の活用によってパフォーマンス改善」と言った感じのものです。 パフォーマンスが求められるようなライブラリの作者にとっては重要になりますが、 多くのC#開発者にとっては直接利用する機能ではないかもしれません。 ただし、そういった開発者にとっても、 「知らないうちに使っていた」とか「使っているライブラリがなんだか速くなった」というような、 間接的な恩恵が受けられるものです。

また、C# 7.1に引き続いての小さな更新がいくつかあります。

先頭区切り文字

0b0xの直後に区切り文字の _ を入れることができるようになりました。

// C# 7.0 から書ける
var b1 = 0b1111_0000;
var x1 = 0x0001_F408;

// C# 7.2 から書ける
// b, x の直後に _ 入れてもOKに
var b2 = 0b_1111_0000;
var x2 = 0x_0001_F408;

区切り文字に関しては「数字区切り文字」を参照してください。

非末尾名前付き引数

Ver. 7.2

前の方の引数を名前付きにできるようになりました。 例えば、以下のような書き方が許されるようになりました。

// C# 7.2
// 末尾以外でも名前を書けるように
Sum(x: 1, 2, 3);

詳しくは「オプション引数・名前付き引数」で説明します。

private protected

private protectedというキーワード(語順は自由)で、「protectedかつinternal」なアクセシビリティを指定できるようになりました。

private protected

詳しくは「実装の隠蔽」で説明します。

readonly の注意点

$
0
0

概要

定数」で、読み取り専用のフィールドが作れるという話をしました。 この時点ではまだクラス構造体値型と参照型の違いなどについて触れていなかったのでreadonly修飾子の簡単な紹介だけに留めましたが、 本項で改めてreadonlyについて説明します。

整数などの基本的な型に対して使う分には特に問題は起きないんですが、構造体やクラスなど、複合型に対して使うときには注意が必要です。

参照型のフィールドに対して readonly

readonlyに関して最も注意が必要な点は、readonlyは再帰的には働かないという点です。 readonlyを付けたその場所だけが読み取り専用になり、参照先などについては書き換えが可能です。

例えば以下のコードを見てください。Programクラスのフィールドcにはreadonlyが付いていますが、 cが普通に書き換え可能なクラスのフィールドなので、クラスの中身は自由に書き換えられます。

// 書き換え可能なクラス
class MutableClass
{
    // フィールドを直接公開
    public int X;

    // 書き換え可能なプロパティ
    public int Y { get; set; }

    // フィールドの値を書き換えるメソッド
    public void M(int value) => X = value;
}

class Program
{
    static readonly MutableClass c = new MutableClass();

    static void Main()
    {
        // これは許されない。c は readonly なので、c 自体の書き換えはできない
        c = new MutableClass();

        // けども、c の中身までは保証してない
        // 書き換え放題
        c.X = 1;
        c.Y = 2;
        c.M(3);
    }
}

参照型のフィールドに対してreadonlyを付ける例

クラスを書き換えできないように作る場合、クラス自体を書き換え不能に作りましょう。 (クラスの方で、フィールドをreadonlyにしたり、プロパティをget-onlyにします。)

値型のフィールドに対して readonly

クラス(参照型)とは対照的に、構造体(値型)の場合はデータを直接持ちます。 そのため、構造体のフィールドに対してreadonlyを付けると、構造体の中身も読み取り専用になります。 ただし、メソッドの呼び出しなどを行う際、コピーが発生するという別の注意が必要です。

例えば以下のように、readonlyが付いたフィールドc自体に加えて、cのフィールドも書き換えできません。

using System;

// 書き換え可能なクラス
struct MutableStruct
{
    // フィールドを直接公開
    public int X;

    // フィールドの値を書き換えるメソッド
    public void M(int value) => X = value;
}

class Program
{
    static readonly MutableStruct c = new MutableStruct();

    static void Main() => Allowed();

    private static void NotAllowed()
    {
        // これはもちろん許されない。c は readonly なので、c 自体の書き換えはできない
        c = new MutableStruct();

        // 構造体の場合、フィールドに関しては readonly な性質を引き継ぐ
        c.X = 1;
    }

    private static void Allowed()
    {
        // でも、メソッドは呼べてしまう
        c.M(3); // X を 3 で上書きしているはず?

        Console.WriteLine(c.X); // でも、X は 0 のまま

        //↑のコードは、実はコピーが発生している
        // 以下のコードと同じ意味になる

        var local = c;
        local.M(3);

        Console.WriteLine(c.X); // 書き換わってるのは local (コピー)の方なので、c は書き換わらない(0)

        Console.WriteLine(local.X); // もちろんこっちは書き換わってる(3)
    }
}

値型のフィールドに対してreadonlyを付ける例

この例の後半を見ての通り、メソッドは呼べてしまいます。 フィールドXは書き換えれないはずなのに、そのXを書き換えているメソッドMを呼んでもエラーになりません。 C# では、こういう場合に、readonlyであることを保証しつつメソッドを呼び出せるように、フィールドを一度コピーしてから、そのコピーに対してメソッドを呼ぶということをしています。

すなわち、コピーが発生してまずいような場合(例えば構造体のサイズが大きくてコピーにコストが掛かるとか)には、readonlyなフィールドを使うことで問題が発生することがあります。 この問題は、in引数などでも発生しまえます。 後述するreadonly structを使えばこの問題は少し緩和するので、そちらも参照してください。

構造体の this 書き換え

C# のreadonlyフィールドには少し片手落ちなところがあって、実は、構造体の場合にちょっとした問題を起こせたりします。

構造体のメソッドの中ではthisが「自分自身の参照」の意味なんですが、このthis参照は書き換えできてしまいます。 そのため、以下のように、readonlyで一見書き換えができなさそうなフィールドを書き換えてしまうことができます。

using System;

struct Point
{
    // フィールドに readonly を付けているものの…
    public readonly int X;
    public readonly int Y;

    public Point(int x, int y) => (X, Y) = (x, y);

    // this の書き換えができてしまうので、実は X, Y の書き換えが可能
    public void Set(int x, int y)
    {
        // X = x; Y = y; とは書けない
        // でも、this 自体は書き換えられる
        this = new Point(x, y);
    }
}

class Program
{
    static void Main()
    {
        var p = new Point(1, 2);

        // p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる

        // でも、このメソッドは呼べるし、X, Y が書き換わる
        p.Set(3, 4);

        Console.WriteLine(p.X); // 3
        Console.WriteLine(p.Y); // 4
    }
}

わざわざこんな紛らわしいことをしようとは思わないのでめったに問題になることはないんですが、一応は注意が必要です。 また、この問題は、次節で説明する通り、C# 7.2で少し緩和されます。

readonly struct

Ver. 7.2

C# 7.2で、構造体自体にreadonly修飾を付けられるようになりました。 readonlyを付けた構造体は以下のような状態になります。

  • 全てのフィールドに対して readonly を付けなければならなくなる
  • this参照もreadonly扱いされる

thisreadonly扱いになるので、前節のようなthis書き換えの問題は起きません。

using System;

// 構造体自体に readonly を付ける
readonly struct Point
{
    // フィールドには readonly が必須
    public readonly int X;
    public readonly int Y;

    public Point(int x, int y) => (X, Y) = (x, y);

    // readonly を付けない場合と違って、以下のような this 書き換えも不可
    //public void Set(int x, int y) => this = new Point(x, y);
}

class Program
{
    static void Main()
    {
        var p = new Point(1, 2);

        // p.X = 0; とは書けない。これはちゃんとコンパイル エラーになる
        // p.Set(3, 4); みたいなのもダメ

        Console.WriteLine(p.X); // 1 しかありえない
        Console.WriteLine(p.Y); // 2 しかありえない
    }
}

readonly struct によるコピー回避

前述の通り、(無印の)構造体のreadonlyフィールドに対してメソッドを呼ぶとコピーが発生するという問題があります。 これに対して、readonly structであれば、このコピーを回避できます。

例えば以下のように、ほぼ同じ構造・どちらも書き換え不能な構造体を作ったとして、readonly structになっているかどうかでコピー発生の有無が変わります。

using System;

// 作りとしては readonly を意図しているので、何も書き換えしない
// でも、struct 自体には readonly が付いていない
struct NoReadOnly
{
    public readonly int X;
    public void M() { }
}

// NoReadOnly と作りは同じ
// ちゃんと readonly struct
readonly struct ReadOnly
{
    public readonly int X;
    public void M() { }
}

class Program
{
    static readonly NoReadOnly nro;
    static readonly ReadOnly ro;

    static void Main()
    {
        // readonly を付けなかった場合
        // フィールド参照(読み取り)は問題ない
        Console.WriteLine(nro.X);

        // メソッド呼び出しが問題。ここでコピー発生
        // (呼び出し側では、「M の中で特に何も書き換えていない」というのを知るすべがないので、防衛的にコピーが発生)
        nro.M();

        // readonly を付けた場合
        // これなら、M をそのまま呼んでも何も書き換わらない保証があるので、コピーは起きない
        ro.M();
    }

    // これも問題あり(コピー発生)
    // in を付けたので readonly 扱い → M を呼ぶ際にコピー発生
    static void F(in NoReadOnly x) => x.M();

    // こちらも、readonly struct であれば問題なし(コピー回避)
    static void F(in ReadOnly x) => x.M();
}

C# 7.2 以降では、書き換えを意図していない構造体に対してはreadonly修飾を付けるのが無難でしょう。

また、「フィールド直接参照なら大丈夫だけど、メソッドを(プロパティも)呼ぶとコピー発生」という性質上、 書き換えを最初から意図している構造体の場合は、プロパティよりも、フィールドを直接publicにしてしまう方が都合がいいことがあります。

Span構造体

$
0
0

概要

Ver. 7.2

Span<T>構造体(System名前空間)は、span (区間、範囲)という名前通り、連続してデータが並んでいるもの(配列など)の一定範囲を読み書きするために使う型です。

この型によって、ファイルの読み書きや通信などの際の、生データの読み書きがやりやすくなります。 生データの読み書きを直接行うことは少ないでしょうが、通信ライブラリなどを利用することで間接的にSpan<T>構造体のお世話になることはこれから多くなるでしょう。

Span<T>構造体は、 .NET Core 2.1 からは標準で入ります。それ以前のバージョンや、.NET Framework では、System.Memoryパッケージを参照することで利用できます。

C# 7.2の新機能のうちいくつかは、この型を効率的に・安全に使うために入ったものです。 そこで、言語機能に先立って、このSpan<T>構造体自体について説明しておきます。

サンプル コード

連続データの一定範囲の読み書き

「一定範囲の読み書き」の説明に、まずは配列で例を示します。 例えば以下のような書き方で、配列の一部分だけの読み書きができます。

// 長さ 8 で配列作成
// C# の仕様で、全要素 0 で作られる
var array = new int[8];

// 配列の、2番目(0 始まりなので3要素目)から、3要素分の範囲
var span = new Span<int>(array, 2, 3);

// その範囲だけを 1 に上書き
for (int i = 0; i < span.Length; i++)
{
    span[i] = 1;
}

// ちゃんと、2, 3, 4 番目だけが 1 になってる
foreach (var x in array)
{
    Console.WriteLine(x); // 0, 0, 1, 1, 1, 0, 0, 0
}

このコードで、以下のような書き換えが発生します。

配列の一部分だけを読み書きする例

Span<T>構造体を作る部分は、以下のように、拡張メソッドでも書けます。

var span = array.AsSpan().Slice(2, 3);

このAsSpanは、System.SpanExtensionsクラスで定義されている拡張メソッドで、 配列全体を指す Span<T> を作るものです。 また、SliceメソッドはSpan<T>構造体の、さらに一部分だけを抜き出すメソッドです。

ちなみに、読み書き両方可能なSpan<T>に加えて、読み取り専用のReadOnlySpan<T>構造体もあります。

// 読み取り専用版
ReadOnlySpan<int> r = span;
var a = r[0]; // 読み取りは OK
r[0] = 1;     // 書き込みは NG

配列に限って言えば、「配列の一部分を指す型」として、昔からArraySegment<T>構造体(System名前空間)がありました。 しかし、以下のような差があります。

  • Span<T>は、配列だけでなく、いろいろなものを指せる
  • Span<T>の方が効率的で、読み書きがだいぶ速い

いろいろなタイプのメモリ領域を指せる

Span<T>は、配列だけでなく、文字列、スタック上の領域、.NET 管理外のメモリ領域などいろいろな場所を指せます。 以下のような使い方ができます。

using System;
using System.Runtime.InteropServices;

class Program
{
    static void Main()
    {
        // 配列
        Span<int> array = new int[8].AsSpan().Slice(2, 3);

        // 文字列
        ReadOnlySpan<char> str = "abcdefgh".AsReadOnlySpan().Slice(2, 3);

        // スタック領域
        Span<int> stack = stackalloc int[8];

        unsafe
        {
            // .NET 管理外メモリ
            var p = Marshal.AllocHGlobal(sizeof(int) * 8);
            Span<int> unmanaged = new Span<int>((int*)p, 8);

            // 他の言語との相互運用
            var q = malloc((IntPtr)(sizeof(int) * 8));
            Span<int> interop = new Span<int>((int*)q, 8);

            Marshal.FreeHGlobal(p);
            free(q);
        }
    }

    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr malloc(IntPtr size);

    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    static extern void free(IntPtr ptr);
}

部分参照

Span<T>は、配列や文字列の一部分を直接参照しています。

例えば、stringSubstringメソッドを使うと、部分文字列をコピーした新しい別のstringが生成されて、ちょっと非効率です。 これに対して、Span<char>Sliceを使えば、コピーなしで部分文字列を参照できます。

例えば以下のようなコードを書いたとします。

var s = "abcあいう亜以宇";

var sub = s.Substring(3, 3);
var span = s.AsReadOnlySpan().Slice(3, 3);

for (int i = 0; i < 3; i++)
{
    Console.WriteLine((sub[i], span[i])); // あ、い、う が2つずつ表示される
}

sub (Substringメソッドを利用)とspan (Sliceメソッドを利用)はいずれも、「3番目から3つ分」の部分文字列を取り出しています。 しかし、以下のように、subではコピーが発生し、spanでは発生しません。

Substring と Span の差

配列とポインターに両対応

Span<T>を使う利点は、配列とポインターの両方に、1つの型で対応できることです。

ネイティブ コードとの相互運用で有用なのはもちろん、 C# だけでプログラムを作るにしてもポインターを使いたいことが稀にあります (主に、パフォーマンスが非常に重要になる場面で)。

例えば以下のようなコードを考えます。 unsafe を使うと速い処理の典型例として、一定範囲を 0 クリアする処理を、ポインターを使って書いています。

// unsafe を使うと速い処理の典型例として、一定範囲を 0 クリアする処理
class Program
{
    // 作る側
    // ライブラリを作る側としては別に unsafe コードがあっても不都合はそこまでない
    static unsafe void Clear(byte* p, int length)
    {
        var last = p + length;
        while (p + 8 < last)
        {
            *(ulong*)p = 0;
            p += 8;
        }
        if (p + 4 < last)
        {
            *(uint*)p = 0;
            p += 4;
        }
        while (p < last)
        {
            *p = 0;
            ++p;
        }
    }

    // 使う側
    static void Main()
    {
        var array = new byte[256];

        // array をいろいろ書き換えた後、全要素 0 にクリアしたいとして

        // ライブラリを使う側に unsafe が必要なのは怖いし面倒
        unsafe
        {
            fixed (byte* p = array)
                Clear(p, array.Length);
        }
    }
}

コード中にも書いていますが、ここで問題になるのは、使う側に unsafe コードを強要する点です。 ライブラリを作る側は作る人の責任で多少危険なコードも書けますが、 どういう人が使うかはコントロールできないので、使う側に unsafe を求めるのはつらいです。 また、見ての通り、unsafefixedなどのブロックで囲う処理は面倒です。

そこで、通常、以下のようにいくつかのオーバーロードを増やすことになります。

// 使う側に unsafe を求めないために要するオーバーロードいろいろ
static void Clear(ArraySegment<byte> segment) => Clear(segment.Array, segment.Offset, segment.Count);
static void Clear(byte[] array, int offset = 0) => Clear(array, offset, array.Length - offset);
static void Clear(byte[] array, int offset, int length)
{
    unsafe
    {
        fixed (byte* p = array)
        {
            Clear(p + offset, length);
        }
    }
}

1セットくらいなら別にまだ平気なんですが、例えばコピー処理(コピー元とコピー先の2セット必要)とか、引数が増えるとかなり大変なことになります。

// Clear は1つしか引数がないのでまだマシ。
// コピー(コピー元とコピー先)とか、2つになるとだいぶ面倒に。

static void Copy(ArraySegment<byte> source, ArraySegment<byte> destination)
    => Copy(source.Array, source.Offset, destination.Array, destination.Offset, source.Count);
static void Copy(byte[] source, int sourceOffset, byte[] destination, int destinationOffset)
    => Copy(source, sourceOffset, destination, destinationOffset, source.Length - sourceOffset);
static void Copy(byte[] source, int sourceOffset, byte[] destination, int destinationOffset, int length)
{
    unsafe
    {
        fixed (byte* s = source)
        fixed (byte* d = destination)
        {
            Copy(s + sourceOffset, d + destinationOffset, length);
        }
    }
}
// 他にも、利便性を求めるなら、
// source, destination の片方だけが ArraySegment のパターンとか
// 片方だけがポインターのパターンとか(組み合わせなのでパターンが多くなる)

static unsafe void Copy(byte* source, byte* destination, int length)
{
    var last = source + length;
    while (source + 8 < last)
    {
        *(ulong*)source = *(ulong*)destination;
        source += 8;
    }
    if (source + 4 < last)
    {
        *(uint*)source = *(uint*)destination;
        source += 4;
    }
    while (source < last)
    {
        *source = *destination;
        ++source;
    }
}

この問題に対して、Span<T>であれば、この構造体1つで配列でもポインターでも、その全体でも一部分でも受け取れるので、 オーバーロードは1つで十分です。

// 作る側
// Span<T> なら配列でもポインターでも、その全体でも一部分でも受け取れる
static void Clear(Span<byte> span)
{
    unsafe
    {
        // 結局内部的には unsafe にしてポインターを使った方が速い場合あり
        fixed (byte* pin = &span.DangerousGetPinnableReference())
        {
            var p = pin;
            var last = p + span.Length;
            while (p + 8 < last)
            {
                *(ulong*)p = 0;
                p += 8;
            }
            if (p + 4 < last)
            {
                *(uint*)p = 0;
                p += 4;
            }
            while (p < last)
            {
                *p = 0;
                ++p;
            }
        }
    }
}

// 使う側
static void Main()
{
    var array = new byte[256];

    // array をいろいろ書き換えた後、全要素 0 にクリアしたいとして

    // 呼ぶのがだいぶ楽
    Clear(array);
}

安全な stackalloc

C# の速度最適化のコツの1つに、「ガベージ コレクションを避ける」というのがあります。 要は、可能であれば、クラスや配列の new を避けろという話になります。 (割かし「言うは易し」で、なかなかnewを避けるのが大変なことはよくありますが。)

例えば、ファイルからデータを読み出しつつ、何か処理をしたいとします。 データは一気に全体を見る必要はなく、一定サイズずつ(仮にここでは128バイトずつ)読んでは捨ててを繰り返せるものとします。 これまでであれば、以下のように、そのサイズ分の配列を new して使うことになります。

const int BufferSize = 128;

using (var f = File.OpenRead("test.data"))
{
    var rest = (int)f.Length;
    var buffer = new byte[BufferSize];

    while (true)
    {
        var read = f.Read(buffer, 0, Math.Min(rest, BufferSize));
        rest -= read;
        if (rest == 0) break;

        // buffer に対して何か処理する
    }
}

こういう場合に、これまでも、unsafe コードを使えば配列の new を避ける手段がありました。 stackallocというものを使って、スタック上に一時領域を確保できます。 (スタックはガベージ コレクションの負担になりません。) ただ、これだけのために unsafe コードを必要とするもの、ちょっとしんどいものがあります。

これに対して、C# 7.2では、Span<T>構造体と併用することで、unsafe なしで stackallocを使えるようになりました。 例えば先ほどのコードは、以下のように書き直せます。 このコードはunsafeなしでコンパイルできます。 (※ 最新の System.IO パッケージの参照が必要です。現状ではプレビュー版のみ。)

const int BufferSize = 128;

using (var f = File.OpenRead("test.data"))
{
    var rest = (int)f.Length;
    // Span<byte> で受け取ることで、new (配列)を stackalloc (スタック確保)に変更できる
    Span<byte> buffer = stackalloc byte[BufferSize];

    while (true)
    {
        // Read(Span<byte>) が追加された
        var read = f.Read(buffer);
        rest -= read;
        if (rest == 0) break;

        // buffer に対して何か処理する
    }
}

ちなみに、スタック上の領域確保は、あんまり大きなサイズにはできません。 一般的には、多くても数キロバイト程度くらいまでしか使いません。 そのため、確保したいバッファーのサイズに応じて、stackallocと配列のnewを切り替えたいと言ったこともあります。 そこでC# 7.2 では、以下のように、条件演算子でstackallocを使うこともできるようになっています。

Span<byte> buffer = bufferSize <= 128 ? stackalloc byte[bufferSize] : new byte[bufferSize];

また、unsafeが不要なことからもわかる通り、Span<T>との併用であればstackallocは安全です。 以下のように、範囲チェックが掛かって、確保した分を越えての読み書きはできないようになっています。

// Span 版 = safe
static void Safe()
{
    Span<byte> span = stackalloc byte[8];

    try
    {
        // 8バイトしか確保していないのに、9要素目に書き込み
        span[8] = 1;
    }
    catch(IndexOutOfRangeException)
    {
        // ちゃんと例外が発生してここに来る
        Console.WriteLine("span[8] はダメ");
    }
}

// ポインター版 = unsafe
static unsafe void Unsafe()
{
    byte* p = stackalloc byte[8];

    try
    {
        // 8バイトしか確保していないのに、9要素目に書き込み
        p[8] = 1;
    }
    catch (Exception)
    {
        // ここには来ない!
        // 結果、不正な場所に 1 が書き込まれてるはず(かなり危険)
        // それも、エラーを拾う手段がないので気づきにくい
        throw;
    }
}

Span の内部的な話

前節ではSpan<T>構造体の用途を見てきましたが、続いて、その中身がどうなっているかについて説明しておきます。

ArraySegment<T>よりもSpan<T>の方が高速な理由でもありますが、 Span<T>の中身は参照になっています。

比較のためにArraySegment<T>の中身から説明しましょう。 ArraySegment<T>は以下のようなメンバーを持った構造体です。

struct ArraySegment<T>
{
    T[] Array;
    int Offest;
    int Count;
}

ArraySegmentの中身

一方で、Span<T>構造体は、論理的には以下のようなメンバーを持った構造体です。 (「論理的には」と断っているのは、これをそのまま書くことはできないため。)

struct Span<T>
{
    ref T Reference;
    int Length;
}

Spanの中身

要するに、以下のような点が Span<T> の特徴になります。 (この他、Span<T>は .NET ランタイムが特別扱いしていくつか特殊な最適化を掛けてくれるため高速になります。)

  • 必要な範囲の先頭を直接参照しているので、+ Offset分の計算が省ける
  • ArrayOffsetと分けて持つ必要がないので、1メンバー分省サイズ
  • 配列に限らずどこでも(ポインターでも)参照できる

slow Span と fast Span

先ほど、Span<T>の中身には「論理的には」ref Tなフィールドがあるという話をしました。 ただ、 .NET の型システム上、フィールドに ref を付けることはできません。 実のところ、Span<T>はこういう「参照フィールド」を実現するためにちょっと特殊なことをしています。

fast Span (.NET Core 2.0 以降向けの Span)

.NET Core 2.0 では、ランタイム側で特殊処理を入れて、「参照フィールド」に相当する機能を使えるようにしました。 .NET Core 2.0 以降向けの Span<T> は以下のような構造になっています。 (coreclr レポジトリ内にソースコードがあります。)

struct Span<T>
{
    ByReferenct<T> _pointer;
    int _length;
}

ByReference<T> が特殊対応部分です。 ランタイム側で「この型は参照フィールドとして扱う」という特別扱いをすることで、所望の動作を得ています。

slow Span (旧来のランタイム向けの Span)

「.NET Core 2.0以降でしか使えません」ということになると使い勝手が悪すぎるため、 旧来のランタイム向けの「ちょっと遅い」Span<T>実装もあります。 (こちらはcorefx リポジトリ内にソースコードがあります。)

こちらは、概ね以下のような構造です。

struct Span<T>
{
    Pinnable<T> _pinnable;
    IntPtr _byteOffset;
    int _length;
}

Pinnable<T>はただのクラスです。 ガベージ コレクション管理下の参照と、管理外の参照を同列に扱えないからこういう構造になっています。 管理メモリ(配列)は _pinnable (ただのクラス)で扱い、管理外メモリ(相互運用で得たポインターやstackallocで確保したメモリ)は _byteOffset に直接ポインター値を入れて扱います。

結果的に、管理/管理外で条件分岐が必要だったり、構造体のサイズが大きくなるせいで、少し動作が遅くなります。 ただし、それでも、ArraySegment<T>を使うよりはだいぶ高速です。

参照フィールド

要するに、Span<T>構造体は、論理的には「参照フィールドと、長さのペア」です。 実際、「fast Span」な実装では、参照フィールドに相当するものを、ランタイム側の特殊対応で実現しています。

となると、Span<T>の取り扱いには少し注意が必要になります。 「参照戻り値と参照ローカル変数」で説明していますが、 参照渡しでは、参照先が必ず有効であることを保証するために、いくつかの制限を掛けています。 それと同じ制限がSpan<T>型の引数・変数・戻り値にも掛からなければいけません。

正確な条件などについては次節の「ref 構造体」で説明します(近々書く予定)。

ref構造体

$
0
0

概要

前項では、C# 7.2 の新機能と深くかかわる Span<T> 構造体という型を紹介しました。 この型は、論理的には (ref T Reference, int Length) というような、「参照フィールド」と長さのペアを持つ構造体です。 「参照」を持っているので、参照戻り値や参照ローカル変数と同種の「出所の保証」が必要です。 またSpan<T> には「スタック上に置かれている必要がある」(ヒープに置けない)という制限が必要です。

さらに、Span<T> に制限か掛かっている以上、「Span<T>を持つ型」にも再帰的に制限が掛かります。 「Span<T> を持つか持たないか」だけで挙動が変わるのでは影響範囲が大きすぎるため、 「Span<T> を持ちたければ ref という修飾が必要」という制約もあります。

ここでは、これらの Span<T> の「スタック上に置かれている必要がある」という制約や、「ref 構造体」について説明していきます。 (ref構造体という機能ではありますが、主用途がSpan<T>に関するものなので、span safety ruleと呼ばれたりもします。)

ref 構造体

Span<T> には制限が必要といっても、C# コンパイラーとしては Span<T> だけを特別扱いしたくはありません。 そこで、ref構造体 (ref struct)というものを導入しました。

ref構造体は、名前通り、ref 修飾子が付いた構造体です。 Span<T> 構造体自身にも ref 修飾子がついています。 そして、ref構造体をフィールドとして持てるのはref構造体だけです。

// Span<T> は ref 構造体になっている
public readonly ref struct Span<T> { ... }

// ref 構造体を持てるのは ref 構造体だけ
ref struct RefStruct
{
    private Span<int> _span; //OK
}

逆に言うと、ref 修飾子がついていない構造体や、クラスはref構造体をフィールドとして持てません。

// NG。構造体以外を「ref 型」にはできない
ref class InvalidClass { }

// ref がついていない普通の構造体は ref 構造体を持てない
struct NonRefStruct
{
    private Span<int> _span; //NG
}

そして、以下で説明する制約は、Span<T> 構造体だけでなく、すべての ref 構造体に対して掛かります。

戻り値で返せるもの

ref 構造体を戻り値として使いたい場合、 ref 戻り値・ref ローカル変数と同様に、大元をたどって調べて(フロー解析して)、返していいものかどうかを判定します。 以下のようなルールがあります(ref戻り値と同じルールです)。

  • 引数で受け取ったものは戻り値に返せます
  • ローカルで確保したものは返せません
  • 引数などを介して多段に参照している場合、コードをたどって大元が安全かまで調べます
// 引数で受け取ったものは戻り値で返せる
private static Span<int> Success(Span<int> x) => x;

// ローカルで確保したもの変数はダメ
private static Span<int> Error()
{
    Span<int> x = stackalloc int[1];
    return x;
}

// 多段の場合も元をたどって出所を調べてくれる
private static Span<int> Success(Span<int> x, Span<int> y)
{
    var r1 = x;
    var r2 = y;
    var r3 = r1.Length >= r2.Length ? r1 : r2;

    // r3 は出所をたどると引数の x か y
    // x も y も引数なので大丈夫
    return r3;
}

private static Span<int> Error(Span<int> x, int n)
{
    var r1 = x;
    Span<int> r2 = stackalloc int[n];
    var r3 = r1.Length >= r2.Length ? r1 : r2;

    // r2 がローカルなのでダメ
    return r3;
}

ちなみに、上記のErrorと似たようなコードでも、以下のコードはコンパイルできます。 ちゃんと「メモリ確保があったかどうか」を見ていて、「defaultであれば何も確保していない」という判定もしています。

// ちゃんと「メモリ確保」があったかどうかを見てる
// 同じようなコードでもこれは OK (default だと何も確保しない)
private static Span<int> Success1()
{
    Span<int> x = default;
    return x;
}

このルールは、ref構造体と、ref引数・ref戻り値の間でも働きます。 例えば、引数由来の Span<T>から得たref Tな戻り値にできます、ローカル由来のものはできません。

// 引数で受け取った Span 由来の ref 戻り値は返せる
private static ref int Success(Span<int> x) => ref x[0];

// ローカルで確保した Span 由来の ref 戻り値はダメ
private static ref int Error()
{
    Span<int> x = stackalloc int[1];
    return ref x[0];
}

readonly ref

C# 7.2 で追加された構造体がらみの修飾子にはreadonlyというものもあります。 readonly修飾は、一見、参照がらみの機能とは無関係に見えますが、実はこれも「参照として返せるかどうか」の判定に関係しています。

例えば以下のコードを見てください。

using System;

// ref だけ
ref struct RefToSpan
{
    private readonly Span<int> _span;
    public RefToSpan(Span<int> span) => _span = span;

    // 例え _span に readonly が付いていても、this 書き換えが可能
    public void Method(Span<int> span) { this = new RefToSpan(span); }
}

// readonly ref
readonly ref struct RORefToSpan
{
    private readonly Span<int> _span;
    public void Method(Span<int> span) { }
}

class Program
{
    public static void LocalToRef(RefToSpan r)
    {
        Span<int> local = stackalloc int[1];
        r.Method(local); // ここでエラーになる。r の中身が書き換えられることで、local が外に漏れる可能性を危惧

        // 注: この例の場合は実際には漏れはしないものの、RefToSpan の作り次第なので保証はできない
    }

    public static void LocalToRORef(RORefToSpan r)
    {
        Span<int> local = stackalloc int[1];
        r.Method(local); // readonly ref に対してなら OK
    }
}

ローカルで定義したSpan<T>を、引数で渡ってきたref構造体のメソッドに対して渡しています。 この場合、readonlyがついている場合にだけコンパイルできます。 readonlyがついていない方では、メソッドの中でrが書き換わる可能性があります。 その結果「ローカルのSpan<T>が外に漏れる可能性がある」という判定を受けるため、コンパイル エラーになります。 readonlyがついている方では「書き換えがあり得ない」ということで、「外にも漏れない」という判定になります。

余談: さすがに unsafe までは追えない

参照がらみのフロー解析は、あくまでrefローカル変数や、ref構造体に対してだけ働きます。 unsaefを使って、ポインターなどを介するとさすがに追跡できません。

例えば、以下のコードは不正で、実行時エラーであったり、予期しない動作を招く可能性があります。 しかし、コンパイラーが不正を判定できず、コンパイル時にエラーにすることができません。

unsafe static Span<int> X()
{
    // ローカル
    int x = 10;

    // unsafe な手段でローカルなものの参照を作って返す
    // これをやってしまうとまずいものの、コンパイル時にはエラーにできない
    return new Span<int>(&x, 1);
}

「スタックのみ」制約

ref構造体はスタック上に置かれている必要があります。 この性質から、ref構造体は「stack-only 型」と呼ばれることもあります。 この制限が必要になるのは以下の2つの理由からです。

  • そもそも参照自体がスタック上でしか働かない
  • マルチスレッド動作時に安全性を保証できない

まず、ref 構造体以前に、参照自体がスタック上でしか使えません。 参照は、常にその参照の出所をトラッキングする必要があります。 例えば、出所がクラス(.NET のガベージ コレクションの管理下)の場合、 それを参照する方もガベージ コレクションのトラッキングの対象になります。 このトラッキング処理を低コストで行うためには、参照がスタック上になければなりません。

次に、マルチスレッド動作に関してですが、 Span<T> の中身が論理的には (ref T Reference, int Length) という2要素からなることによります。 安全に使うには、この2つがアトミックに読み書きされなければなりません。 もし、Reference だけが書き換わり、Length がまだ書き換わっていないタイミングで参照先を読み書きされてしまうと、 範囲チェックが正しく働かず、不正な領域を読み書きしてしまう危険性が出てきます。

ということで、「スタック上に置かれている必要がある」という制約が掛かります。 具体的には、以下のような制限があります。

  • クラスのフィールドとして持てない(クラスに ref 修飾子を付けれない理由はこれ)
  • クラスのフィールドに昇格する可能性があることができない
  • ボックス化できない
    • objectdynamic、インターフェイス型の変数に代入できない
    • ToString など、object 型のメソッドを呼べない
    • インターフェイスを実装できない
  • ジェネリック型引数として使えない
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

//❌ そもそもクラスに ref を付けれないのも stack-only を保証するため
ref class Class { }

//❌ インターフェイス実装
ref struct RefStruct : IDisposable { public void Dispose() { } }

class Program
{
    //❌ 非同期メソッドの引数
    static async Task Async(Span<int> x)
    {
        //❌ 非同期メソッドのローカル変数
        Span<int> local = stackalloc int[10];
    }

    //❌ イテレーターの引数
    static IEnumerable<int> Iterator(Span<int> x)
    {
        Span<int> local = stackalloc int[10];
        local[0] = 1; //⭕ yield return をまたがないならOK
        yield return local[0];
        //❌ yield をまたいだ読み書き
        local[0] = 2; // ダメ
    }

    static void Main()
    {
        Span<int> local = stackalloc int[1];

        //❌ box 化
        object obj = local;

        //❌ object のメソッド呼び出し
        var str = local.ToString();

        //❌ クロージャ
        Func<int> a1 = () => local[0];
        int F() => local[0];

        //❌ 型引数にも渡せない
        List<Span<int>> list;
    }
}

余談: TypedReference

型付き参照」で説明しているTypedReference型も、内部的に参照を持っている型の1つです。 TypedReferenceにも参照に類する制限が元々あるんですが、実は、今回入ったref構造体とは似て非なる制限のかかり方をしています。

理想を言うとref構造体という仕様が入った今、TypedReferenceref構造体として扱う方がいいでしょう。 しかし、既存の挙動を変えてまでTypedReferenceref構造体の挙動をそろえたいとは誰も思わないので、 TypedReference の挙動はこれまで通りになります。ref修飾子も追加されていません。

[雑記] インライン化

$
0
0

概要

前述の通り、関数によって「同じ処理を何度も繰り返し書かない」、「意味のある単位で明確な名前を付ける」ということができ、プログラムを読みやすく・書きやすくすることができます。

一方で、ここでは、プログラムのパフォーマンスの面から関数を見てみましょう。関数呼び出しには多少のコストが掛かります。このコストをなくすため、コンパイラーによってインライン化という最適化が行われます。

関数呼び出しのコスト

読みやすさ・書きやすさの面は抜きにして、関数のパフォーマンス面だけを考えてみます。

関数呼び出しのパフォーマンス上のメリット・コスト

まずメリットですが、関数化によって重複コードが消えることで、プログラム全体のサイズが小さくなります。 サイズの減少量にもよりますが、基本的には小さい方が、プログラム自身の読み込み速度などの面で、実行速度的にもメリットになります。

一方で、関数化することで、関数の呼び出しや戻り時のジャンプにコストが掛かります。 また、共通化した結果、処理の前後を見ての最適化はかけづらくなります。

特に、関数の中身が小さい時には、コードの共通化によってサイズが減るメリットがほとんどなく、 ただ単にコストが掛かるだけになってしまいます。

インライン化

関数化にはコストが掛かるといっても、 パフォーマンス改善のために、関数化すべきところをわざわざ手作業でコピペ展開する必要はありません。 コンパイラーが自動的に最適化してくれます。

すなわち、「展開する方が確実に良い」と判定できる関数に対しては、関数の中身を呼び出しカ所に、コンパイラーが自動的に展開します。 この処理をインライン化(inlining: in-lineに埋め込む)やインライン展開(inline expansion)と呼びます。

インライン化の例

C# のインライン化

C# の場合、C# コンパイラー自身はインライン化を全くしません。 .NET ランタイムがILを解釈する際にインライン化が行われます。 すなわち、インライン化が掛かるタイミングはJITコンパイル時です。

実際にインライン化が掛かるかどうかはランタイムの実装依存で、仕様としては決まっていません。 現在インライン化が掛からない場合であっても、将来的には掛かるようになる可能性もあります。 公式にドキュメントがあるわけでもないのですが、非公式なブログ等の情報によると、以下のような判定を行うそうです。

  • C# コンパイル結果の IL 命令が32バイトを超える場合、インライン化しない
  • 反復処理を含む場合、インライン化しない
  • 例外処理を含む場合、インライン化しない

また、そもそも原理的にインライン化できない場合もあります。通常、仮想呼び出しになっている関数をはインライン化できません。 その結果、インターフェイスデリゲートを介した関数呼び出しはインライン化できません。

.NET は、ある程度インライン化の有無を制御する手段も提供しています。 以下のように、MethodImpl属性(System.Runtime.CompilerServices名前空間)を付けます。

// 積極的にインライン化してもらいたい
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int SumAgressive(int[] a)
{
    var sum = 0;
    foreach (var x in a)
    {
        sum += x;
    }
    return sum;
}

// 全くインライン化させたくない
[MethodImpl(MethodImplOptions.NoInlining)]
static int SumNo(int[] a)
{
    var sum = 0;
    foreach (var x in a)
    {
        sum += x;
    }
    return sum;
}

AggressiveInliningが付いている場合、前述の「32バイト」「反復処理・例外処理を含む」という条件が緩和されます。 あくまで「緩和」であって、無条件にインライン化されるわけではありません。 この例の場合は、「foreachループを含んではいるものの、関数の中身自体は十分に小さい」という条件なので、 何も属性を付けなければインライン化されず、AggressiveInliningを付けるとインライン化されます。

一方、NoInliningを付けると絶対にインライン化されなくなります。 わざわざ最適化を阻害するものなので、かなり特殊な用途でしか使わないでしょう。

インライン化によるパフォーマンス改善

このインライン化の有無によってどの程度性能が変わるかを見てみましょう。 以下に、計測用のコードを示します。

どちらもかなり関数の中身が小さいものなので、インライン化の有無が顕著に効いてきます。 単純な加算の方に至っては倍以上の速度差があります。

頻出経路の最適化

反復処理や例外処理でインライン化が阻害される性質を考えると、 阻害する部分だけを切り出してしまうことでプログラムを高速化できることがあります。

あくまで、以下のような限られた場面でしか使えないテクニックですし、高速化といっても数%程度のものではありますが、 実行速度が非常に重要になる場面では役立つでしょう。

  • 引数としてわたってくるものの頻度を予測できる
  • 高頻度で中身が単純な経路と、低頻度で中身が複雑な経路に分かれている

例えば以下のようなコードを見てみましょう。

static int Sum(int[] a)
{
    // ほとんどの場合、Length == 1 または 2 のところを通るという想定
    if (a.Length == 1) return a[0];
    else if (a.Length == 2) return a[0] + a[1];
    else if (a.Length >= 3)
    {
        // 反復がインライン化を阻害
        var sum = 0;
        foreach (var x in a)
        {
            sum += x;
        }
        return sum;
    }

    // 例外がインライン化を阻害
    throw new IndexOutOfRangeException();
}

単に配列の総和を取るコードですが、 「ほとんどの場合長さ1か2の配列しか来ない」というような前提で、 その長さ1か2の場合を特別扱いしているものです。

このSumメソッドは、反復処理と例外処理を含んでいるため、インライン化できません。 しかし、この反復処理と例外処理は、先ほどの前提から言うと、めったに通らない個所にあります。 そこで、以下のように書き換えます。

static int OptimizedSum(int[] a)
{
    // ほとんどの場合、Length == 1 または 2 のところを通るという想定
    if (a.Length == 1) return a[0];
    else if (a.Length == 2) return a[0] + a[1];
    else if (a.Length >= 3) return LongSum(a);
    ThrowIndexOutOfRange();
    return 0;
}

// インライン化を阻害しているものを外に追い出す
private static int LongSum(int[] a)
{
    var sum = 0;
    foreach (var x in a)
    {
        sum += x;
    }
    return sum;
}
private static void ThrowIndexOutOfRange() => throw new IndexOutOfRangeException();

めったに通らないくせにインライン化を阻害していたforeachループと例外のthrowを外に追い出しています。 その結果、OptimizedSumメソッド自体にはインライン化が掛かるようになり、関数呼び出しのコストが消えます。 数%程度ですが、これで高速化します。


[雑記] デリゲートの内部

$
0
0

概要

デリゲートは、内部実装的には「インスタンスと関数ポインターをペアで管理しているクラス」になっています。

ここではデリゲートの内部挙動と、 それを踏まえたパフォーマンス上の注意点を説明します。

デリゲートの内部

デリゲートは .NET ランタイム内で特殊な扱いをされていて、 デリゲート内部で起こっていることをそのまま C# で書くことはできないので、 ここでの説明は疑似コード的なものになります。

型定義

例えば、以下のようなデリゲートがあったとします。

delegate int F(int x);

これは内部的には以下のような扱いになっています。 概ね、インスタンスと関数ポインターのペアです。

class F : System.Delegate
{
    object Target;
    IntPtr FunctionPointer;
    // 実際には Delegate クラスのメンバー
    // あと、object がもう1個と、IntPtr がもう1個ある

    public F(object target, IntPtr fp) => (Target, FunctionPointer) = (target, fp);

    public virtual int Invoke(int x)
    {
        // return FunctionPointer(Target, x); 的な処理
    }
}

実際にはこの他に2つのフィールドがあると書いていますが、 1つはマルチキャスト用、 もう1つは後述する静的メソッドのために使うフィールドです。

デリゲートのインスタンス生成

C# では(C# 2.0 以降)、以下のように、デリゲート型の変数に対してメソッドを直接渡すような形でデリゲートを作ります。

// インスタンス メソッドから生成
var x = new Sample();
F i = x.Instance;

// 静的メソッドから生成
F s = Sample.Static;

これは省略形で、省略せずに書くなら以下のように、デリゲート型のインスタンスをnewします (C# 1.0 時代はこの書き方しかできない)。

// インスタンス メソッドから生成
var x = new Sample();
F i = new F(x.Instance);

// 静的メソッドから生成
F s = new F(Sample.Static);

ここで、先ほど説明した通り、デリゲートFのコンストラクターは内部的にはF(object, IntPtr)という形になっています。 そして、上記のコードは、実際にはこのコンストラクターを呼ぶように展開されます。

まずインスタンス メソッドの方は以下のような処理に展開されます。

  • インスタンスx を読み込む
  • メソッド Instance の関数ポインターを読み込む(IL にはそのための ldftn という命令がある。)
  • FのコンストラクターF(object, IntPtr)を呼び出す

静的メソッドの場合にも同じコンストラクターを呼びます。 object targetには null が渡ります。 すなわち、以下のような処理に展開されます。

  • nullを読み込む。
  • メソッド Static の関数ポインターを読み込む
  • FのコンストラクターF(object, IntPtr)を呼び出す

ただし、JIT 時の最適化でコンストラクター呼び出しの部分が書き換えられて、 最終的にはインスタンス メソッド・静的メソッドそれぞれ専用の別処理が呼ばれるようです。 静的メソッドの場合には、後述する「ちょっとしたトリック」のための追加の処理が掛かります。

呼び出し側(Invokeの中身)

デリゲートの呼び出しは以下のように書きます。

i(10);
s(20);

これも省略形みたいもので、省略せずに書くとInvokeメソッドの呼び出しになっています。

i.Invoke(10);
s.Invoke(20);

ただし、JIT 時の最適化でInvokeメソッド呼び出しの部分が書き換えられて、 最終的には以下のような処理が残ります。

  • デリゲートの Target フィールドを読み込む
  • 引数のint (上記の例の 10 や 20)を読み込む
  • デリゲートの FunctionPointer に格納してあるアドレスにジャンプ

静的メソッドを渡すと遅い

インスタンス メソッドと静的メソッドは、内部的には実のところだいぶ異なる引数の受け取り方をしています。 インスタンス メソッドは、以下のように、静的メソッドよりも暗黙的に1引数多く受け取っています。

class Sample
{
    static void StaticMethod(int x)
    {
        // 静的メソッドの場合は正真正銘、引数は x の1つだけ
    }

    void InstanceMethod(int x)
    {
        // 引数が1つだけに見えて…

        // 実は暗黙的に this を受け取っている
        Console.WriteLine(this);
    }

    // ということで ↑の InstanceMethod は、以下のような静的メソッドと同じ引数の受け取り方をしてる
    static void InstanceLikeMethod(Sample @this, int x)
    {
        Console.WriteLine(@this);
    }
}

このことを踏まえた上で、 前節の最後で説明したデリゲート呼び出しの手順を改めてみてみます。

  1. デリゲートの Target フィールド(静的メソッドの時には null が入っている)を読み込む
  2. 引数のint を読み込む
  3. デリゲートの FunctionPointer に格納してあるアドレスにジャンプ

デリゲートはインスタンス メソッドを参照していることもあれば、 静的メソッドを参照していることもあります。 しかし、呼び出し側では(インスタンス/静的によらず)常にこの手順で引数を渡しています。 すなわち、インスタンス メソッドの場合には素直に呼び出せるんですが、 静的メソッドの場合には内部的にちょっとしたトリックが働いています。

デリゲートに対して静的メソッドを渡すと、FunctionPointerには以下のような処理をする別のメソッドが入ります。

  • 1. で読み込んだインスタンスを無視して、引数の int だけを並べ直す
  • 改めて、本来の静的メソッドにジャンプする

この処理は意外と負担が高くて、デリゲートに対して静的メソッドを渡した場合、その呼び出しはかなり遅いです (参考: 計測コード)。

要するに、デリゲートはインスタンス メソッドの時に処理が単純で高速になるように作られていて、 その代わりに静的メソッドが低速です。 C# ではインスタンス メソッドの方が圧倒的に利用頻度が高いので、 インスタンス メソッドに対して最適化した方が、全体としてのパフォーマンスは上がります。

カリー化デリゲート

前節の「静的メソッドに対するトリック」を回避して、 デリゲート越しの静的メソッドの呼び出しを速くする方法が1つあります。 「拡張メソッドのデリゲートへの代入」で説明しているカリー化デリゲートという手段を使うと、インスタンス メソッドと同じコストで静的メソッドを呼べます。

拡張メソッドは、実体としては以下のように、第1引数でインスタンスを受け取る構造になっていて、 これがインスタンス メソッドの暗黙的な this 引数と同じ受け取り方になります。

class Sample
{
    public void InstanceMethod(int x)
    {
        // 引数が1つだけに見えて、実は暗黙的に this を受け取っている
    }

    // ということで ↑の InstanceMethod は、以下のような静的メソッドと同じ引数の受け取り方をしてる
    static void InstanceLikeMethod(Sample @this, int x)
    {
    }
}

static class SampleExtensions
{
    // であれば、こういう拡張メソッドも InstanceMethod と同じ引数の受け取り方になる
    public static void ExtensionMethod(this Sample @this, int x)
    {
    }
}

そこで、C# では、以下のように拡張メソッドに対して、インスタンス メソッドと同じようなデリゲートの作り方を認めています (x.E のような書き方を、カリー化デリゲートと呼びます)。

var x = new Sample();

Action<int> i = x.InstanceMethod;

// 拡張メソッドに対して、インスタンス メソッドと同じようなデリゲートの作り方を認めてる
Action<int> e = x.ExtensionMethod;

iの方もeの方のどちらも、以下のように扱われます。

  • インスタンスx を読み込む
  • メソッド InstanceMethod/ExtensionMethod の関数ポインターを読み込む
  • Action<int>のコンストラクターAction<int>(object, IntPtr)を呼び出す

通常の静的メソッドの場合と違って前述のトリックのための別処理への分岐も掛からず、 内部的にも完全に同じ処理になります。 呼び出しの際にもインスタンス メソッドと同じ処理になるため、 カリー化デリゲートは呼び出しは高速になっています。

(最適化手法1) 普通の静的メソッドを拡張メソッドに置き換え

ちなみに、こういう内部挙動の結果、 以下のように、静的メソッドに対してダミー引数を1つ増やしてわざわざ拡張メソッド化する高速化手法が使えたりします。

using System;

static class Program
{
    // 普通の静的メソッド
    static int F(int x) => 2 * x;

    // わざわざ使いもしない第1引数を増やして、拡張メソッドに変更
    static int F(this object dummy, int x) => 2 * x;

    static void Main()
    {
        // 静的メソッドからデリゲート作成
        Func<int, int> s = F;

        // わざわざ null を使ってカリー化デリゲートにする
        Func<int, int> e = default(object).F;

        // 以下の2つの呼び出しでは、e (カリー化デリゲート)の方が圧倒的に高速
        s(10);
        e(10);
    }
}

(最適化手法2) 匿名関数を拡張メソッドに置き換え

ちょっとした変換処理などに対して、匿名関数を使うよりも拡張メソッドを挟んだ方が速くなることもあります。

単純な例として、あるインスタンスを返すだけのラムダ式を、 拡張メソッドに置き換えることで高速化してみましょう。 以下のように書けます。

using System;

class Program
{
    // Func 越しに何かのインスタンスを取りたい
    static void M(Func<string> factory)
    {
        Console.WriteLine(factory());
    }

    static void Main()
    {
        // でも、呼ぶ側としては単に何かインスタンスを1個渡したいだけ
        string s = Console.ReadLine();

        // そこで、ラムダ式で1段覆って、string から Func<string> を作る
        // これだと、匿名関数の仕様から、匿名のクラスが作られて、その new のコストが余計にかかる
        M(() => s);

        // 一方で、以下のように、拡張メソッドを介することで、カリー化デリゲート(速い)になる
        M(s.Identity);
    }
}

static class TrickyExtension
{
    // 素通しするだけの拡張メソッドを用意
    public static T Identity<T>(this T x) => x;
}

この例の「素通し」よりもう少し複雑な場合でも同様です。 いくつか例を挙げると、以下のような場合にも同様の手法が使えます。

匿名関数(特にラムダ式)と比べるとはるかに手間がかかる書き方なので使い勝手はかなり悪いですが、 よっぽど「速度最優先」な場合には有効です。

プロジェクト管理

$
0
0

Visual Studio で C# を使ったプログラム開発を行う場合、まず、「プロジェクト」というものを作ります。 また、この際、「ソリューション」というものが一緒に作られます。

ここでは、そのプロジェクトの作り方の簡単な紹介と、プロジェクトやソリューションが何なのか、どうして必要なのかを説明します。

概要

Visual Studio で C# を使ったプログラム開発を行う場合、まず、「プロジェクト」というものを作ります。 また、この際、「ソリューション」というものが一緒に作られます。

ここでは、そのプロジェクトの作り方の簡単な紹介と、プロジェクトやソリューションが何なのか、どうして必要なのかを説明します。

ポイント
  • プロジェクト: 1つのプログラムを作るのに必要な各種ファイルを管理するもの
  • ソリューション: 1つの課題を解決するのに必要な一連のプロジェクト(≒ プログラム)を管理するもの

ちなみに、プログラムを作るのにどうして「プロジェクト」という単位が必要になるのかや、 どういう単位で「プロジェクト」を区切るのかなどは 「プロジェクトの分割」で説明します。

プロジェクトの作成

プロジェクトやソリューションの説明は次節以降でしますが、まずは、そのプロジェクトの作り方を簡単に紹介します。 (※ここでは、Visual Studio 2015を使ってスクリーンショットを撮っています。他のバージョンでもそれほど大きな違いはありません。) Visual Studio を起動すると以下のような画面になっていると思います。

Visual Studio 起動直後

ここで、「スタート」→「新しいプロジェクト」を選ぶと、以下のようなダイアログが表示されます。

「新しいプロジェクト」で「プロジェクト テンプレート」を選択

この中から、作りたいプログラムの種類に応じた「プロジェクト テンプレート」(後述)を選びます。

この「C# によるプログラミング入門」での説明の多くは「コンソール アプリケーション」で動きます。ここでも「コンソール アプリケーション」を選びます。通常は、「名前」のところには作りたいプログラムの名前に応じた適切な名前を入力します。今回はとりあえず、最初から入っている「ConsoleApplication1」という名前のままでプロジェクトを作ってみましょう。これで、以下のような画面になるはずです。

プロジェクト作成直後

これで、プログラム作成に必要な最低ラインの準備が整いました。ちなみに、この時点で(プロジェクトの新規作成をしただけで)、ソリューションというものも一緒に作られています。 以降では、ここで出てきた「プロジェクト」「ソリューション」「プロジェクト テンプレート」などの言葉について説明していきます。

プロジェクトとは

プログラムの作成・実行 で説明するように、ソースコードをコンパイルすることでプログラムを作ります。 通常、ソースコードは切りのいい単位ごとに別ファイルに分けて作るので、プログラムの作成には複数のソースコード ファイルが必要になります。 また、プログラムの種類によっては、ソースコード以外にも画像や音声など、様々な素材(アセット(asset)と呼んだりします)が必要です。

これら、プログラムを作るのに必要なファイルを管理するのがプロジェクトです。 例えば、下図の場合、ConsoleApplication1 という名前のプロジェクトに、App.config、Logic.cs、Program.cs という3つのファイルが管理されています。

enter image description here

その他、C#のプロジェクトでは以下のようなものを管理します。

  • ライブラリ の参照
  • コンパイル オプション(プログラム名、C# のバージョンや、コンパイル時の警告の厳しさなど)の設定
  • プログラムの種類によっては、インストーラー作成や、サーバーやストアへのアップロード方法などの設定

ソリューションとは

何かやりたいこと(課題)があるからプログラムを作るはずです。 そしてそのやりたいことの実現(解決)には、1つのプログラムではなく、複数のプログラムを作る場合があります。 あるいは、それらのプログラムから共通利用する「部品」も必要になったりします。

このような、やりたいことを実現(課題を解決)するために必要な複数のプログラムや部品(その1つ1つがプロジェクトとして管理される)を束ねるのがソリューション(solution: 解決策)です。

以下の図は、例として、「アプリ1」「社内ツール1」「社内ツール2」という3つのプログラムと、この3つから使う「共通部品」という、合わせて4つのプロジェクトを作った例です。 この3つのプロジェクトを管理するのが「アプリ1」ソリューションです。

4つのプロジェクトを含むソリューションの例

ソリューション内には、プロジェクト間の依存関係などが記録されています。

プロジェクト テンプレート

プログラムに必要なソースコードなどのファイルは、どんなプログラムでも同じなような必要最低限のものであっても、結構な分量があります。 どんなプログラムでもだいたい同じになるなら、最初からひな形を用意しようというのが、プロジェクト テンプレート(project template: プロジェクトのひな形)です。

いくつか紹介しましょう。

テンプレートの種類説明
コンソール アプリケーション “古き良き”文字だけのプログラムのことをコンソール アプリケーション(console application: console は操作卓という意味。昔はコンピューターをキーボードからの文字入力だけで操作していたのの名残)といいます。
ASP.NET Web アプリケーション Web アプリを作ります。ASP.NET は、C#でWebアプリを作るためのフレームワークの名前です。
Blank App (Windows Universal) (Windows 10以降用の)Windows GUIアプリを作ります。C#でGUIアプリを作るためのフレームワークは、詳細は省きますが、歴史的経緯で、他にも WPF、Windows Formsなどたくさんあります。

とにかく習うより慣れろで見た目に面白いものや、実用的なものを作りたいという方は、Web アプリや GUI アプリから始めるのもいいでしょう。

作るのが簡単なのはコンソール アプリです。 このサイトでは、C# というプログラミング言語や、プログラミングに関する概念的な説明が主題なので、ほとんどのサンプルがコンソール アプリになっています。

その他にもいろんな種類のテンプレートが用意されています。例えば、Visual Studio 2015では、iOSやAndroidなど、マイクロソフト以外の製品向けのアプリ開発ができるテンプレートも用意されています。選べるプログラミング言語も、C#の他、C++、Visual Basic、JavaScript、TypeScriptなどたくさんあります。Visual Studioが標準で用意しているものの他にも、拡張機能(いろんな会社から、商用・非商用問わずたくさん出ています)を入れることでより多くのテンプレートを利用できます。

Code-Awareなライブラリ

$
0
0

.NET Compiler Platformによって、Code-Awareなライブラリを作れるようになりました。 Code-Aware、直訳すると「コードを理解している」「コードを意識している」という感じになります。ライブラリだけでなく、ライブラリ利用側のコードを理解して、問題点の指摘やその修正方法まで提供することを言います。

概要

Ver. 6

(C# 6の言語的な機能ではありませんが、C# 6と同世代(Visual Studio 2015世代)の技術なので、利用できるのはC# 6以降になります。)

.NET Compiler Platformを使うことで、 コードを解析してその問題点の指摘やその修正方法を提供できる、コード アナライザーというものを作れます。

ライブラリを作る際、そのライブラリに特化したコード アナライザーを作って同梱して配ることで、 「ライブラリ自身が、ライブラリ利用側のコードを理解して、問題点の指摘やその修正方法まで提供」ということが実現できます。

こういう、「利用側のコードを理解するライブラリ」を、Code-Aware なライブラリと呼びます。

サンプル

https://github.com/ufcpp/UfcppSample/tree/master/Chapters/DevEnv/CodeAwareLibrarySample

コード アナライザー

(アナライザー自体の説明はたぶんそのうち別ページに分ける。分けたあかつきには: 1. C#はリアルタイムに、書いてるそばからエラー・警告出してもらえる言語。 2. Visual Studio以外もRoslyn対応そのうちするはず。Xamarinは近々?VSチームがOmniSharpにも協力してたはず。)

C# 6 の新機能の冒頭で触れていますが、しばらくの間、C#コンパイラーの再実装に工数を割いていて、C# 6は言語機能としては大きなものがあまりありません。言語的に大きな機能追加がなかった代わりと言ってはなんですが、新しくなったコンパイラーには別の「売り」があります。それが、コード アナライザーの作成です。

コード アナライザーは、「コードを解析するもの」という名前通り、C#ソースコードを解析して、問題点を指摘したり、その修正機能を提供するものです。こういう解析機能を、誰でも自由に作れるようになりました。

その結果、C#コンパイラー自身では提供しにくいような、以下の様なコード解析が実現できます。

  1. 経験則的な警告・エラーの提示
    • 機械的な判断だと間違う可能性のあるものは、コンパイラー機能としては実装しにくい
  2. チーム内でのローカル ルールの解析
    • ルールはチームごとに違ったりするので、コンパイラーにはわからない
  3. ライブラリ固有の事情
    • 特定のライブラリのためだけのコード解析は、標準では提供すべきではない

本稿の主題は3つ目の「ライブラリ固有の事情」になります。

ライブラリ固有の事情の例

簡単な例を上げてみましょう。以下のようなコードをライブラリ化することを考えます。

namespace FluentArithmetic
{
    public static class FluentExtensions
    {
        public static int Add(this int x, int y) => x + y;
        public static int Sub(this int x, int y) => x - y;
        public static int Mul(this int x, int y) => x * y;
        public static int Div(this int x, int y) => x / y;
    }
}

a + b - c などと書く代わりに、a.Add(b).Sub(c) と書けるようにするコードです。特に使い道はないんですが、「ライブラリ固有の事情」の説明にはなかなか手頃だと思います。

単純な四則演算なわけですから、いくつかの経験則が働くでしょう。ここでは2つほど挙げると、以下のようなものがあります。

  • 1.Div(0) というような、「0割り」は実行してみるまでもなくエラーになることがわかっている
  • 1.Add(2) というような、リテラル同士の演算は、3 などに置き換えて最適化できる

Div(0) と書いたら常にエラーを起こす」というのは完全にこのライブラリ固有の事情になります。このライブラリのこのDivメソッドに限り、常に実行時エラーを起こすんだからもうコンパイル時にエラーにしたい。しかし、コンパイラーは、そんな特定のライブラリだけの特殊対応なんてできません。こんな時こそコード アナライザーの出番です。

ライブラリ固有のコード アナライザーの例

ということで、以下のような機能を提供するコード アナライザーを考えます。

  • Div(0) を見たら問答無用でコンパイル エラーにする
  • 1.Add(2) とかのリテラル同士の演算は最適化できる旨、情報を出す
    • この最適化を自動的に行う「クイック アクション」を提供する

実際に実装してみた感じが以下の通り。

まず、Div(0)エラー。ちゃんとコンパイル エラーです。直さないとコンパイルできません。

Div(0) エラー

次が、1.Add(2) は最適化できるという情報。この場合、別にエラーにもならないし、警告も出ません。

リテラル同士の演算

エラーにも警告にもなりませんが、該当箇所には電球(lightbulb)マークがついています。この電球をクリックする(もしくは、Ctrl+. ショートカットキーを押す)と、コードの自動修正(code fix)が働きます。

電球マークをクリック

実行すると以下のようになるはずです。

自動修正の結果

ちなみに、Visual Studio的には、この「電球マークを起点として何か処理を掛ける」操作を「クイック アクション」といいます。

Code-Aware ライブラリ

サンプルは実際にこの例のライブラリとそれ用コード アナライザーを実装したものです。

  • FluentArithmetic.dll: ライブラリ本体。a.Add(b).Sub(c) という類の書き方で整数の四則演算をするためのライブラリ。
  • FluentArithmeticAnalyzer.dll: FluentArithmetic 専用のコード アナライザー。

ライブラリ専用なわけですから、コード アナライザーはライブラリ本体とセットで配布すべきでしょう。 そうすることで、ライブラリ自身がライブラリ利用側のコードを見て、問題点の指摘やその修正方法まで提供できる状態になります。こういう状態のライブラリを、「利用側のコードまで理解する」、「利用側のコードを意識している」という意味で「Code-Aware」と言います。

要するに、以下の様なパッケージを作って配布します。

  • ライブラリと、それ用コード アナライザーを同梱する
  • パッケージ インストール時に走るスクリプトで、コード アナライザーへの参照を足す

(書きかけ)以下、作成手順の説明

  • テンプレート通りに「Analyzer with Code Fix (NuGet + VSIX)」を作れば大体それっぽいものできてる
    • nuspec とか、install.ps1 最初からある
  • これに、ライブラリ本体の参照を足す
  • 今回の場合、nuspec とか install.ps1 とか、「ビルド後に実行するコマンドライン」でNuGetコマンドを呼ぶ処理は別プロジェクトに分離・移動
  • コード アナライザーの動作確認は Test プロジェクトの実行で
    • Vsix プロジェクトを実行すると別 Visual Studio が立ち上がってデバッグ実行できるんだけど、重いし、エラーが出た時悲惨だし
  • 一応 NuGet Gallary においてある: https://www.nuget.org/packages/FluentArithmetic/

(書きかけ)どういう場合に有効かの例をもう何例か列挙

  • JSONパーサー ライブラリ同梱で、文字列リテラル中のJSONを解析
  • 正規表現ライブラリ同梱で、Regex("ここの解析")

プロジェクトの分割

$
0
0

プログラムや、プログラムを作るための部品は「プロジェクト」という単位で管理します。

ここでは、プロジェクトという単位に分ける動機などについて説明していきます。

概要

プロジェクト管理で触れたように、プログラムや、プログラムを作るための部品は「プロジェクト」という単位で管理します。

  • 依存関係を考える最小単位がプロジェクト
  • .NETの場合、プロジェクトの成果物は「アセンブリ」(dllもしくはexe)
  • いろんなプログラム、いろんな環境で使える部分を別プロジェクトに切り出して使う

依存関係

プログラムをちゃんと部品として分けて作って、その部品をいろんなプログラムから再利用するためには、依存関係の整理が必要です。

依存関係(dependecy)について説明するために、まずはクラスの依存について考えてみます。例えば、以下の図のようなコードを見てください。

クラスの依存関係

図中の矢印が「依存」です。あるクラスが別のクラスから使われているという状態。

この例は、ゲームでよくありそうなデータ構造です。

  • 各ユーザー(User)が、ユニット(Unit)を複数持っていて、1つの同盟(Alliance)に入っている。
  • 各同盟には複数のユーザー(User)がメンバーとして含まれている。
  • 各ユニットは装備品(Equipment)を複数装備している。

今回の場合、1クラス1ファイルで分けているので、ソースコード ファイル的にも以下のような依存関係があります。

ソースコード ファイルの依存関係

この関係からは、以下のようなことが言えます。

  • Equipment.cs は単体で切り出せる。
  • Unit.csを使うにはEquipment.csもセットで必要。
  • User.csを使おうとすると、芋づる式にAlliance.csUnit.csEquipment.csすべてが必要。
  • User.csAlliance.csは相互依存していて、絶対に切り離せない。

このように、依存関係を整理することによって、「何かを抜き出して使うには別の何かが必要になる」というような状況が見えてきます。

補足: .NETのソースコード ファイル配置

(C#を含め).NETでは、.csファイルなどのソースコードと、クラス、メソッドなどのC#文法構成要素との間に何の制約もありません。A.csというファイルにBというクラスがあっても構いません。「クラスの分割定義」ができるので、複数のファイルにわたって1つのクラスを書くこともできます。

(他の言語だと、ファイル名を使って参照解決したり、ファイル名とクラス名が同じでないといけないという制約があったりするものもあります。)

ただ、一般的に推奨される方針としては、名前空間と同じフォルダー階層の下に、クラス名と同じ名前の.csファイルを作る方がよいとされています。

プロジェクト

これまでファイル単位で説明してきましたが、クラスやファイルという単位は細かすぎて、依存管理には不向きです(管理しきれない)。もう少し大きな単位で区切る必要があって、これが「プロジェクト」を作る動機です。

enter image description here

プロジェクト内のファイルやクラスに関しては、依存関係をあまり気にする必要はありません。仕組み上は、どれだけ複雑な依存関係を持っていても構いません。

(ただし、もちろん、「最初は1つのプロジェクトとして作ってしまったけども、この部分は汎用的に使えそうだから抜き出して別プロジェクトにしよう」というようなこともよくあるので、プロジェクト内であっても依存関係には気をつけておいた方が後々よい結果になることはあります。)

一方で、プロジェクト間の依存関係はきちんと考えないと行けません。ここをサボると、何かを抜き出して使いたい時に、芋づる式にいろいろなものが必要になって困ることがあります。必要な物が増える事による問題の例をいくつか挙げると、以下のようなものがあります。

  • 必要なコードが増えて、最終的なプログラムのサイズが肥大化する
  • 依存先がアップデートされたときの追従が大変になる
  • 動かせないリスク、例えば、「Windowsプログラムで今まで動いていたコードをiOSアプリで使いたい」とか言う時に動かせない可能性が高まる(依存先すべてがiOS実行に対応していないと行けない)

プロジェクトの成果物

プロジェクト内のファイルは個別に切り出して使うことをあまり想定しません。プロジェクトというものが、プログラム、あるいは、プログラムを作るための部品の最小単位です。そのため、プロジェクトによって得られる成果物は、1つのファイルにまとめたりします。

C#など、.NET言語の場合は、複数のソースコードのコンパイル結果を最初から1つのファイルにまとめて出力します。(他のプログラミング言語では、バラバラに出力されたコンパイル結果を ZIP 形式ファイルなどにまとめたりするものもあります。) 出力されるファイルは以下のいずれかです。

  • ライブラリ … 単体で実行できない。他のプログラムから参照して使う部品。拡張子dll(dymamic link library。dymamicとかlinkとかの言葉の意味はまた別の機会に)。
  • 実行可能ファイル … 単体で実行できるプ プログラム。拡張子exe。

.NET の場合、dllでもexeでも中身はほとんど同じ(exeの方にはプログラムの開始地点の情報が多い程度)です。これらは合わせて、アセンブリ(assembly: (組み立て)部品)といいます。

プロジェクトの分け方

プロジェクトを分ける(プログラムを部品化する)1番の動機は、いろんなプログラム、いろんな環境から使いたいというものです。

いろんなプログラムから1つの部品を使う

最初にゲームっぽい例を出したついでですし、ここでもゲームを作ることを考えましょう。

今どきはどんなゲームもオンラインでつながっています。ゲーム本体と同じコードをサーバー側で共有したいことも多いでしょう。また、ゲーム自体のプログラムの他にも、テスト用や、企画者向けのプログラムが必要になったりします。

  • ゲーム本体 … ゲーム専用ハードウェア上で動かしたりします。今どきだとiPhoneやAndroid向けも多いです。
  • ゲーム サーバー … 複数のプレイヤー間でゲーム本体からのデータを中継するだけの簡単なものから、ゲームの核となる処理がすべてサーバー上で動いている場合まであります。
  • テスト用プログラム … ゲーム中の特定の部位だけを抜き出して、そこを重点的にデバッグしたりすることがあります。
  • 企画者向けツール … ゲーム中のデータを作ったりバランス調整したりするため、開発者以外の人でも動作を確認しつつデータを編集できるツールが必要になります。

こういう場合、このいずれからも使う部分を別プロジェクトに切り出して、全部から参照して使います。

enter image description here

いろんな環境から1つの部品を使う

Windowsに限って言っても、組み込み製品、デスクトップ、サーバー、ゲーム機など、要件の異なるいろいろな機器の上で動きます。スマホやタブレットでいうと、iOSやAndroidとシェアが分かれていて、そのどちらにも対応したプログラムを書く必要がある場面は多いです。そして、こういう環境が変わると、使える機能(参照できるアセンブリ)が違ってきます。

こういう場合、環境によらず共通して使える部分と、それぞれの環境に応じた(それぞれの専用のフレームワークに依存した)部分に分かれます。

enter image description here

型フォワーディング

$
0
0

.NETでは、「アセンブリ+名前」の組み合わせで型を厳密に判定します。 その結果、異なるアセンブリでまったく同じ名前の型を定義しても、それぞれ別の型として扱われます。 これは、人的ミスの削減や、悪意あるコードへの耐性につながる一方で、 型の定義場所を移動させたいときに困ります。

そこで.NETは、型の検索の際に、別のアセンブリに転送する仕組みを提供しています。 これを型フォワーディング(Type Forwarding)と呼びます。

概要

.NETでは、「アセンブリ+名前」の組み合わせで型の所在を検索します。 その結果、異なるアセンブリでまったく同じ名前の型を定義しても、それぞれ別の型として扱われます。 これは、人的ミスの削減や、悪意あるコードへの耐性につながる一方で、 型の定義場所を移動させたいときに困ります。

そこで.NETは、型の検索の際に、別のアセンブリに転送する仕組みを提供しています。 これを型フォワーディング(type forwarding: 型の転送)と呼びます。

サンプル

TypeForwardedTo属性

型フォワーディングには、TypeForwardedTo属性(System.Runtime.CompilerServices)というものを使います。

例えば、ActualLibraryという名前のライブラリがあって、この中に以下のようなクラスが定義されているとします。

public class Class1
{
    public string Name => GetType().Assembly.GetName().Name + " / " + nameof(Class1);
}

このActualLibraryを参照して、以下のような.csファイルを含む、TypeForwardingLibraryという名前のライブラリを作ります。

using System.Runtime.CompilerServices;

[assembly: TypeForwardedTo(typeof(Class1))]

これで、アプリが「TypeForwardingLibraryにあるはずのClass1」を使おうとすると、 実際には「ActualLibraryで定義されたClass1」が返ってきます。

型フォワーディング

これで、例えば、「元々ライブラリAにあった型を、ライブラリBに移した」という場合でも、 AにTypeForwardedTo属性を書いておけば、互換性を崩さずに型を移動できます。

で、型の転送ができて何が嬉しいかというと、主に2通りの用途が考えられます。

  • モジュール分割
  • バックポーティング

モジュール分割

ありがちな技術的負債の1つに、単一のライブラリに債務を詰め込み過ぎるというのがあります。

取り急ぎ開発を進めていると、クラスの置き場に困ってついつい1つのライブラリになんでもかんでも置いてしまうということがあります。 そして、後から振り返ると、別々のライブラリに分けたくなったりします。

例えば、以下の様な状態です。

詰め込み過ぎたライブラリ

「よく使う処理を拡張メソッドにして、ライブラリ化しよう」と試みて、気がつけば、文字列関連、ネットワーク関連、数値処理関連など、全然違う債務を1箇所に詰め込み過ぎてしまっています。 こういう状態を「一枚岩」(monolithic)であると言います。

そして、このライブラリの利用者が増えてきた頃に、「文字列関連だけが使いたい」「ネットワーク関連だけが使いたい」など、個別の要求が出てきます。

反省して複数のライブラリに分割することになったとして、問題は既存のユーザーです。 「アセンブリ+名前」で型を探すわけで、別ライブラリに移動してしまうと互換性を崩します。

そこで、型フォワーディングの出番です。 詰め込み過ぎたライブラリを別のライブラリに分割した上で、元のライブラリにはTypeForwardedTo属性だけを書きます。

詰め込み過ぎたライブラリをモジュール分割

これで、互換性は崩さずに型を移動できます。

修正後のように、債務ごとに綺麗に分かれた状態を「モジュール型」(modular)と言います。 一枚岩な状態は、依存関係が大きくなりすぎたり、部分的な更新ができなかったりといった問題を抱えることになるので、 モジュール型な状態を保つよう心がけるべきです。

余談: 逆のやり方

ちなみに、TypeForwardedTo属性を付けるのを逆にすることもできます。

上記の例で言うと、

  • 将来的にライブラリを StringClassLibrary, HttpClassLibrary, NumericClassLibrary の3つに分けることに決まった
  • が、今は分けてる余裕ない。MonolithicClassLibraryは今のままで維持したい
  • なので、新しく作ったStringClassLibrary, HttpClassLibrary, NumericClassLibrary の側にTypeForwardedTo属性をつけて、MonolithicClassLibraryに型を転送する

というやり方もできます。

内部的には一枚岩のままなので、根本的には問題解決しません(依存関係は大きいままだし、部分更新できない)が、 「将来こう分割するよ」という予告にはなります。

余談: .NET 標準ライブラリのモジュール化

「初期段階で一枚岩に作ってしまって、後からモジュール分割」という流れ、 .NET Frameworkの標準ライブラリが典型例だったりします。

.NET Framework 4までは、標準ライブラリ中のクラスの大半がmscorlib.dllというアセンブリに詰め込まれていました。 それが、.NET Framework 4.5で、System.Net.dll, System.Threading.dll, System.Linq.dll… など、債務ごとに分割されました。

ちなみに、方法としては前節の「逆のやり方」でやっています。 mscorlib.dll自体は元のままで、モジュール分割した側のアセンブリにTypeForwardedTo属性が入っています。

一方で、2015年に、Windowsデスクトップ向けの.NET Frameworkとは別に、 クロスプラットフォーム向けの「.NET Core」というバージョンの別実装が出てきました。

こっちのバージョンでは、最初からモジュール分割済みの実装が行われています。

バックポーティング

C#の言語バージョンと.NET Frameworkバージョンで書いていますが、 C#の新しめの機能を、古いバージョンの.NET上で動かすためには、 いくつかライブラリのバックポーティング(新しいバージョンで追加された機能を、古いバージョンに向けて移植する作業)が必要なものがあります。

ここで問題になるのが、バージョンの混在です。

C# 5.0のasync/awaitを例にとって話しましょう。 まず、以下のように、.NET 4.5で完結している場合にはそもそもバックポーティングが必要なく、何の問題もありません。

.NET 4.5 での async/await

一方で、例えばAsyncBridgeという名前でバックポーティング用のライブラリを用意したとします。 これを使う場合でも.NET 3.5で完結するなら、以下のように特に問題は置きません。

.NET 3.5 での async/await バックポーティング

問題は、.NET 4.5向けライブラリと.NET 3.5向けライブラリの混在です。 .NET 4.5向けのものは標準ライブラリ(System.Threading.Tasks.dll)のTaskクラスを参照し、 .NET 3.5向けのものはバックポーティング(AsyncBridge.dll)のTaskクラスを参照している状態になります。 同名の別実装があると、どちらを参照すればいいのかわからなくなって色々と問題を起こします (回避方法もなくはないものの、基本的にはコンパイルできなくなります)。

この問題の解決にも型フォワーディングが使えます。 以下のように、標準ライブラリへの型フォワーディングを書いたAsyncBridge.dllを用意します。

標準ライブラリとバックポーティングの混在

つまり、以下のような実装が必要になります。

  • .NET 3.5向けに、標準ライブラリをバックポーティング実装を書いた AsyncBridge.dll を作る
    • .NET 3.5アプリからはこれを参照する
  • .NET 4.5向けに、標準ライブラリへの型フォワーディングを書いた AsyncBridge.dll を作る
    • .NET 4.5アプリからはこれを参照する

ちなみに、サンプルでは、async/awaitではなく、もっと実装が簡単なFormatableStringの実装例を書いています。

Viewing all 75 articles
Browse latest View live