次期C#言語概説
C# 6.0で知っておくべき13の新機能
― オープンな議論からの最新情報 ―
Visual Studio 2015 CTP 5で検証可能になっているC#言語の次期バージョン「6.0」の新機能を、公開されている議論を基に解説する。
「C# 6.0」と今のところ呼ばれているC#の次期バージョンは、Visual Studio 2015 CTP 5*1で検証可能になっている。
次期バージョンでは、“Roslyn”(コード名)と呼ばれる新しいコンパイラーの導入が決定しており、静的解析APIの提供など、コンパイラーまわりに大きな変更が行われている。一方、言語機能に目を向けると、async/awaitという大きな機能が追加されたC# 5.0に比べると、一つ一つの新機能自体は小さい。しかし、それらはプログラムをより書きやすくするための機能なので、C#開発者にとってはやはり重要なアップデートとなるだろう。
そして、これらの新機能に関する議論は、昨年4月より公開された場所(今年1月まではCodePlex、それ以降はGitHub)で行われている。この議論は、次期バージョンのみならず、すでにその次の「C# 7.0」と呼ばれるバージョンの言語機能についても行われている(※現在は、週次のDesign Notesの結果がGitHubのIssueに投稿されており、今後さらにオープンにしていくことが検討されている)。
本稿では、そうやって公開されている議論を基に、C# 6.0の新しい言語機能を解説する。
- *1 執筆時点では、Visual Studio 2015 CTP 5が、公開された最新のC# 6.0言語の文法で開発できる環境である。文法の詳細については、このバージョンに合わせて書いており、サンプルコードの動作確認もこのバージョンで検証している。正式版で仕様が変更される可能性もあるので、ご了承いただきたい。
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(_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);
}
|
使い方は、それぞれ下の行に書いている同等の意味を持つ「string.Fomatを使った記述方法」と比べてほしい。文字列リテラルの引用符(")の前に$を付けて記述する。逐語的文字列リテラルと併用することも可能で、その場合は@の前に$をつける。string.Formatでは{0}と可変長引数のインデックスを指定したところを、直接NameやPriceといったプロパティ名(およびフィールド変数名やローカル変数名など)を指定して記述する。右揃えや通貨形式といった書式指定をすることもできる。また、文字列としての{や}を記述したい場合は、{{や}}のように2つ重ねることでエスケープ指定する。
ちなみに、書式指定を行う場合に「InvariantなCulture」(=特定の国や地域に依存しないカルチャ)を指定できるよう、以下のサンプルコードのように書式指定した文字列をIFormattable型として扱って処理する方法も提案されているが、Visual Studio 2015 CTP 5時点では導入されていない。
|
public static string INV(IFormattable formattable)
{
return formattable.ToString(null,
System.Globalization.CultureInfo.InvariantCulture);
}
public void DumpInvariant()
{
var s = INV("{Name,20}: {Price:C} {{-}}");
}
|
6nameof operators(nameof演算子)
nameof演算子は式を与えて、その式の名前を文字列で返す演算子である。単純には、変数やプロパティ、メソッドなどの式をnameof演算子に与えると、それらの名前を文字列として取得できる。
実際に指定できるものは式になるが、一見、式のように見えて式ではないためnameof演算子に使えないものもある。具体的には下記のサンプルコードを見てほしい。
|
public int Prop { get; set; }
void F() { }
void F(int x) { }
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>));
// 以前はGenericsを指定する必要はなかったが、CTP 5ではエラー
Console.WriteLine(nameof(Tuple<,,,,,,,>.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) }
};
// インデックス初期化だけをC# 5の記法で書くことは今まで通り可能。
// この記法は対象クラスが IEnumerable<T> を実装していることが必要
var dict3 = new Dictionary<int, Customer>
{
{ 1, new Customer(1) },
{ 2, new Customer(2) }
};
}
|
8Exception filters(例外フィルター)
VB(Visual Basic)やF#では同様の機能が実装されているが、C#にもException filtersが実装された。例外処理のcatch節にif節を記述すると。if節の条件がtrueの場合のみ、そのcatchブロック内が実行される。Exception filtersはStackTraceを変更しないので、例外をキャッチ&リスローするより都合のいいことがある。
|
public static void Execute()
{
try
{
DoSomeHttpRequest();
}
catch (WebException e) if (e.Status == WebExceptionStatus.NameResolutionFailure)
{
Console.WriteLine("名前解決できませんでした");
}
catch (WebException e) if (e.Status == WebExceptionStatus.RequestCanceled)
{
Console.WriteLine("リクエストがキャンセルされました");
}
catch
{
Console.WriteLine("その他のエラー");
}
}
|
if節には条件しか書けないが、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) if (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(パラメーターを持たない構造体コンストラクター)
パラメーターを持たないコンストラクターを構造体に定義できるようになった。
|
struct Person10
{
public string Name { get; }
public int Age { get; }
public Person10(string name, int age) { Name = name; Age = age; }
public Person10() : this("Taro Tanaka", 10) { }
public override string ToString()
{
return $"{nameof(Person10)}: {nameof(Name)}={Name}, {nameof(Age)}={Age}";
}
}
|
注意事項としては、あくまで構造体をnew T(※「T」は構造体名)したときのみ、パラメーターなしで定義したコンストラクターが実行される。default(T)やnew T[]と記述した場合、定義したコンストラクターは呼ばれない。下記のサンプルコードを実行してみると違いが分かるだろう。
|
Console.WriteLine(new Person10());
// 以下は定義したコンストラクターは呼ばれない
Console.WriteLine(default(Person10));
Console.WriteLine(new Person10[1].First());
|
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(オーバーロード解決の向上)
「定義されたオーバーロードメソッドのうち、どのメソッドを実行するか決定するオーバーロード解決を向上した」と記述されている。しかし、どのように向上して、挙動がどう変わったかについての詳細な言及は今のところ公式には確認できていない。
13#pragma Warning Disable(#pragmaによるユーザー定義コンパイラー警告の抑止)
“Roslyn”と呼ばれる新しいコンパイラープラットフォームにより、誰でもコンパイラー警告を増やせるようになった。もともと、「#pragma(プラグマ)」と呼ばれる機能でコンパイラー警告を抑止する機能があったが、この機能をユーザーが定義したコンパイラー警告に対しても利用できるようにした。
|
#pragma warning disable "MyCustomDiagnostics"
#pragma warning restore
|
■
以上、13の項目が、現在のところC# 6.0で追加される予定の新機能である。String Interpolationなど、「提案されている文法・機能」と「CTP 5での実装」がまだ一致していない機能もあるため、正式リリースまでに変更が行われる可能性もある。
また、“Roslyn”が昨年4月にリリースされた時点で予定されていた機能と比べると、少なく・小ぶりになっているのに気付かれた方もいるかもしれない。これは、次期バージョンのC#および.NETの最大の目標が“Roslyn”という新しいコンパイラープラットフォームを導入することにあり、安定した“Roslyn”のリリースを優先しているためでもある。そして、コンパイラーやSDKに新機能を載せるだけでなく、「Visual Studio」というIDEでの新機能の支援まで対応してからリリースしている。これは、「C#」という言語の特徴だといえるだろう。