C# 7 早わかりリファレンス(後編)

C# 7 早わかりリファレンス(後編)

C# タプル/ローカル関数/ラムダ式/非同期処理/例外処理 最速再入門【7.0対応】

2017年4月28日 改訂(初版[6.0対応]: 2016/04/14)

C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。後編では、タプルと分解(デコンストラクション)/ローカル関数/構造体/継承とインターフェース/列挙型/イテレーター/例外処理/リソースの解放/ラムダ式/非同期処理(async/await)を説明する。

  • このエントリーをはてなブックマークに追加

 前編ではC#言語の文法の中でも最もベーシックな「型」「変数」「演算子」「ステートメント(文)」といった基本機能を、中編ではオブジェクト指向言語であるC#の要となる「クラス」関連の機能をできるだけコンパクトに説明した。今回後編では、それ以外の残りの言語仕様や機能として、「タプルと分解(デコンストラクション)」「ローカル関数」「構造体」「継承とインターフェース」「列挙型」「イテレーター」「例外処理」「リソースの解放」「ラムダ式」「非同期処理(async/await)」について説明する。前編から後編までの内容で、C#の言語機能を一通り確認したことになる。

<目次>【前編】【中編】【後編】のラベルをクリック/タップすると開閉します。)

▲目次に戻る

タプルと分解(デコンストラクション)

▲目次に戻る

タプル

 C# 6.0以前では、メソッドの返り値に複数のオブジェクトの集合を返したいが独立したクラスとして適切な名前が思いつかない場合、独立したメソッドとせずに匿名オブジェクトとして扱うか、Tuple<T1, T2, ……>を利用するかのどちらかの方法を取っていた。しかし、匿名オブジェクトはメソッドをまたげないこと、Tupleはプロパティ名が無意味なItem1Item2、……で分かりづらくなることが問題だった。この問題に対し、C# 7.0でタプル型が導入された。実体はValueTupleクラス(System名前空間)である。また「タプル」という表現がC# 6.0以前のTupleと紛らわしいため、NuGetからダウンロードしてVisual Studioなどから参照できるcorefx(.NET Coreライブラリ)などのドキュメントコメントでは、C# 6.0以前のTupleを「」と表現している。

 なお執筆時点で筆者が調べた環境では、C# 7.0で導入されたタプルを利用するためには、.NET Framework 4.7(=Windows 10 Creators Updateに含まれている)以外の環境(.NET Framework 4.6以前および.NET Core 1.0と1.1)では、「System.ValueTuple」パッケージをNuGetから追加しておく必要がある。

C#
using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    // 文字列を全て小文字にしたものと全て大文字にしたものを取得する
    var texts = new[] { "aaA", "bBb", "cCC" };

    // 匿名オブジェクトを利用
    foreach (var text in texts.Select(x => new { lower = x.ToLower(), upper = x.ToUpper() }))
    {
      Console.WriteLine(text);
    }

    // Tuple(組)を利用
    foreach (var text in texts.Select(x => new Tuple<string, string>(x.ToLower(), x.ToUpper())))
    {
      Console.WriteLine(text);
    }

    // C# 7.0のタプルを利用
    foreach (var text in texts.Select(Convert))
    {
      Console.WriteLine($"lower={text.lower}, upper={text.upper}");
    }

    var str1 = "aaAA";
    // タプルリテラルで宣言
    var t1 = (str1.ToLower(), str1.ToUpper()); // 要素名は任意なのでつけなくてもよい
    Console.WriteLine($"Item1={t1.Item1}");    // つけない場合はC#6.0以前のタプルと同様のItemNという名前で参照

    var t2 = (lower: str1.ToLower(), upper: str1.ToUpper());

    // new形式はコンパイルエラー
    //var t3 = new (int, int)(0, 1);

    var t4 = (lower: str1.ToLower(), str1.ToUpper()); // 一部のみ要素名を省略することも可
    Console.WriteLine($"{t4.lower}, {t4.Item2}");

    // 同じ要素名で重複するとコンパイルエラー
    //var t5 = (lower: text.ToLower(), lower: text.ToLower());
  }

  static (string lower, string upper) Convert(string text)
  {
    return (lower: text.ToLower(), upper: text.ToUpper());
  }
}
リスト12B-1 C# 6.0以前のタプル(C# 7.0では組とも呼ばれる)、匿名オブジェクトとC#7.0で導入されたタプル(System.ValueTuple)

 C# 7.0で導入されたタプルは、匿名オブジェクトと同じように要素名に任意の名前をつけられるうえに、型であるため、返り値の型に指定することができる。またタプルリテラルという記述で宣言することもできる。

 型として利用する場合、要素名の命名は任意であり、各要素の型が一致していれば要素の名前が異なっていても代入可能である。また、要素名をつけずに利用することもでき、その場合はC# 6.0以前のタプル(組)と同じItemN(Nは要素のインデックス)という名前で参照できる。

▲目次に戻る

分解デコンストラクション

 C# 7.0ではタプルが導入されたことに加えて、タプルを要素ごとに分けて受け取る分解Deconstruction)も導入された。これによりタプルの固まりをそのまま受け取るのではなく、あらかじめ変数に分けた状態で受け取ることができる。

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var tuple = (name: "Tom", age: 34);
    string name; int age;
    (name, age) = tuple;       // 宣言済みの変数(=式)に分解して代入する
    Console.WriteLine(age);

    int x, y;
    (x, y) = new Ex12B_2_Destruction_Point(3, 5);  // Deconstructメソッド(後述)を持つインスタンスを分解する
    Console.WriteLine($"{x} {y}");

    // 分解して新しく宣言した変数に代入する
    (var newName, var newAge) = tuple;
    (var myX, var myY) = new Ex12B_2_Destruction_Point(3, 5);

    // 式と変数宣言の併用はコンパイルエラー
    //(x, var y2) = new Point(3, 4);

    // Out Varと同じく_で読み捨てることができる
    (var x1, var _) = new Ex12B_2_Destruction_Point(4, -3);
    Console.WriteLine($"{x1}");
    // var _ は変数宣言扱いなので変数への代入と併用するとコンパイルエラー
    //(x1, var _) = new Point(4, -3);
  }
}

class Ex12B_2_Destruction_Point
{
  public int X { get; }
  public int Y { get; private set; }

  public Ex12B_2_Destruction_Point(int x, int y) { X = x; Y = y; }
  public void Deconstruct(out int x, out int y)
  {
    x = X;
    y = Y;
  }
}
リスト12B-2 分解(デコンストラクション)

 分解を受け取る側は、あらかじめ宣言済みの変数(=式)に代入する場合と、その場で宣言した変数に代入する場合の2通りがある。両者を混在させることはできない。その場で宣言する場合は_を使って読み捨てることもできる。分解される側は、ValueTuple型のオブジェクトに加えて、デコンストラクトメソッドを持つオブジェクトに分解できる。デコンストラクトメソッドは、public voidDeconstructという名前であり、分解される変数を引数に記述する。

▲目次に戻る

ローカル関数

▲目次に戻る

ローカル関数の定義

 C# 7.0からローカル関数が追加された。ローカル関数は、関数の中でしか利用できない関数を関数内部に定義できる機能である。C# 6.0まではラムダ式に代表される匿名関数の機能があったが、再帰が書きづらいことやイテレーターが記述できないなどの制約があった。ローカル関数は匿名関数に比べると、宣言ステートメントとしてしか記述できないためメソッド呼び出しの引数などでは利用できないが、通常のメソッド定義と同様に再帰やイテレーターが記述できるようになる。

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    // 匿名関数で再帰する場合は最初に宣言しないといけない
    Func<int, int> f2 = null;
    f2 = n => (n >= 1) ? (n * f2(n - 1)) : 1;
    var res = f2(3);
    Console.WriteLine(res);

    // ローカル関数は通常の関数同様再帰を記述できる
    int f(int x) => x >= 1 ? x * f(x - 1) : 1;
    // ローカル関数を暗黙的に型宣言するとコンパイルエラー
    //var f3(int x) => x >= 1 ? x * f(x - 1) : 1;
    res = f(3);
    Console.WriteLine(res);

    // ローカル関数内でお互いの関数を呼び出すコードは記述できる
    // が、無限に再帰する場合は実行時エラーとなる
    void Add(int x, int y)
    {
      Console.WriteLine($"{x} + {y} = {x + y}");
      Multiply(x, y);
    }
    void Multiply(int x, int y)
    {
      Console.WriteLine($"{x} * {y} = {x * y}");
      Add(30, 10);
    }
    Add(2, 3);
  }
}
リスト12C-1 匿名関数とC# 7.0で導入されたローカル関数

 匿名関数の場合は「最初に宣言して代入する」という手間が必要だったが、ローカル関数での再帰は通常の関数のように記述できる。ローカル関数もラムダ式と同様に、暗黙的に型宣言するとコンパイルエラーとなる。また、ローカル関数同士で再帰的な呼び出しも可能であるが、無限に再帰する場合、実行時エラーとなるのは通常の関数と同じである。ローカル関数の利用例として、イテレーターメソッドでのnullチェック非同期メソッドの結果のキャッシュを別項目に記載しているので参考にしてほしい。

▲目次に戻る

構造体

▲目次に戻る

構造体の宣言

C#
class Program
{
  static void Main(string[] args)
  {
    var p1 = new Point(10);
    // 参照ではなく、コピーした値がp2に割り当てられる
    var p2 = p1;
    // p1のプロパティを更新してもp2には影響がない
    p1.X = 1;
    // p1.X=1
    // p2.X=10

    // 構造体の初期値はnullにはなり得ない
    var points = new Point[10];
    // point[0] != null
    // point[0].X == 0  // 各フィールドはデフォルト値で初期化される

    // [参考]一方、クラスの初期値はnullである
    var uris = new System.Uri[10];
    // uris[0] == null
  }

  struct Point
  {
    private int x;
    public int X
    {
      get { return x; }
      set { x = value; }
    }
    public Point(int x)
    {
      this.x = x;
    }
  }
}
リスト13-1 構造体

 構造体は、クラスによく似た構造だが、値型でありヒープ割り当てを必要としない。そのため、値を持つ小規模なデータ構造に適している。

 構造体は値型であるため、構造体を参照する変数を別の変数に割り当てると、参照ではなくコピーした値が代入される。サンプルコードでは、変数p1p2に代入しているが、その後p1のプロパティを変更してもp2は変更されない。

 また、値型であるため、構造体自身の初期値はnullではなく、構造体の各フィールドはそれぞれのデフォルト値で初期化した値となる。そのため構造体を要素とする配列を初期化した時点で、配列の各要素には構造体の初期値が代入されている。

▲目次に戻る

継承とインターフェース

▲目次に戻る

継承仮想overridenew

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass1();
    my.Test2(); // MyClass1.Test2

    var my2 = new MyClass2();
    my2.Test2(); // 1
  }
}

public class MyBase
{
  public int Value { get; set; } = 0;

  public void Test1()
  {
    Console.WriteLine("MyBase.Test1");
  }

  public virtual void Test2()
  {
    Console.WriteLine("MyBase.Test2");
  }
}

public class MyClass1 : MyBase
{
  public new int Value { get; set; } = 1;

  public override void Test2()
  {
    Console.WriteLine("MyClass1.Test2");
  }
}

public class MyClass2 : MyClass1
{
  public override void Test2()
  {
    Console.WriteLine(Value);
  }
}
リスト14-1 継承

 オブジェクト指向の特徴の一つであるクラスの継承は、クラス名の後に:基底クラスを宣言することで記述できる。C#ではvirtual省略したメソッドは、非仮想メソッドであり、継承したクラスでオーバーライドすることはできない。そのため、オーバーライドを許可するメソッドは基底クラスの側でvirtual修飾子を付けてオーバーライドを許可する必要がある。オーバーライドする側のメソッドはoverride修飾子を付けるが、さらに継承先のクラスでオーバーライドすることもできる。

 また、継承したクラスで基底クラスのメソッドやプロパティと同じ名前のメンバーを宣言することも可能だ。このとき、基底クラスのメンバーは隠ぺいされてしまうため、継承したクラスではメソッドやプロパティなどのメンバーにnew修飾子を付けて隠ぺいしていることを明示できる。

▲目次に戻る

抽象クラスシールクラスシールメソッド

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var my = new MyClass();
    my.Test1(); // MyAbstractClass.Test1
    my.Test2(); // MyClass.Test2
    my.Test3(); // MyClass.Test3

    var my2 = new MyClass2();
    my2.Test1(); // MyAbstractClass.Test1
    my2.Test2(); // MyClass2.Test2
    my2.Test3(); // MyClass.Test3
  }
}

public abstract class MyAbstractClass
{
  public void Test1()
  {
    Console.WriteLine("MyAbstractClass.Test1");
  }

  public abstract void Test2();

  public abstract void Test3();
}

public class MyClass : MyAbstractClass
{
  public override void Test2()
  {
    Console.WriteLine("MyClass.Test2");
  }

  public sealed override void Test3()
  {
    Console.WriteLine("MyClass.Test3");
  }
}

public sealed class MyClass2 : MyClass
{
  public override void Test2()
  {
    Console.WriteLine("MyClass2.Test2");
  }
}
リスト14-2 抽象クラスとシールクラス、シールメソッド

 抽象クラスは直接インスタンス化できないクラスで、通常のメソッド定義に加え抽象メソッド抽象プロパティをメンバーに持つことができる。これは継承先のクラスが持つ共通の振る舞いをあらかじめ定義しておくときに役立つ。抽象メソッドを継承したクラスでオーバーライドするときはoverride修飾子を付ける。

 抽象クラスからの継承に限らず、定義したクラスを継承することを禁止する場合、sealed修飾子を付けてシールクラスにすることができる。また、メソッド単位でオーバーライドを禁止する場合も、sealed修飾子を付けてシールメソッドにすることができる。

▲目次に戻る

インターフェース明示的実装

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var s1 = new Surface();
    s1.Name = "a";
    s1.Paint(); // Paint
    ((ISurface)s1).Paint(); // Paint
    ((IPaintable)s1).Paint(); // Paint

    var s2 = new Surface2();
    s2.Paint(); // Paint
    ((ISurface)s2).Paint(); // Paint
    ((IPaintable)s2).Paint(); // IPaintable.Paint
  }
}
interface IPaintable
{
  void Paint();
}

interface ISurface
{
  string Name { get; set; }
  void Paint();
}

public class Surface : ISurface, IPaintable
{
  public string Name { get; set; }
  public void Paint()
  {
    Console.WriteLine("Paint");
  }
}

public class Surface2 : ISurface, IPaintable
{
  public string Name { get; set; }
  public void Paint()
  {
    Console.WriteLine("Paint");
  }

  void IPaintable.Paint()
  {
    Console.WriteLine("IPaintable.Paint");
  }
}
リスト14-3 インターフェースと明示的実装

 インターフェースはメソッドやプロパティのコントラクト(=実装すべき規約)を定義でき、インターフェースを実装するクラスや構造体は、必ずそのコントラクトに従って実装する必要がある。C#では継承するクラスは1つのみだが、インターフェースは複数実装できる。

 インターフェースを複数実装する場合、異なるインターフェースが同じコントラクトを持っていることがある(サンプルコードではIPaintableISurfaceインターフェースの両方にvoid Paint()メソッドがある)。この場合、特に指定せずに実装すると両者のインターフェースは同じ実装メソッドを呼び出す。しかし、それぞれのインターフェースで異なる実装を行いたい場合は、明示的に実装可能だ。サンプルコードのSurface2クラスのIPaintable.Paintメソッドがその例で、IPaintableインターフェースにキャストして呼び出すと、明示的実装が呼び出されることになる。

▲目次に戻る

列挙型

▲目次に戻る

列挙型の宣言

C#
class Program
{
  static void Main(string[] args)
  {
    var c1 = Color.Red;
    var c2 = Color.Blue;
    // 定義されていない値をキャスト可能だが、非推奨
    var t3 = (LongType)23;
  }
}
public enum Color
{
  Red,
  Blue,
  Orange
}

public enum LongType : long
{
  Solid = int.MaxValue + 1L,
  Soft = 0,    // 必ず0を定義することが推奨される
  Hard = Solid
}
リスト15-1 列挙型

 列挙型(=列挙体)は、定数のリストを名前付きで管理するための構造である。例えばリスト15-1のColor列挙型の各値には名前を付けているが、その実体はint型もしくはlong型の数値である。数値を明示的に指定しない場合は、宣言された順に0から1つずつ増えた値が割り当てられる。明示的に指定する場合、0が割り当てられる列挙型の値を必ず定義することが推奨されている。

 列挙型は数値であるため、数値を列挙型にキャストでき、実際には定義されていない値であってもキャスト可能である。しかし、これは予期しない実行時エラーを起こしかねないので推奨されていない。

▲目次に戻る

Flags属性

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var colors = Color.Red | Color.Blue;
    colors.HasFlag(Color.Red);      // True
    var f1 = (colors == Color.Red); // False
    colors.HasFlag(Color.Yellow);   // False
  }
}

[Flags]
public enum Color
{
  Red,
  Blue,
  Yellow
}
リスト15-2 Flags属性

 列挙型の値を利用する際に、複数の値を持っている状態を表現したい場合がある。その場合は、列挙型の宣言にFlags属性を付けることでビットフラグを表現できる。複数の値を持つ値は|演算子で記述でき、複数とり得る値の中で指定した列挙型の値が含まれているかどうかは、HasFlagメソッドで検査できる。

▲目次に戻る

イテレーター

▲目次に戻る

イテレーターの宣言yield

C#
using System;
using System.Collections;
using System.Collections.Generic;

class Program
{
  static void Main(string[] args)
  {
    // a b c z
    var samples = MyCollection.GetSamples();
    // 1, 9, 25, 49, 81
    foreach (var element in new MyCollection())
    {
      Console.WriteLine(element);
    }
  }
}
public class MyCollection : IEnumerable<int>
{
  public static IEnumerable<string> GetSamples()
  {
    yield return "a";
    yield return "b";
    yield return "c";
    yield return "z";
  }

  public IEnumerator<int> GetEnumerator()
  {
    for (int i = 0; i < 10; i++)
    {
      if (i % 2 == 0)
        continue;

      yield return i * i;
    }
  }

  // IEnumerableインターフェースのGetEnumerator()メソッドを実装
  IEnumerator IEnumerable.GetEnumerator()
  {
    return GetEnumerator();
  }
}
リスト16-1 イテレーター

 返り値がIEnumerable型およびそのジェネリスク型のIEnumerable<T>である場合、yield returnキーワードで反復表現を記述できる。yield returnするたびにIEnumerable型オブジェクトの要素1つを返すことができ、メソッドが完了した時点で要素の列挙が終わったことになる。

 また、クラスがIEnumerableインターフェースもしくはそのジェネリクス型のインターフェースを実装する場合の、GetEnumeratorメソッドの実装にもyield returnを実装できる。

▲目次に戻る

ローカル関数を使ったイテレーターでのnullチェック

C#
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    IEnumerable<int> input = null;
    var old = input.ApplyOld(x => x.ToString()); // ここではまだnullチェックされない
    try
    {
      // 列挙が実行されて初めてApplyOld内の処理が実行され、nullチェックされる
      var array = old.ToArray();
    }
    catch (ArgumentNullException)
    {
      Console.WriteLine("NullCheck");
    }

    try
    {
      // イテレーターを返すローカル関数の場合は、メソッドを呼び出した時点でnullチェックが実行される
      var newApply = input.Apply(x => x.ToString());
    }
    catch (ArgumentNullException)
    {
      Console.WriteLine("NullCheck");
    }
  }
}

// LINQのSelectメソッドで同一のことができるが、サンプルとして記述
static class Extensions
{
  public static IEnumerable<string> ApplyOld(this IEnumerable<int> source, Func<int, string> converter)
  {
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (converter == null) throw new ArgumentNullException(nameof(converter));

    foreach (var x in source)
      yield return converter(x);
  }

  public static IEnumerable<string> Apply(this IEnumerable<int> source, Func<int, string> converter)
  {
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (converter == null) throw new ArgumentNullException(nameof(converter));

    IEnumerable<string> Inner()
    {
      foreach (var x in source)
        yield return converter(x);
    }
    return Inner();
  }
}
リスト16-2 ローカル関数を使ったイテレーターのnullチェック

 イテレーターの引数でnullチェックを行いたい場合、イテレーターを返すメソッドの最初でnullチェックを行うと、例外が直感的でない場所でスローされることがある。これは、イテレーターを返すメソッドの処理が、メソッドを呼び出した場所では必ずしも実行されず、ToArrayなど実際にイテレーターの列挙を実行する場所で行われるためである。C# 7.0で導入されたローカル関数を利用し、イテレーターを返す部分をローカル関数で実装すると、nullチェックがメソッド呼び出しの時点で実行されるようになる。

▲目次に戻る

例外処理

▲目次に戻る

throwtry-catch

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    try
    {
      ThrowException();
    }
    catch (MyException ex) when (ex.Value >= 1)
    {
      Console.WriteLine("Catch MyException");
    }
    catch (Exception ex)
    {
      Console.WriteLine("Catch Exception: " + ex.Message);
      throw;   // 再スロー
    }
  }
  static void ThrowException()
  {
    throw new MyException() { Value = 1 };
  }
}
public class MyException : Exception
{
  public int Value { get; set; }
}

// C# 7.0より例外を式としてスローできるようになった箇所
public class MyClass
{
  public string Name { get; }

  // 1ラムダ式や式形式メンバーの=>の後
  public MyClass() => throw new Exception();

  // 2null合算演算子の第2オペランド
  public MyClass(string name) => Name = name ?? throw new Exception();

  // 3三項演算子の第2・第3オペランド
  public MyClass(int i) => Name = i > 0 ? GetNameById(i) : throw new Exception();
  private string GetNameById(int i) => $"name-{i}";
}
リスト17-1 例外処理

 例外の発生をthrow句で記述できる。C#ではメソッド宣言にスローされる例外を記述することはできず、任意の例外がスローされる可能性がある。スローする例外は、定義済みの例外もしくはExceptionクラスを継承したユーザー定義の例外をスローできる。

 例外処理はtry-catch構文で記述し、tryブロックの中で発生した例外をcatchブロックで処理できる。catchブロックは、まず例外の型により複数宣言でき、スローされた例外が代入可能な最初のcatchブロックが実行される。代入可能なcatchブロックがない場合はメソッド呼び出し元に例外がスローされる。C# 6.0からcatchブロックにwhenキーワードでさらにcatchするかどうかの条件を記述できるようになった。

 catchブロック内で例外を再スローしたい場合は、throw句を記述する。throw句の後の例外インスタンスは省略でき、省略した場合は呼び出し階層を記録したスタックトレースが複雑にならないため、特別の理由がない限りは例外インスタンスを省略した方がいいだろう。

 C# 7.0より、リスト17-1の最後に示した3カ所で例外のスローを式として記述できるようになった。1ラムダ式や式形式メンバーの=>の後、2null合体演算子??の第2オペランド、3条件演算子? : の第2・第3オペランド(被演算子)、の3つである。これは「C# 6.0以降で式として記述できる箇所が増えたため、それに合わせて例外のスローも式として記述できるようにしたい」という要望に合わせたものである。

▲目次に戻る

リソースの解放

▲目次に戻る

finallyusingによるリソースの解放

C#
using System;

class Program
{
  static void Main(string[] args)
  {
    var resource = new MyResource();
    try
    {
      resource.Execute();
    }
    finally
    {
      resource.Close();
    }

    using (var r1 = new MyDisposableResource())
    using (var r2 = new MyDisposableResource())
    {
      r1.Execute();
      r2.Execute();
    }
  }
}
public class MyResource
{
  public void Execute()
  {
    Console.WriteLine("MyResource.Execute");
  }

  public void Close()
  {
    Console.WriteLine("MyResource.Close");
  }
}

public class MyDisposableResource : IDisposable
{
  public void Execute()
  {
    Console.WriteLine("MyDisposableResource.Execute");
  }

  public void Dispose()
  {
    Console.WriteLine("MyDisposableResource.Dispose");
  }
}
リスト18-1 リソースの解放

 利用したインスタンスに対し、リソースの解放などのために特定のメソッドを呼び出す必要がある場面を考えよう。例外がスローされる状況でも必ず解放処理を行うために、try-finally句を使うことができる。finally句内の処理は例外が起きた場合でも必ず実行される。しかしこの構文は少し冗長であり、変数のスコープもtryの外側に及んでしまう。

 そのため、C#ではusingステートメント(前述のusingディレクティブとは異なるキーワード)が用意されている。IDisposableインターフェースを実装したクラスはusing内に記述でき、ブロックを抜ける際に必ずその実装クラスのDispose()メソッドが呼ばれるようになっている。

▲目次に戻る

ラムダ式

▲目次に戻る

ラムダ式の宣言

C#
using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    Action a1 = () =>
    {
      Console.WriteLine("Action");
    };
    // Action
    a1();
    Action<int> a2 = i => Console.WriteLine(i);
    // 2
    a2(2);

    Func<int> f1 = () => 1;
    // 1
    f1();
    Func<string, string, int> f2 = (s1, s2) => int.Parse(s1 + s2);
    // 54
    f2.Invoke("5", "4");

    // LINQ
    // 9,1
    new[] { 1, 3, 5 }
    .Where(i => i <= 3)
    .Select(i => i * i)
    .OrderByDescending(i => i);

    // イベント
    Changed += (s, ea) => { Console.WriteLine("OnChanged"); };
  }
  
  static event EventHandler Changed;
}
リスト19-1 ラムダ式

 ラムダ式はデリゲート型などを簡潔に記述するための文法である。ラムダ式を暗黙的な型宣言varに代入することはできないが、指定された型に変換できるようにコンパイラーが判断する。基本的な記述方法は、(引数) => {式ブロック}であり、引数が1つの場合は()が省略でき、返り値の型は式ブロック内でreturnしているインスタンスの型で判断する。

 LINQのメソッド構文を記述するときやイベントの購読処理を記述するときによく利用される。

▲目次に戻る

非同期処理(async/await)

▲目次に戻る

async/await

C#
using System;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
  static void Main(string[] args)
  {
    // Mainメソッドはasyncにできない
    // 非同期メソッドを同期的に待機する場合、GetAwaiter().GetResult() が使える
    ExecuteAsync().GetAwaiter().GetResult();
  }

  static async Task ExecuteAsync()
  {
    var sw = Stopwatch.StartNew();
    // 2秒待機
    await TestAsync();
    sw.Stop();
    // await: 2 [sec]
    Console.WriteLine($"await: {sw.Elapsed.Seconds} [sec]");
    sw.Restart();
    // この時点では待機しない
    var task = TestAsync();
    // 未await: 0 [sec]
    Console.WriteLine($"未await: {sw.Elapsed.Seconds} [sec]");
    await task;
    // await: 2 [sec]
    Console.WriteLine($"await: {sw.Elapsed.Seconds} [sec]");
    sw.Restart();
    // 一度完了したTaskをawaitすると、すぐに完了する
    await task;
    // 再await: 0 [sec]
    Console.WriteLine($"再await: {sw.Elapsed.Seconds} [sec]");

    var task2 = TestAsync();
    sw.Restart();

    sw.Restart();
    // async voidメソッドを呼んでも待機しない
    VoidAsync();
    // async void: 0 [sec]
    Console.WriteLine($"async void: {sw.Elapsed.Seconds} [sec]");

    sw.Restart();
    var r1 = await TestAsync2();
    // await Task<int>: Result=1 2 [sec]
    Console.WriteLine($"await Task<int>: Result={r1} {sw.Elapsed.Seconds} [sec]");

    var t2 = TestAsync2();
    sw.Restart();
    // Task<T> のResultプロパティで結果を取得できるが、プロパティアクセスで待機することになる
    var r2 = t2.Result;
    // Get Task<int>.Result: Result=1 2 [sec]
    Console.WriteLine($"Get Task<int>.Result: Result={r2} {sw.Elapsed.Seconds} [sec]");

    sw.Restart();
    // await同様、一度完了したTaskのResultプロパティは待機せずに取得できる
    var r3 = t2.Result;
    // Get Task<int>.Result again: Result=1 0 [sec]
    Console.WriteLine($"Get Task<int>.Result again: Result={r3} {sw.Elapsed.Seconds} [sec]");
  }

  static async Task TestAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(2));
  }

  static async void VoidAsync()
  {
    await Task.Delay(TimeSpan.FromSeconds(2));
  }

  static async Task<int> TestAsync2()
  {
    await Task.Delay(TimeSpan.FromSeconds(2));
    return 1;
  }
}
リスト20-1 非同期関数

 C#ではasync修飾子を持つ関数を非同期関数と呼び、非同期という用語はこのasyncを扱う説明に使っている。C#の非同期プログラミングはasyncのみならず、APIも利用できるが、この記事ではasyncawaitの使い方を中心とした説明にとどめる。

 C# 5.0および6.0では、非同期関数は返り値がTask型およびそのジェネリクス型のTask<TResult>(いずれもSystem.Threading.Tasks名前空間)、もしくはvoidである必要がある。C# 7.0以降では、ある一定の規約に従った任意の型を戻り値にできるようになった。特にValueTask型(System.Threading.Tasks名前空間)を使った例を後述の項に載せている。

 TaskTask<TResult>型を返り値とする非同期関数は、await式により結果の取得を待機できる。サンプルコードでは非同期に2秒時間がかかる処理をしているが、awaitすることで呼び出し側が2秒待機していることが分かる。

 await Taskvoidに相当し返り値を持たず、await Task<TResult>TResult型に相当する返り値を持つ。非同期関数をawaitしない場合は、待機せず、その返り値であるタスクをawaitする、もしくはResultプロパティにアクセスしようとした時点で待機する。一度待機して完了したタスクは、再度結果を参照する時には待機しない(=再実行されない)。

 async voidな非同期関数は待機できない。主にイベントハンドラーの購読に登録する場合に利用することが多いが、例外がスローされた場合の処理が複雑になりがちであり、注意が必要である。

▲目次に戻る

ローカル関数を使ったTaskのキャッシュ

C#
using System;
using System.Threading.Tasks;

class Program
{
  static void Main(string[] args)
  {
    // Mainメソッドはasyncにできない
    // 非同期メソッドを同期的に待機する場合、GetAwaiter().GetResult() が使える
    ExecuteAsync().GetAwaiter().GetResult();
  }

  static async Task ExecuteAsync()
  {
    // "Executing time consuming method..." は最初の1回のみ表示され
    // 2回目以降はキャッシュされたTaskをすぐに返している
    var r1 = await GetAsync();
    var r2 = await GetAsync();
    var r3 = await GetAsync();
  }

  static Task<int> cache;

  static Task<int> GetAsync()
  {
    async Task<int> inner()
    {
      Console.WriteLine("Executing time consuming method...");
      await Task.Delay(3000);
      return 1;
    }
    cache = cache ?? inner();
    return cache;
  }
}
リスト20-2 ローカル関数を使ったTaskのキャッシュ

 非同期メソッドの中には、「初回実行時のみ値を非同期で取得する必要があるが、1回取得すれば2回目以降は非同期処理を実行せずに初回実行時の結果だけを返したい」というユースケースがある。このような場合、最初に実行したときの結果であるTaskインスタンスを保持しておき、2回目以降はそのTaskインスタンスを返せばよい。C# 7.0で導入されたローカル関数を使うことで、このようなTaskインスタンスのキャッシュ処理も簡潔に記述できるようになった。

▲目次に戻る

ValueTaskクラス

 C# 7.0より、ある一定の規約に従った任意の型をasyncメソッドの返り値として利用できるようになった。この規約自体はやや複雑であるためこの記事では触れないが、ValueTaskクラスというものが標準でasyncメソッドの返り値の型として追加された。

 ValueTaskは名前の通り、「値型として扱えるTask」的な型である。実装としては、待機している処理が完了したときに得られる値、もしくは完了前であればTaskインスタンスのどちらかを保持する構造体となっている。また、C# 6.0以前でTaskもしくはTask<T>を使っていたところを、そのままValueTaskもしくはValueTask<T>に置き換えることができる。

 執筆時点で筆者が調べた環境では、.NET Core 1.1TargetFrameworknetcoreapp1.1と指定)以外(調べた範囲は.NET Framework 4.7および4.6)も含めて、NuGetから「System.Threading.Tasks.Extensions」パッケージの追加が必要となっている。なおValueTaskクラス自体はNuGetライブラリをダウンロードしなくてもクラスライブラリに含まれているがasyncメソッドの返り値の型として利用するためにNuGetライブラリの追加が必要なケースがある。

C#
using System;
using System.Threading.Tasks;

class Program
{
  static void Main(string[] args)
  {
    async Task Inner()
    {
      var res = await SearchAsync(100);
      Console.WriteLine(res);
      res = await SearchAsync(1);
      Console.WriteLine(res);
    }
    Inner().GetAwaiter().GetResult();
  }

  static async ValueTask<int> SearchAsync(int a)
  {
    if (a != 100)
      return 0;
    await Task.Delay(1000);
    return 1;
  }
}
リスト20-3 ValueTask

 これがパフォーマンス上のメリットとなる場合がある。asyncキーワードの付いた非同期メソッドとなっているが、ほとんどの場合(=サンプルコードでは「100」以外の値の場合)、同期的に処理を返すような処理があったとする。返り値がTaskの場合、同期的に処理を行う場合でもTaskクラスをインスタンス化しないといけないため、GC(ガベージコレクション)のコストがかかるが、ValueTaskは構造体であるためValueTaskを生成してもGCのコストはかからない。

▲目次に戻る

 以上でC#の主要な文法を一通り説明した。文法を羅列するだけでもかなりの項目数だったが、本稿では実利用者目線でできるだけコンパクトにまとまるように努力した。ぜひ、日々のコーディングの「あれ、どう書くんだっけ?」という場面で役立てていただけるとうれしい。

1. C# 基礎文法【6.0対応】 ― 1回完結の最速再入門!

項目を羅列するだけでもかなり長くなってしまうC# 6.0の主要な文法を、実利用者目線でできるだけコンパクトにまとめた。日々のコーディングの「あれ、どう書くんだっけ?」を素早く解決するためのリファレンス。

2. C# 基礎文法 最速再入門【7.0対応】

「あれ、どう書くんだっけ?」を素早く解決するための、C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。前編では、C#の歴史/開発ツール/プログラムの実行と制御/型と変数/演算子/ステートメントを説明する。

3. C# クラスの基本機能 最速再入門【7.0対応】

C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。中編では、名前空間/クラス/メソッド/プロパティ/イベント/インデクサー/演算子オーバーロード/コンストラクターとデストラクターを説明する。

4. 【現在、表示中】≫ C# タプル/ローカル関数/ラムダ式/非同期処理/例外処理 最速再入門【7.0対応】

C# 7.0主要文法がコンパクトにまとまったリファレンス(全3回)。後編では、タプルと分解(デコンストラクション)/ローカル関数/構造体/継承とインターフェース/列挙型/イテレーター/例外処理/リソースの解放/ラムダ式/非同期処理(async/await)を説明する。

サイトからのお知らせ

Twitterでつぶやこう!


Build Insider賛同企業・団体

Build Insiderは、以下の企業・団体の支援を受けて活動しています(募集概要)。

ゴールドレベル

  • 日本マイクロソフト株式会社
  • グレープシティ株式会社