最新C# 6言語概説
C# 6.0で知っておくべき12の新機能
― オープンな議論からの最新情報 ―
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(自動実装プロパティ用の初期化子)
public class Person1
{
public string First { get; set; } = "Taro";
public string Last { get; set; } = "Tanaka";
}
|
自動実装プロパティの初期値を記述できるようになった。初期値はフィールドに直接代入され、Setterが実行されるわけではない。また、その他のインスタンスフィールド同様、記述された順に実行される。
また、他のフィールド同様、this
で自身のインスタンスを参照することはできない(※this
をつけずに参照しても同じである)。全てのフィールドの初期化が完了してから、オブジェクトの初期化が完了し、this
で参照できるようになるからである。
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; // コンパイルエラー
}
|
Getter-only auto properties(Getterのみの自動実装プロパティ)
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のみの自動プロパティは、バッキングフィールドがreadonlyとして扱われるのだが、先に紹介した「自動プロパティの初期化」による初期値を与える方法(B)、もしくはコンストラクター内での初期値の代入によって(A)、初期化ができるようになった。
C# 5.0までは、readonlyなプロパティを作成するときには自動実装プロパティは使えなかった。つまり、Immutableなクラス(=不変クラス。オブジェクトを初期化した時点でプロパティやフィールドの値が確定し、変更されないクラス)を実装したいときは、自動実装プロパティが使えなかった。この機能により、ImmutableなクラスでもMutableなクラス同様、自動実装プロパティにより実装できる。
2Expression bodied function members(ラムダ式本体によるメンバーの記述)
式形式のラムダ式の本体(=ラムダ演算子=>
の右側が{}
でくくられるステートメントではなく、式であるものの、式本体)を、メソッドやプロパティの本体の記述に使えるようになった。
Expression bodies on method-like members(ラムダ式本体によるメソッドの記述)
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
キーワードは不要である。
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することができる。
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);
}
}
|
4Null-conditional operators(Null条件演算子)
この言語機能が提案された当初は、「Null propagating operators(Null伝搬演算子)」とも呼ばれていた機能だが、正式名はどうやらNull-conditional operatorsになるようである。?.
という演算子を導入し、機能としては、?.
の前がnull
であればnull
を返し、null
でなければ後続の処理結果を返す、という機能である。
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許容型(例えばint?
など)になる。
また、delegate型のオブジェクトに対してはInvoke
メソッドを通じて、そのデリゲートのメソッドを実行できる。一度、ローカル変数に格納した上で、nullチェックをして実行されるため、スレッドセーフな呼び出しになる。特にEventHandlerを呼び出すときなど、C# 5.0までは「ローカル変数への格納」「nullチェック」「メソッド呼び出し」と3行の記述が必要だったのが、Null条件演算子を使うと1行で簡潔に記述できるようなった。
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"));
}
}
|
5String interpolation(文字列補間)
String interpolationはstring
(あるいはIFormattable
)型の値を、文字列リテラルだけで完結して記述するための機能である。
C# 5.0まではString.Format
メソッドで同様のことができていたが、文字列内の置換変数と置換対象が離れており分かりづらいのに加えて、置換変数のインデックスと、置換対象の可変長引数の順番や数が一致しないバグが起きやすかった。そういった課題を踏まえて、String interpolationが導入された。
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}
と可変長引数のインデックスを指定したところを、直接Name
やPrice
といったプロパティ名(およびフィールド変数名やローカル変数名など)を指定して記述する。右揃えや通貨形式といった書式指定をすることもできる。また、文字列としての{
や}
を記述したい場合は、{{
や}}
のように2つ重ねることでエスケープ指定する。また、三項演算子を直接{ }
内に記述するとコンパイルエラーになるが(:
が書式指定と認識されるため)、( )
でくくると三項演算子として解釈されるようになる。
なお、String interpolationには.NET 4.6でのみ利用可能な機能がある。$" "
でくくられたリテラルは通常、string
型のインスタンスとなる。しかし、.NET 4.6で、かつ代入先の変数の型をIFormattable
にすると、その変数はIFormattable
インターフェースを実装した型のインスタンスになる。この機能を利用すると、以下のようにカルチャを指定した書式変換ができるようになる。
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));
}
|
6nameof operators(nameof演算子)
nameof
演算子は式を与えて、その式の名前を文字列で返す演算子である。単純には、変数やプロパティ、メソッドなどの式をnameof演算子に与えると、それらの名前を文字列として取得できる。
実際に指定できるものは式になるが、一見、式のように見えて式ではないためnameof演算子に使えないものもある。具体的には下記のサンプルコードを見てほしい。
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*));
}
|
System.Int32
は指定できるが、組み込み型であるint
は指定できない。bool
、object
なども同様である。また、以前までは型制約を空白にして指定したものが使えなくなり、具体的な型を指定しないといけないようになった。
nameof
演算子の主な目的は、変数名の文字列が必要な場所で今まで文字列を指定していたため、IDEのリファクタリング機能などが使えなかったところを使えるようにすることである。いくつかnameof
を活用できる例を挙げてみる。
// 引数のチェック
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"; }
}
|
<%= Html.ActionLink("Login",
@typeof(UserController),
@nameof(UserController.Login))
%>
|
7Index initializers(インデックス初期化子)
Dictionary<TKey, TValue>
に代表されるインデックスアクセス可能なオブジェクトを初期化する際に使える、新しい記法が導入された。これにより、通常のオブジェクト初期化子によるプロパティの初期化と同じ記法で初期化できるようになった。
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
節に変更されているので注意してほしい。
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
を変更しない点を活用してロギングに用いることもできる。
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で導入されたasync/awaitだが、catch
およびfinally
ブロック内でawait
できないという仕様があった。このため、非同期メソッド(下記の例ではSendAsync
メソッド)で例外発生時に、非同期メソッド(例ではRetryAsync
メソッド)でリトライしたい場合や、リソース解放処理が非同期メソッド(例ではCloseAsync
メソッド)になっている場合には、catch
およびfinally
ブロック内でそういった非同期メソッドを呼ばないように工夫をする必要があった。C# 6.0ではcatch
およびfinally
ブロック内でawait
できるようになったので、非同期メソッドが呼び出せる。
var req = new MyRequest();
try
{
var res = await req.SendAsync(); // 非同期メソッド
}
catch (Exception e)
{
await req.RetryAsync();
}
finally
{
if (req != null) await req.CloseAsync();
}
|
10Parameterless constructors in structs(パラメーターを持たない構造体コンストラクター)
パラメーターを持たないコンストラクターはCTP 5時点では新機能として実装されていたが、正式版では実装が見送られることになった。
11Extension Add methods in collection initializers(コレクション初期化子内でのAdd拡張メソッドの利用)
コレクション初期化子で追加される要素は、(その内部では)Add
メソッドを実行して追加処理が行われる。そのため、Add
メソッドを持たないクラスは、コレクション初期化子で初期化できなかった。拡張メソッドでAdd
メソッドを定義してもコレクション初期化子が利用できなかったが、C# 6.0でこの挙動が変更され、拡張メソッドが定義されていればコレクション初期化子が利用できるようになった。
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);
}
}
|
12Improved overload resolution(オーバーロード解決の向上)
「定義されたオーバーロードメソッドのうち、どのメソッドを実行するか決定するオーバーロード解決を向上した」と記述されている。しかし、どのように向上して、挙動がどう変わったかについての詳細な言及は今のところ公式には確認できていない(※本節の以下の説明は2015/7/24に追記しました)。
このオーバーロード解決の向上は、C#のバージョンが上がるたびに行われており、より「優れている(betterness)」ように改善されている。その中の一部分ではあるが、Microsoft MVPのブログに基づく情報だが、返り値がメソッドグループ(=Action
やFunc<T>
、および引数を指定するジェネリクスが追加されたもの)である複数のメソッドのオーバーロード解決がC# 6.0で改善されていることが指摘されている。参照先のブログ記事のコード(以下に引用)は、Visual Studio 2012(C# 5.0)ではコンパイルできないが、Visual Studio 2015(C# 6.0)ではコンパイルできる。
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では、メソッドグループを引数に指定したところ、Action
とFunc<int>
の間の解決があいまいになり、かつAction
の場合の返り値がvoid
であるため、メソッドの結果を変数に代入しているところでもエラーになる。
C# 6.0では、返り値の型まで参照して、より良いFunc<int>
にオーバーロードが解決されている。
13#pragma Warning Disable(#pragmaによるユーザー定義コンパイラー警告の抑止)
“Roslyn”と呼ばれる新しいコンパイラープラットフォームにより、誰でもコンパイラー警告を増やせるようになった。もともと、「#pragma(プラグマ)」と呼ばれる機能でコンパイラー警告を抑止する機能があったが、この機能をユーザーが定義したコンパイラー警告に対しても利用できるようにした。
#pragma warning disable "MyCustomDiagnostics"
#pragma warning restore
|
■
以上、12(=13-1)の項目が、C# 6.0で追加された新機能である。
また、“Roslyn”が2014年4月にリリースされた時点で予定されていた機能と比べると、少なく・小ぶりになっているのに気付かれた方もいるかもしれない。これは、最新バージョンのC#および.NETの最大の目標が“Roslyn”という新しいコンパイラープラットフォームを導入することにあり、安定した“Roslyn”のリリースを優先しているためでもある。そして、コンパイラーやSDKに新機能を載せるだけでなく、「Visual Studio」というIDEでの新機能の支援まで対応してからリリースしている。これは、「C#」という言語の特徴だといえるだろう。
1. 【現在、表示中】≫ C# 6.0で知っておくべき12の新機能
Visual Studio 2015正式版のリリースで利用可能になったC#言語の最新バージョン「6.0」の新機能を解説する。CTP 5→正式版に合わせて改訂。
2. C# 7.0で知っておくべき10の新機能(前編)
Visual Studio 2017およびVisual Studio Codeで利用可能になったC#言語の新バージョン「7.0」の新機能を、公開されている議論を基に解説。前編として「データ中心設計」に関連する4つの新機能を説明する。
3. C# 7.0で知っておくべき10の新機能(後編)
Visual Studio 2017およびVisual Studio Codeで利用可能になったC#言語の新バージョン「7.0」の新機能を、公開されている議論を基に解説。前編として「パフォーマンス向上」と「コード記述の単純化」に関連する6つの新機能を説明する。