C# 7.2はC# 7.1に続くC#の「ポイントリリース」であり、主に値型をより効率的に扱うための機能の追加に力点が置かれている。
C# 7.2はC# 7.1に続くC#の「ポイントリリース」である。Visual Studio 2017 Update 5(以下、VS 2017)および.NET Core SDK 2.0以降でサポートされる。VS 2017などでC# 7.2を有効にするにはプロジェクトのプロパティで言語バージョンを「7.2」または「C# の最新のマイナーバージョン」に設定する必要がある。詳しい手順については上記リンクを参照のこと。
C# 7.2で追加されたのは主に以下のような機能だ。
簡単なものから見ていこう。
C# 7.0では数値リテラルの表記について以下の2つが可能となった。
C# 7.2では、2進/16進の整数リテラルで接頭辞に続いてアンダースコアを記述できるようになった。つまり、C# 7.2では以下のようなコードを記述できる。
int h = 0x_00ff_0001;
int b = 0b_1001_1111;
言語バージョンをC# 7.1としたときには以下のようにエラーが発生するが、言語バージョンをC# 7.2にすればこのエラーは発生しなくなる。
2進/16進で整数リテラルを記述する際にアンダースコアを接頭辞に続けて記述することで、コードがより明確に記述できるようになる。
名前付き引数を使うと、メソッドなどのパラメーターリストに指定されたパラメーター名を用いて実引数をメソッドに渡せる。以下に例を示す。
class Program
{
static void WritePhoneBook(string name, string phone, string addr)
{
Console.WriteLine($"Name: {name}, Phone: {phone}, Addr: {addr}");
}
static void Main(string[] args)
{
WritePhoneBook(addr: "tokyo", phone: "1111-1111", name: "insider.net");
WritePhoneBook("insider.net", addr: "tokyo", phone: "1111-1111");
Console.ReadKey();
}
}
ただし、位置指定引数と名前付き引数を混在させる場合、位置指定引数をまずは指定した後に、名前付き引数を指定する必要があった。つまり、上のWritePhoneBookメソッドは次のように呼び出すことはできなかった(以下のように、位置指定引数と同じ位置で名前付き引数を指定した場合でも)。
WritePhoneBook(name: "insider.net", "1111-1111", "tokyo");
この制約がC# 7.2では緩くなった。つまり、上の呼び出しが可能となっている。ただし、位置指定引数よりも前に名前付き引数を指定する場合には以下のような制約がある。
つまり、以下のような呼び出しはできない。
WritePhoneBook("insider.net", addr: "tokyo", "1111-1111");
この機能は「この引数が何を意味しているか」をコード中に明確に記述して、コードの可読性を高めるためのものだと考えられる。
C# 7.2では新たなアクセス修飾子として「private protected」が追加された(「protected private」とも記述可能)。C#で利用可能なアクセス修飾子を以下にまとめておこう。
アクセス修飾子 | 意味 |
---|---|
public | どこからでもアクセス可能 |
protected | そのメンバを含んだクラス、あるいはその派生クラスからのみアクセス可能 |
internal | 現在のアセンブリからのみアクセス可能 |
protected internal | 現在のアセンブリ、あるいはそのメンバを含んだクラスの派生クラスからのみアクセス可能 |
private | そのメンバを含むクラスからのみアクセス可能 |
private protected | そのメンバを含んだクラス、あるいは同一アセンブリ内でそのクラスから派生するクラスからのみアクセス可能 |
C# 7.2で使用可能なアクセス修飾子 |
新設された「private protected」は、その修飾子でアクセスされたメンバを含んだクラス、もしくは同一アセンブリ内に存在し、そのクラスから派生するクラスからのみアクセスが可能であることを意味する。
これは「値型のオブジェクトでも参照渡しのセマンティクスを利用できる」ようにするためのものだと考えられる。値型のオブジェクトをメソッドに渡すような場合には、そのコピーが渡される(値渡し)。参照渡しのセマンティクスを値型にも適用できるようにすることで、値渡し時のコピーを行わないようにし、プログラムのパフォーマンスを上げようというものだ。
そのために、C# 7.2では以下のような機能が追加されている。
以下では幾つかの要素を紹介する。
C# 7.2で追加された「in」修飾子は「メソッド呼び出し時に実引数がパラメーターに参照渡しされ、メソッド内ではそのパラメーターの値が変更されないこと」を意味する。例えば、次のような構造体があるとする。
public struct Point
{
public int x;
public int y;
public override string ToString()
{
return $"{this.x}, {this.y}";
}
public static Point Plus(in Point p1, in Point p2)
{
return new Point() { x = p1.x + p2.x, y = p1.y + p2.y };
}
}
この構造体のPlusメソッドには2つのパラメーターがあり、それらはともに「in」で修飾されている。これは、構造体Pointのインスタンスが「値渡しされ、その値がメソッド内では変更されない」ことを示している。この構造体はサイズも小さいので、それほどのメリットはないだろうが、サイズが大きな構造体では値渡しではなく参照渡しを行うことで構造体のコピーを抑えることのメリットは大きいはずだ。その一方で、値渡しではオブジェクトのコピーが渡されるので、パラメーターに渡された値を変更しても、元の値にはその影響は及ばない。値型のオブジェクトを参照渡しすることには、この原則を破る危険性がある。C#にはout修飾子とref修飾子があったが、これらにin修飾子を追加することで、参照渡しされるパラメーターをどう扱おうとしているのかプログラマーの意図を明確にできる。
in修飾子により効率と安全性の両者をコードに記述でき、不正な変更はコンパイル時にエラーとなるようになった。例えば、上のPlusメソッドでp1パラメーターを変更しようとすると、以下のようにエラーが発生する。
呼び出し時には実引数にin修飾子を付加する必要はない(一方、out/ref修飾子では必要)。以下に例を示す。
class Program
{
static void Main(string[] args)
{
var p1 = new Point() { x = 1, y = 1 };
Console.WriteLine($"{Point.Plus(p1, new Point() { x = 2, y = 2 })}");
}
}
ref readonly修飾子はメソッドの戻り値に適用する。これにより、戻り値が参照渡しされることと、戻された値は変更できないことを示す。例として、上のPointクラスにメンバを追加してみよう。
public struct Point
{
public int x;
public int y;
private static Point origin = new Point();
public override string ToString()
{
return $"{this.x}, {this.y}";
}
public static Point Plus(in Point p1, in Point p2)
{
return new Point() { x = p1.x + p2.x, y = p1.y + p2.y };
}
public static ref readonly Point GetOrigin()
{
return ref origin;
}
}
originフィールドは原点(0, 0)を表す静的メンバであり、GetOriginメソッドはそれを参照渡しで戻すメソッドだ。戻り値が参照渡しされ、かつ変更不可能であることを意味する「ref readonly」でメソッドが修飾されていることと、戻り値を返す際には「return ref」文が使われていることに注意されたい。このようにrefキーワードを付加して戻される値のことを「参照戻り値」と呼ぶ。
メソッドを呼び出す側では、次のようにして戻り値を受け取る。
static void Main(string[] args)
{
ref readonly var p1 = ref Point.GetOrigin();
Console.ReadKey();
}
ローカル変数にrefキーワードを用いて宣言すると、それは「refローカル変数」となる。refローカル変数を使用するときには変数宣言と代入の右辺の両方でrefキーワードを使用する必要がある点には注意しよう。上のコードではさらに「readonly」キーワードも使われている。これにより、そのrefローカル変数を読み取り専用としている。GetOriginメソッドがref readonly修飾されているので、これは必須である。上のコードから「readonly」キーワードを外すと次のようにエラーとなる。
また、受け取った側で戻り値を変更しようとするとエラーが発生する。以下に例を示す。
ref readonly var p1 = ref Point.GetOrigin();
p1 = new Point() { x = 1, y = 1 }; // エラー
p1.x = 100; // エラー
ただし、GetOriginメソッドの戻り値は次のようにしても受け取ることができる。
ref readonly var p1 = ref Point.GetOrigin();
var p2 = Point.GetOrigin();
p2.x = 100; // エラーとはならない
// 以下の出力は「0, 0」となり、originフィールドは変更されていないことが分かる
Console.WriteLine(Point.GetOrigin());
この場合は、「参照戻り値のコピー」が変数p2に代入される(つまり、通常の値型の代入と同様)。これによって、ref readonly修飾されている値(この場合は原点座標)を保護しながら、戻り値を受け取った側ではその値を自由に変更できるようになる。プログラマーが実際に何を行いたいかが、ref/ref readonly/varキーワードを使うことで、コード上で明確になるというのが、この代入のメリットといえる(もちろん、varキーワードを使うと値のコピーが行われるので、参照渡しのメリットは失われるが、それでもなお受け取った値を変更したいという場合はあるだろう)。
今見たような「防御的なコピー」は、さまざまな箇所で行われる。例えば、以下のModifyメソッドをPointクラスに追加したとする。
public struct Point
{
//…… 省略 ……
public void Modify()
{
this.x = 100;
Console.WriteLine(this);
}
}
class Program
{
static void Main(string[] args)
{
ref readonly var p = ref Point.GetOrigin();
p.Modify(); // 出力:「100, 0」
Console.WriteLine(p);
Console.ReadKey(); // 出力:「0, 0」
}
}
コメントに記した通り、上のコードを実行するとModifyメソッドでは「100, 0」と出力され、Mainメソッド内のWriteLineメソッド呼び出しでは「0, 0」と出力される。これは、読み取り専用であるという性質(不変性)を保つために自動的に防御的なコピーが行われるようになっているからだ。
最後に「readonly struct」宣言についても簡単に触れておく。readonly struct型は「不変な構造体」(immutable struct)を作成するためのものだ。以下に簡単な例を示す。
public readonly struct ReadOnlyPoint
{
public readonly int x;
public readonly int y;
public ReadOnlyPoint(int x, int y) => (this.x, this.y) = (x, y);
}
不変な構造体を宣言するには「その構造体の宣言と、全てのフィールドの宣言」に「readonly」キーワードを付加する。これにより、全てのフィールドが読み取り専用となる。全てのフィールドが読み取り専用であることから、上で見たModifyメソッドで行っているようなフィールドの変更が不可能になる。よって、不意のデータ書き換えが行われることもないので、防御的なコピーも必要なくなる(そのため、readonly structではパフォーマンスの向上が見込める)不変なデータ構造を高速に取り扱う場合には便利に使えるだろう。
なお、ref struct宣言とこれに関連するSpan<T>構造体については回をあらためて取り上げる予定だ。
C# 7.2はC# 7.1に続くC#の「ポイントリリース」であり、本稿で見てきたように値型をより効率的に扱うための機能の追加に力点が置かれている。
Copyright© Digital Advantage Corp. All Rights Reserved.