連載
» 2017年12月19日 05時00分 公開

Dev Basics/Keyword:C# 7.2

C# 7.2はC# 7.1に続くC#の「ポイントリリース」であり、主に値型をより効率的に扱うための機能の追加に力点が置かれている。

[かわさきしんじ,Insider.NET編集部]
「Dev Basics/Keyword」のインデックス

連載「Dev Basics/Keyword」

 C# 7.2C# 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.2で追加されたのは主に以下のような機能だ。

  • 数値リテラルにアンダースコア(_)を前置可能
  • 位置指定引数よりも前に名前付き引数やオプション引数を指定可能
  • アクセス修飾子「private protected」が追加
  • 値型で参照型のセマンティクスを使用可能

 簡単なものから見ていこう。

数値リテラルにアンダースコアを前置可能

 C# 7.0では数値リテラルの表記について以下の2つが可能となった。

  • 「0b」接頭辞を使用した2進整数リテラルの表記
  • アンダースコア(_)による桁区切り

 C# 7.2では、2進/16進の整数リテラルで接頭辞に続いてアンダースコアを記述できるようになった。つまり、C# 7.2では以下のようなコードを記述できる。

int h = 0x_00ff_0001;
int b = 0b_1001_1111;

2進/16進整数リテラルに対するアンダースコアの前置

 言語バージョンをC# 7.1としたときには以下のようにエラーが発生するが、言語バージョンをC# 7.2にすればこのエラーは発生しなくなる。

C# 7.1では0x/0b接頭辞に続いてアンダースコアを記述できない C# 7.1では0x/0b接頭辞に続いてアンダースコアを記述できない

 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");

引数addrの位置がパラメーターリストと異なっているのでエラーとなる

 この機能は「この引数が何を意味しているか」をコード中に明確に記述して、コードの可読性を高めるためのものだと考えられる。

アクセス修飾子「private protected」が追加

 C# 7.2では新たなアクセス修飾子として「private protected」が追加された(「protected private」とも記述可能)。C#で利用可能なアクセス修飾子を以下にまとめておこう。

アクセス修飾子 意味
public どこからでもアクセス可能
protected そのメンバを含んだクラス、あるいはその派生クラスからのみアクセス可能
internal 現在のアセンブリからのみアクセス可能
protected internal 現在のアセンブリ、あるいはそのメンバを含んだクラスの派生クラスからのみアクセス可能
private そのメンバを含むクラスからのみアクセス可能
private protected そのメンバを含んだクラス、あるいは同一アセンブリ内でそのクラスから派生するクラスからのみアクセス可能
C# 7.2で使用可能なアクセス修飾子

 新設された「private protected」は、その修飾子でアクセスされたメンバを含んだクラス、もしくは同一アセンブリ内に存在し、そのクラスから派生するクラスからのみアクセスが可能であることを意味する。

値型で参照型のセマンティクスを使用可能

 これは「値型のオブジェクトでも参照渡しのセマンティクスを利用できる」ようにするためのものだと考えられる。値型のオブジェクトをメソッドに渡すような場合には、そのコピーが渡される(値渡し)。参照渡しのセマンティクスを値型にも適用できるようにすることで、値渡し時のコピーを行わないようにし、プログラムのパフォーマンスを上げようというものだ。

 そのために、C# 7.2では以下のような機能が追加されている。

  • パラメーターに対する「in」修飾子
  • 戻り値に対する「ref readonly」修飾子
  • 読み取り専用の構造体であることを意味する「readonly struct」宣言
  • 構造体がマネージドメモリ領域に直接アクセスし、それが必ずスタック領域に割り当てられなければならないことを意味する「ref struct」宣言

 以下では幾つかの要素を紹介する。

 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 };
  }
}

サンプルのPoint構造体

 この構造体のPlusメソッドには2つのパラメーターがあり、それらはともに「in」で修飾されている。これは、構造体Pointのインスタンスが「値渡しされ、その値がメソッド内では変更されない」ことを示している。この構造体はサイズも小さいので、それほどのメリットはないだろうが、サイズが大きな構造体では値渡しではなく参照渡しを行うことで構造体のコピーを抑えることのメリットは大きいはずだ。その一方で、値渡しではオブジェクトのコピーが渡されるので、パラメーターに渡された値を変更しても、元の値にはその影響は及ばない。値型のオブジェクトを参照渡しすることには、この原則を破る危険性がある。C#にはout修飾子とref修飾子があったが、これらにin修飾子を追加することで、参照渡しされるパラメーターをどう扱おうとしているのかプログラマーの意図を明確にできる。

  • out修飾子:そのパラメーターには実引数が参照渡しされ、渡された実引数の値が変更される
  • ref修飾子:そのパラメーターには実引数が参照渡しされ、渡された実引数の値は変更されるかもしれない
  • in修飾子:そのパラメーターには実引数が参照渡しされ、渡された実引数の値は変更されない

 in修飾子により効率と安全性の両者をコードに記述でき、不正な変更はコンパイル時にエラーとなるようになった。例えば、上のPlusメソッドでp1パラメーターを変更しようとすると、以下のようにエラーが発生する。

in修飾されたパラメーターは変更できない in修飾されたパラメーターは変更できない

 呼び出し時には実引数に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 })}");
  }
}

Plusメソッドの呼び出し例

 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フィールドとそれを取得するGetOriginメソッドを追加

 originフィールドは原点(0, 0)を表す静的メンバであり、GetOriginメソッドはそれを参照渡しで戻すメソッドだ。戻り値が参照渡しされ、かつ変更不可能であることを意味する「ref readonly」でメソッドが修飾されていることと、戻り値を返す際には「return ref」文が使われていることに注意されたい。このようにrefキーワードを付加して戻される値のことを「参照戻り値」と呼ぶ。

 メソッドを呼び出す側では、次のようにして戻り値を受け取る。

static void Main(string[] args)
{
  ref readonly var p1 = ref Point.GetOrigin();

  Console.ReadKey();
}

ref readonlyローカル変数に戻り値を受け取る

 ローカル変数にrefキーワードを用いて宣言すると、それは「refローカル変数」となる。refローカル変数を使用するときには変数宣言と代入の右辺の両方でrefキーワードを使用する必要がある点には注意しよう。上のコードではさらに「readonly」キーワードも使われている。これにより、そのrefローカル変数を読み取り専用としている。GetOriginメソッドがref readonly修飾されているので、これは必須である。上のコードから「readonly」キーワードを外すと次のようにエラーとなる。

ref readonly宣言されているメソッドの戻り値を単なるrefローカル変数で受け取ることはできない ref readonly宣言されているメソッドの戻り値を単なるrefローカル変数で受け取ることはできない

 また、受け取った側で戻り値を変更しようとするとエラーが発生する。以下に例を示す。

ref readonly var p1 = ref Point.GetOrigin();
p1 = new Point() { x = 1, y = 1 };  // エラー
p1.x = 100;  // エラー

ref readonlyローカル変数の値は変更できない

 ただし、GetOriginメソッドの戻り値は次のようにしても受け取ることができる。

ref readonly var p1 = ref Point.GetOrigin();
var p2 = Point.GetOrigin();
p2.x = 100;  // エラーとはならない

// 以下の出力は「0, 0」となり、originフィールドは変更されていないことが分かる
Console.WriteLine(Point.GetOrigin());

通常のローカル変数に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メソッドを追加

 コメントに記した通り、上のコードを実行すると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 struct宣言による不変な構造体の宣言

 不変な構造体を宣言するには「その構造体の宣言と、全てのフィールドの宣言」に「readonly」キーワードを付加する。これにより、全てのフィールドが読み取り専用となる。全てのフィールドが読み取り専用であることから、上で見たModifyメソッドで行っているようなフィールドの変更が不可能になる。よって、不意のデータ書き換えが行われることもないので、防御的なコピーも必要なくなる(そのため、readonly structではパフォーマンスの向上が見込める)不変なデータ構造を高速に取り扱う場合には便利に使えるだろう。

 なお、ref struct宣言とこれに関連するSpan<T>構造体については回をあらためて取り上げる予定だ。


 C# 7.2はC# 7.1に続くC#の「ポイントリリース」であり、本稿で見てきたように値型をより効率的に扱うための機能の追加に力点が置かれている。

「Dev Basics/Keyword」のインデックス

Dev Basics/Keyword

Copyright© 1999-2018 Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。