目次

C# 7.2

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

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

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

※C# 7.2 は、リリース時点ではバグが多く、その後の更新で修正されたものが結構な数あります。 バグが多いのは主に参照がらみの機能の辺りです。 (具体的なバグについては昔書いたブログがあるのでそちらを参照。) 本サイト内で説明している機能がうまく動かなかったときには、一度コンパイラーやVisual Studioのバージョンを挙げてみてください。

先頭区切り文字

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

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

参照の活用

ここから先が、C# 7.2 の大部分を占める「参照の活用」になります。 小さな機能の組み合わせになっているのでそれぞれについて説明します。

条件演算子での ref 利用

条件演算子の2項目、3項目を参照にできるようになりました。 以下のような書き方ができます。

x > y ? ref x : ref y

詳しくは「条件演算子での ref 利用」で説明します。

ref readonly

「参照渡しだけども読み取り専用」というような渡し方ができるようになりました。 読み取り専用参照(ref readonly)と呼ばれています。

引数の場合にはin修飾子を使って以下のように書きます。

public struct Quaternion
{
    public double W;
    public double X;
    public double Y;
    public double Z;
    public Quaternion(double w, double x, double y, double z) => (W, X, Y, Z) = (w, x, y, z);

    public static Quaternion operator *(in Quaternion a, in Quaternion b)
        => new Quaternion(
            a.W * b.W - a.X * b.X - a.Y * b.Y - a.Z * b.Z,
            a.W * b.X + a.X * b.W + a.Y * b.Z - a.Z * b.Y,
            a.W * b.Y + a.Y * b.W + a.Z * b.X - a.X * b.Z,
            a.W * b.Z + a.Z * b.W + a.X * b.Y - a.Y * b.X);
}

ref引数やout引数とは異なり、in引数は以下のような呼び出し方ができます。

  • F(x) というように、修飾なしで呼ぶ
  • F(10) というように、リテラルを引数として渡す
  • F(x + y) というように、右辺値(式の計算結果)を引数として渡す

また、ローカル変数と戻り値の場合はref readonly修飾子を使います。

static ref readonly int Max(in int x, in int y)
{
    ref readonly var t = ref x;
    ref readonly var u = ref y;

    if (t < u) return ref u;
    else return ref t;
}

詳しくは「入力参照引数 (in 引数)」、「ref readonly」で説明します。

演算子のin引数

これまで、演算子オーバーロードの引数は値渡しである必要がありました。 C# 7.2では、in引数も演算子の引数にできるようになりました。

struct Complex
{
    public double X;
    public double Y;
    public Complex(double x, double y) => (X, Y) = (x, y);

    // これは OK
    public static Complex operator +(Complex a, Complex b)
        => new Complex(a.X + b.X, a.Y + b.Y);

    // これはコンパイル エラーになる
    public static Complex operator +(ref Complex a, ref Complex b)
        => new Complex(a.X + b.X, a.Y + b.Y);

    // これなら OK
    public static Complex operator +(in Complex a, in Complex b)
        => new Complex(a.X + b.X, a.Y + b.Y);
}

参照渡しの拡張メソッド

拡張メソッドの第1引数(thisが付いている引数)を参照渡し(refもしくはin)で渡せるようになりました。

public static class QuaternionExtensions
{
    // 構造体の書き換えを拡張メソッドでやりたい場合に ref 引数が使える
    public static void Conjugate(ref this Quaternion q)
    {
        var norm = q.W * q.W + q.X * q.X + q.Y * q.Y + q.Z * q.Z;
        q.W = q.W / norm;
        q.X = -q.X / norm;
        q.Y = -q.Y / norm;
        q.Z = -q.Z / norm;
    }

    // コピーを避けたい場合に in 引数が使える
    public static Quaternion Rotate(in this Quaternion p, in Quaternion q)
    {
        var qc = q;
        qc.Conjugate();
        return q * p * qc;
    }
}

詳しくは「参照渡しの拡張メソッド」で説明します。

readonly struct

構造体に readonly 修飾子を付けることで、以下のような制約を掛けれるようになりました。

  • すべてのフィールドにreadonlyを付けることが必須
  • this参照もreadonly扱いされて、構造体の書き換えが完全にできなくなる
// 構造体自体に 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);
}

詳細は「readonly struct」で説明します。

「参照」とは直接は関係ないですが、in 引数や、ref safety rule (今後追加予定)と関連して必要になった機能です。

安全な stackalloc

Span<T>構造体と併用することで、unsafe なしで stackalloc を使えるようになりました。

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を使っていますがポインターは不要で、ちゃんと範囲チェックも掛かって安全に扱えます。

詳しくは「Span<T>構造体」で説明します。

ref 構造体

C# 7.2 と深く関連する型にSpan<T>という構造体があります。 この Span<T> は、C#7.2 の主たる目的の「構造体と参照の活用によってパフォーマンス改善」の主役となる構造体です。

この型を安全に使うためにはいくつが制限が必要で、そのためにref構造体という構文と、それに対するフロー解析が実装されました。

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

まず、`Span`を持てるのは`ref`修飾子がついた構造体(`ref`構造体)だけです。

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

ref構造体には参照ローカル変数・参照戻りと同じ制限がかかります。

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

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

その他、ref構造体には「スタック上になければならない(stack-only)」という制限があり、 その結果、例えば以下のような制限がかかります(一部抜粋)。

using System;
using System.Threading.Tasks;

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

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

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

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

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

詳しくは「ref構造体」で説明します。

マイナーな更新

C# のコンパイラーのバージョン 2.7 や、Visual Studio 15.6 というバージョン(2018/3リリース)で、 C# にちょっとした修正が入っています。 かなりマイナーな更新なので、「C# 7.3」とはせず「C# 7.2 fix」(バグ修正扱い、あるいは、バグ修正とまとめてリリースして差し支えない程度の更新)としています。

修正されたのは以下の2点です。

  • 参照渡しの拡張メソッド
    • 2.6 時点: ref thisin this の語順でないとダメ
    • 2.7 から: this refthis in の語順でも OK
  • in引数のメソッド呼び出し/値渡しのメソッドとの呼び分け
    • void M(T x)void M(in T x)の両方のメソッドがあるとき
    • 2.6 時点: M(x) という呼び出しはエラーになる
    • 2.7 から: M(x) だとvoid M(T x)の方が、M(in x) だと void M(in T x)の方が呼ばれる

あくまで「C# 7.2に対する修正」としてリリースされているので、 新しい(2.7以降の)コンパイラーで、昔の(2.6以前の)挙動にすることはできません。 「できてしかるべきことができていなかったのを、できるようにしただけなので問題は起きないはず」という判断です。

更新履歴

ブログ