>>  賞品はこちらで紹介しています 
最新C#言語概説

最新C#言語概説

C# 6.0で知っておくべき12の新機能
― オープンな議論からの最新情報 ―

2015年7月22日 改訂 (初版:2015/2/9)

Visual Studio 2015正式版のリリースで利用可能になったC#言語の最新バージョン「6.0」の新機能を解説する。CTP 5→正式版に合わせて改訂。

株式会社グラニ 田中 孝佳
  • このエントリーをはてなブックマークに追加

 「C# 6.0」と呼ばれているC#の最新バージョンは、Visual Studio 2015*1で利用可能になっている。

 この最新バージョンでは、「.NET Compiler Platform」(コード名:“Roslyn”)と呼ばれる新しいコンパイラーが導入されており、静的解析APIの提供など、コンパイラーまわりに大きな変更が行われている。一方、言語機能に目を向けると、async/awaitという大きな機能が追加されたC# 5.0に比べると、一つ一つの新機能自体は小さい。しかし、それらはプログラムをより書きやすくするための機能なので、C#開発者にとってはやはり重要なアップデートとなっている。

 そして、これらの新機能に関する議論は、2014年4月より公開された場所(今年1月まではCodePlex、それ以降はGitHub)で行われていた。この議論は、当時の次期バージョン(C# 6.0)のみならず、すでにその次の「C# 7.0」と呼ばれるバージョンの言語機能についても行われている(現在は、週次のDesign Notesの結果がGitHubのIssueに投稿されており、今後さらにオープンにしていくことが検討されている)。

 本稿では、そうやって公開されている議論と、実際にリリースされた機能内容を基に、C# 6.0の新しい言語機能を解説する。

  • *1 本稿は、Visual Studio 2015 CTP 5の時点で、検証可能な新機能を解説した記事だったが、Visual Studio 2015 正式版にリリースに合わせて、内容を見直し、最新の情報に改訂したものである。

1Auto-property enhancements(自動実装プロパティの機能強化)

Initializers for auto-properties(自動実装プロパティ用の初期化子)

C#
public class Person1
{
 public string First { get; set; } = "Taro";
 public string Last { get; set; } = "Tanaka";
}
自動プロパティの初期化

 自動実装プロパティの初期値を記述できるようになった。初期値はフィールドに直接代入され、Setterが実行されるわけではない。また、その他のインスタンスフィールド同様、記述された順に実行される。

 また、他のフィールド同様、thisで自身のインスタンスを参照することはできない(thisをつけずに参照しても同じである)。全てのフィールドの初期化が完了してから、オブジェクトの初期化が完了し、thisで参照できるようになるからである。

C#
public class BI_1_1_Person1
{
 public string First { get; set; } = "Taro";
 public string Last { get; set; } = "Tanaka";
 public string FullName { get; set; } = this.First + " " + Last; // コンパイルエラー
}
インタンス参照すると(thisの有無によらず)エラーになる

Getter-only auto properties(Getterのみの自動実装プロパティ)

C#
public class BI_1_2_Person
{
 public string First { get; } = "Taro";
 public string Last { get; } = "Tanaka";

 // Aコンストラクターの引数で初期化
 public BI_1_2_Person(string first, string last)
 {
   First = first;
   Last = last;
 }

 // Bフィールドで記述している初期値で初期化
 public BI_1_2_Person()
 {}
}
Getterのみの自動プロパティ

 自動実装プロパティに初期値を渡して初期化できるようになった。Getterのみの自動プロパティは、バッキングフィールドがreadonlyとして扱われるのだが、先に紹介した「自動プロパティの初期化」による初期値を与える方法(B)、もしくはコンストラクター内での初期値の代入によって(A)、初期化ができるようになった。

 C# 5.0までは、readonlyなプロパティを作成するときには自動実装プロパティは使えなかった。つまり、Immutableなクラス(=不変クラス。オブジェクトを初期化した時点でプロパティやフィールドの値が確定し、変更されないクラス)を実装したいときは、自動実装プロパティが使えなかった。この機能により、ImmutableなクラスでもMutableなクラス同様、自動実装プロパティにより実装できる。

2Expression bodied function members(ラムダ式本体によるメンバーの記述)

 式形式のラムダ式の本体(=ラムダ演算子=>の右側が{}でくくられるステートメントではなく、であるものの、式本体)を、メソッドやプロパティの本体の記述に使えるようになった。

Expression bodies on method-like members(ラムダ式本体によるメソッドの記述)

C#
public class BI_2_1_Point
{
 public BI_2_1_Point(int x, int y)
 {
   X = x;
   Y = y;
 }
 public int X { get; set; }
 public int Y { get; set; }

 // メソッド本体としてラムダ式を使用する例
 public double Distance(BI_2_1_Point p)
   => Math.Sqrt((p.X - X) * (p.X - X) + (p.Y - Y) * (p.Y - Y));

 // voidの場合も記述できる。ラムが式の本体が値を返す場合でも利用可
 public void Dump() => Console.WriteLine("({0},{1})", X, Y);

 // asyncメソッドの場合、Taskを返すラムダ式でawaitすればOK
 //(async、awaitを付けず、返り値がTaskのメソッドにするのも検討してください)
 public async Task LoadAsync(int x, int y) => await Task.Run(() =>
 {
   X = x;
   Y = y;
 });

 // 演算子オーバーロードも可能
 public static BI_2_1_Point operator +(BI_2_1_Point a, BI_2_1_Point b)
   => new BI_2_1_Point(a.X + b.X, a.Y + b.Y);

 // この例はあまり適切ではないが、型変換演算子にも適用可
 public static implicit operator string (BI_2_1_Point p)
   => string.Format("({0},{1})", p.X, p.Y);
}
ラムダ式本体によるメソッドの記述

 メソッド本体の記述として、ラムダ式本体を利用できる。サンプルコードにあるように、基本的には副作用が生じず、何か値を返すだけのメソッドで利用することが想定されている。このような想定があるため、コンストラクター/イベント/デストラクターでは使えない。

Expression bodies on property-like function members(ラムダ式本体によるプロパティの記述)

 メソッドだけではなく、プロパティやインデクサー本体の記述にもラムダ式本体が利用できる。この記述方法を使った場合、自動的にGetterのみとなり、getキーワードは不要である。

C#
public class BI_2_2_Point
{
 public IDictionary<int, int> Store = new Dictionary<int, int>();
 public int X { get; }
 public int Y { get; }

 public BI_2_2_Point(int x, int y)
 {
   X = x;
   Y = y;
 }

 public double Length => Math.Sqrt(X * X + Y * Y);
 public int this[int id] => Store[id];
}
ラムダ式本体によるプロパティの記述

3using static

 staticメソッドを使う際に、そのメソッドが所属するクラスを(usingディレクティブではなく)using staticディレクティブを使って定義すると、クラス名を指定せず、メソッドのみを記述できるようになった。System.ConsoleクラスやSystem.Mathクラスなどが代表的な利用対象だろう。

 なお、Visual Studio 2015 PreviewからCTP 5のタイミングで文法が変更されている(以前はusing staticではなく通常の名前空間と同じusingキーワードだった)。また拡張メソッドは修飾子的にはstaticであるが、インスタンスメソッド的に呼びだすことになるため、using staticは使えない。また、クラスだけでなく、列挙体や構造体についてもusing staticすることができる。

C#
using static System.Console;
using static System.Math;
using static System.Linq.Enumerable;
using static System.Net.HttpStatusCode;
using static System.DateTime;
 
class BI_3_1_Program
{
 internal static void Main2()
 {
   // 上記のusing staticの定義により、System.Consoleの記述は省略できる
   WriteLine(Sqrt(3 * 3 + 4 * 4));

   // 拡張メソッドではないのでOK
   var range = Range(1, 10);

   // 拡張メソッドなのでコンパイルエラーになる
   var odd = Where(range, i => i % 2 == 1);

   // 列挙体や構造体もusing staticできる
   WriteLine(NotFound);
   WriteLine(Now);
 }
}
using staticディレクティブ

4Null-conditional operators(Null条件演算子)

 この言語機能が提案された当初は、「Null propagating operators(Null伝搬演算子)」とも呼ばれていた機能だが、正式名はどうやらNull-conditional operatorsになるようである。?.という演算子を導入し、機能としては、?.の前がnullであればnullを返し、nullでなければ後続の処理結果を返す、という機能である。

C#
class BI_4_1_Program
{
 internal void Execute()
 {
   IList<Person> persons = null;
   // personsがnullであればnull、それ以外ならCount()の結果を返す
   int? count = persons?.Count();

   Person first = persons?[0];
   // デフォルト値をNull合体演算子(??)で記述できる
   int countWithDefault = persons?.Count() ?? 0;

   // 短絡評価する(ショートサーキット)
   int? first1 = persons?[0].Freinds.Count();
   // 下記の文は、上と同じ意味になる
   int? first2 = (persons != null) ?
                    persons[0].Freinds.Count() :
                    (int?)null;
 }

 class Person
 {
   public IEnumerable<Person> Freinds { get; set; }
 }
}
Null条件演算子

 Null条件演算子の返す方は、右辺の返す型がオブジェクトであればそのまま、値型や構造体であればNull許容型(例えばint?など)になる。

 また、delegate型のオブジェクトに対してはInvokeメソッドを通じて、そのデリゲートのメソッドを実行できる。一度、ローカル変数に格納した上で、nullチェックをして実行されるため、スレッドセーフな呼び出しになる。特にEventHandlerを呼び出すときなど、C# 5.0までは「ローカル変数への格納」「nullチェック」「メソッド呼び出し」と3行の記述が必要だったのが、Null条件演算子を使うと1行で簡潔に記述できるようなった。

C#
public Predicate<string> Predicate { get; set; }
public PropertyChangedEventHandler PropertyChanged { get; set; }

public void Execute(object sender, string args)
{
 if (Predicate?.Invoke(args) ?? false)
 {
   PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs("Property"));
   // 下記の文は、上と同じ意味になる
   var handler = PropertyChanged;
   if (handler != null)
     handler(sender, new PropertyChangedEventArgs("Property"));
 }
}
デリゲートやイベントハンドラーに対するNull条件演算子の使用方法

5String interpolation(文字列補間)

 String interpolationstring(あるいはIFormattable)型の値を、文字列リテラルだけで完結して記述するための機能である。

 C# 5.0まではString.Formatメソッドで同様のことができていたが、文字列内の置換変数と置換対象が離れており分かりづらいのに加えて、置換変数のインデックスと、置換対象の可変長引数の順番や数が一致しないバグが起きやすかった。そういった課題を踏まえて、String interpolationが導入された。

C#
public string Name { get; set; }
public int Price;


public void Dump()
{
 var s1 = $"Item name is {Name}";
 var s2 = string.Format("Item name is {0}", Name);


 // {{-}} で {-} と出力される(エスケープ)
 var s3 = $"{Name,20}: {Price:C} {{-}}";
 var s4 = string.Format("{0,20}: {1:C} {{-}}", Name, Price);


 var s5 = $"Now: {DateTime.Now :f}";
 var s6 = string.Format("Now: {0:f}", DateTime.Now);
 var tax = Price * 0.08;
 var s7 = $@"
Price: {Price :C} /
Tax: {tax :C} /
";
 var s8 = string.Format(@"
Price: {0 :C} /
Tax: {1 :C} /
", Price, tax);

 var s9  = $"{string.IsNullOrEmpty(Name) ? "未入力" : Name}"; //コンパイルエラー
 var s10 = $"{(string.IsNullOrEmpty(Name) ? "未入力" : Name)}";

}
文字列補間

 使い方は、それぞれ下の行に書いている同等の意味を持つ「string.Formatを使った記述方法」と比べてほしい。文字列リテラルの引用符(")の前に$を付けて記述する。逐語的文字列リテラルと併用することも可能で、その場合は@の前に$をつける。string.Formatでは{0}と可変長引数のインデックスを指定したところを、直接NamePriceといったプロパティ名(およびフィールド変数名やローカル変数名など)を指定して記述する。右揃えや通貨形式といった書式指定をすることもできる。また、文字列としての{}を記述したい場合は、{{}}のように2つ重ねることでエスケープ指定する。また、三項演算子を直接{ }内に記述するとコンパイルエラーになるが(: が書式指定と認識されるため)、( )でくくると三項演算子として解釈されるようになる。

 なお、String interpolationには.NET 4.6でのみ利用可能な機能がある。$" "でくくられたリテラルは通常、string型のインスタンスとなる。しかし、.NET 4.6で、かつ代入先の変数の型をIFormattableにすると、その変数はIFormattableインターフェースを実装した型のインスタンスになる。この機能を利用すると、以下のようにカルチャを指定した書式変換ができるようになる。

C#
public void DumpCulture()
{
 var s1 = $"{12345.67:C}"; // 現在のカルチャで変換。s1に代入されるのはstring型
 var s2 = ToSpecificCulture($"{12345.67:C}", "en-US"); // カルチャを指定して変換
 IFormattable format = $"{12345.67:C}"; // formatに代入されるのはIFormattableを実装した型
 var s3 = ToSpecificCulture(format, "fr-FR");
}

public static string ToSpecificCulture(IFormattable format, string culture)
{
 return format.ToString(null, new CultureInfo(culture));
}
.NET 4.6でのみ利用可能な文字列補間の機能: IFormattable型

6nameof operators(nameof演算子)

 nameof演算子は式を与えて、その式の名前を文字列で返す演算子である。単純には、変数やプロパティ、メソッドなどの式をnameof演算子に与えると、それらの名前を文字列として取得できる。

 実際に指定できるものはになるが、一見、式のように見えて式ではないためnameof演算子に使えないものもある。具体的には下記のサンプルコードを見てほしい。

C#
public int Prop { get; set; }
void F() { }
void F(int x) { }
public class Person6
{
 private string count;
 public int Age { get; set; }
}

public static void Execute(string args)
{
 Console.WriteLine(nameof(args));
 // 文字列として「args」と出力される

 var x = 2;
 Console.WriteLine(nameof(x));
 Console.WriteLine(nameof(Prop));
 Console.WriteLine(nameof(F));

 var p = new Person6();
 Console.WriteLine(nameof(p.Age));
 // アクセスレベルがアクセスできないのでコンパイルエラー
 Console.WriteLine(nameof(p.count));

 Console.WriteLine(nameof(System.DateTime));

 Console.WriteLine(nameof(List<int>));
 // 初期のCTPでは問題なかったが、現在ではエラーになる
 Console.WriteLine(nameof(Tuple<,,,,,,,>.GetHashCode));
 Console.WriteLine(nameof(Tuple<int,int,int>.GetHashCode));

 // defaultは使えない
 Console.WriteLine(nameof(default(List<int>).Length));
 // intなどの組み込み型は使えない
 Console.WriteLine(nameof(int));
 // 名前空間もOK
 Console.WriteLine(nameof(System.Linq) );

 // 直接文字列は指定できない
 Console.WriteLine(nameof("name"));

 var @int = 5;  // 「@」プリフィックス(逐語的識別子)を付けた変数名
 Console.WriteLine(nameof(@int));

 // pointerは使えない
 Console.WriteLine(nameof(Buffer*));
}
nameof演算子

 System.Int32は指定できるが、組み込み型であるintは指定できない。boolobjectなども同様である。また、以前までは型制約を空白にして指定したものが使えなくなり、具体的な型を指定しないといけないようになった。

 nameof演算子の主な目的は、変数名の文字列が必要な場所で今まで文字列を指定していたため、IDEのリファクタリング機能などが使えなかったところを使えるようにすることである。いくつかnameofを活用できる例を挙げてみる。

C#
// 引数のチェック
public void Log(string x)
{
 if (x == null) throw new ArgumentNullException(nameof(x));
}

// PropertyChangedEvent
int age;
int Age
{
 get { return this.age; }
 set { this.age = value; PropertyChanged(this, new PropertyChangedEventArgs(nameof(this.Age))); }
}

// 属性
[DebuggerDisplay("={" + nameof(GetString) + "()}")]
class C
{
 string GetString() { return "a";  }
}
nameof演算子を活用できる例(1): ArgumentNullExceptionで指定する例外が発生した引数の名前/PropertyChangedEventArgsで指定するプロパティ名/DebuggerDisplay属性で指定するメソッドなどの名前
CSHTML
<%= Html.ActionLink("Login",
      @typeof(UserController),
      @nameof(UserController.Login))
%>
nameof演算子を活用できる例(2):Html.ActionLinkメソッドで指定するコントローラー名やアクション名

7Index initializers(インデックス初期化子)

 Dictionary<TKey, TValue>に代表されるインデックスアクセス可能なオブジェクトを初期化する際に使える、新しい記法が導入された。これにより、通常のオブジェクト初期化子によるプロパティの初期化と同じ記法で初期化できるようになった。

C#
public class CustomerStore
{
 private Dictionary<int, Customer> store = new Dictionary<int, Customer>();
 public Customer this[int id]
 {
   get
   {
     return store[id];
   }
   set
   {
     store[id] = value;
   }
 }
 public string Prop { get; set; }
}

public static void Execute()
{
 var dict = new CustomerStore
 {
   Prop = "Property",
   [1] = new Customer(1),
   [2] = new Customer(2)
 };

 // 2つの記法を混ぜることはできないのでコンパイルエラー
 var dict2 = new CustomerStore
 {
   Prop = "Property",
   { 1, new Customer(1) }
 };

 // コレクション初期化子も今まで通り利用可能。
 // この記法は、対象クラスがIEnumerableを実装し、Addメソッドを実装していることが必要
 var dict3 = new Dictionary<int, Customer>
 {
   { 1, new Customer(1) },
   { 2, new Customer(2) }
 };
}
インデックス初期化子

8Exception filters(例外フィルター)

 VB(Visual Basic)やF#では同様の機能が実装されているが、C#にもException filtersが実装された。例外処理のcatch節にwhen節を記述すると。if節の条件がtrueの場合のみ、そのcatchブロック内が実行される。Exception filtersはStackTraceを変更しないので、例外をキャッチ&リスローするより都合のいいことがある。なお、CTP 5時点ではcatchの条件を指定するのにif節を使ったが、when節に変更されているので注意してほしい。

C#
public static void Execute()
{
 try
 {
   DoSomeHttpRequest();
 }
 catch (WebException e) when (e.Status == WebExceptionStatus.NameResolutionFailure)
 {
   Console.WriteLine("名前解決できませんでした");
 }
 catch (WebException e) when (e.Status == WebExceptionStatus.RequestCanceled)
 {
   Console.WriteLine("リクエストがキャンセルされました");
 }
 catch
 {
   Console.WriteLine("その他のエラー");
 }
}
例外フィルター

 when節には条件しか書けないが、bool値を返すメソッド内で副作用を伴う処理も実行できる。Exception filtersで副作用を伴うのは分かりづらくなることもあるが、StackTraceを変更しない点を活用してロギングに用いることもできる。

C#
private static bool Log(Exception e)
{
 // ロギング処理
 Console.WriteLine(e.Message + e.StackTrace);
 return false;
}

public void Execute2()
{
 try
 {
   DoSomeHttpRequest();
 }
 catch (WebException e) when (Log(e)) { }
}
例外フィルターはスタックトレースを変更しないのでロギングに活用可能

9Await in catch and finally blocks(catchおよびfinallyブロック内でのawait)

 C# 5.0で導入されたasyncawaitだが、catchおよびfinallyブロック内でawaitできないという仕様があった。このため、非同期メソッド(下記の例ではSendAsyncメソッド)で例外発生時に、非同期メソッド(例ではRetryAsyncメソッド)でリトライしたい場合や、リソース解放処理が非同期メソッド(例ではCloseAsyncメソッド)になっている場合には、catchおよびfinallyブロック内でそういった非同期メソッドを呼ばないように工夫をする必要があった。C# 6.0ではcatchおよびfinallyブロック内でawaitできるようになったので、非同期メソッドが呼び出せる。

C#
var req = new MyRequest();
try
{
 var res = await req.SendAsync();  // 非同期メソッド
}
catch (Exception e)
{
 await req.RetryAsync();
}
finally
{
 if (req != null) await req.CloseAsync();
}
catch/finallyブロック内でのawait

10Parameterless constructors in structs(パラメーターを持たない構造体コンストラクター)

 パラメーターを持たないコンストラクターはCTP 5時点では新機能として実装されていたが、正式版では実装が見送られることになった。

11Extension Add methods in collection initializers(コレクション初期化子内でのAdd拡張メソッドの利用)

 コレクション初期化子で追加される要素は、(その内部では)Addメソッドを実行して追加処理が行われる。そのため、Addメソッドを持たないクラスは、コレクション初期化子で初期化できなかった。拡張メソッドでAddメソッドを定義してもコレクション初期化子が利用できなかったが、C# 6.0でこの挙動が変更され、拡張メソッドが定義されていればコレクション初期化子が利用できるようになった。

C#
public static void Execute()
{
 var list = new Queue<string>
 {
   "item1",
   "item2",
   "item3"
 };
}

public static class Extensions
{
 public static void Add<T>(this Queue<T> source, T item)
 {
   source.Enqueue(item);
 }
}
Add拡張メソッドが定義されていればコレクション初期化子が利用できる

12Improved overload resolution(オーバーロード解決の向上)

 「定義されたオーバーロードメソッドのうち、どのメソッドを実行するか決定するオーバーロード解決を向上した」と記述されている。しかし、どのように向上して、挙動がどう変わったかについての詳細な言及は今のところ公式には確認できていない本節の以下の説明は2015/7/24に追記しました)

 このオーバーロード解決の向上は、C#のバージョンが上がるたびに行われており、より「優れている(betterness)」ように改善されている。その中の一部分ではあるが、Microsoft MVPのブログに基づく情報だが、返り値がメソッドグループ(=ActionFunc<T>、および引数を指定するジェネリクスが追加されたもの)である複数のメソッドのオーバーロード解決がC# 6.0で改善されていることが指摘されている。参照先のブログ記事のコード(以下に引用)は、Visual Studio 2012(C# 5.0)ではコンパイルできないが、Visual Studio 2015(C# 6.0)ではコンパイルできる。

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace testoverloadresolution
{
 class Program
 {
   private static void TakesItem(Action item)
   {
     
   }
   private static int TakesItem(Func <int>  item)
   {
     return item();
   }
   public static int somefunction()
   {
     return 50;
   }
   static void Main(string []  args)
   {
     int resultitem = TakesItem(somefunction);
     Console.WriteLine(resultitem);
     Console.ReadKey();
   }
 }
}
参照先の記事にあるコードを転載したもの(引用)

C# 6.0でコンパイルエラーが出ないように、オーバーロード解決が向上した。

 C# 5.0では、メソッドグループを引数に指定したところ、ActionFunc<int>の間の解決があいまいになり、かつActionの場合の返り値がvoidであるため、メソッドの結果を変数に代入しているところでもエラーになる。

 C# 6.0では、返り値の型まで参照して、より良いFunc<int>にオーバーロードが解決されている。

13#pragma Warning Disable(#pragmaによるユーザー定義コンパイラー警告の抑止)

 “Roslyn”と呼ばれる新しいコンパイラープラットフォームにより、誰でもコンパイラー警告を増やせるようになった。もともと、「#pragma(プラグマ)」と呼ばれる機能でコンパイラー警告を抑止する機能があったが、この機能をユーザーが定義したコンパイラー警告に対しても利用できるようにした。

C#
#pragma warning disable "MyCustomDiagnostics"
#pragma warning restore
#pragmaによるユーザー定義コンパイラー警告の抑止

 以上、12(=13-1)の項目が、C# 6.0で追加された新機能である。

 また、“Roslyn”が2014年4月にリリースされた時点で予定されていた機能と比べると、少なく・小ぶりになっているのに気付かれた方もいるかもしれない。これは、最新バージョンのC#および.NETの最大の目標が“Roslyn”という新しいコンパイラープラットフォームを導入することにあり、安定した“Roslyn”のリリースを優先しているためでもある。そして、コンパイラーやSDKに新機能を載せるだけでなく、「Visual Studio」というIDEでの新機能の支援まで対応してからリリースしている。これは、「C#」という言語の特徴だといえるだろう。

GrapeCity Garage 記事内容の紹介

Azure Central の記事内容の紹介

Twitterでつぶやこう!


Build Insider賛同企業・団体

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

ゴールドレベル

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