xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

ラムダ形式メンバーの拡張

2016/09/24 (土) の朝、Expression-Bodied Everything (= どこでもラムダっぽく) に関する Pull-Request が Roslyn の master ブランチにマージされました!

C# 6 ではプロパティとメソッドの 2 箇所についてラムダ形式のメンバーを記述することができましたが、それをさらに拡張していろんなところで使えるようにするというものです。

C# 6 の頃から「ラムダ形式」と書いているのでその名残でタイトルを付けましたが、「式形式 / 式っぽい / 式の体をした」と言うのかもしれません。ここら辺は日本語訳が正式には決まってないと思うので結構テキトーです...。

f:id:xin9le:20160927231108p:plain

新たに使えるようになる場所

C# 7 から増えたもの (= 2016/09/27 時点で動作するもの) は以下の 4 つです。

  • コンストラクタ
  • デストラクタ
  • プロパティのアクセサ
  • インデクサ

例えば以下のように書けます。

class Program
{
    //--- プロパティのアクセサ
    //--- get だけとか set だけもできる
    public string Property
    {
        get => "Getter Property";
        set => Console.WriteLine(value);
    }

    //--- インデクサも同様に
    public string this[int index]
    {
        get => $"{index} : Getter Indexer";
        set => Console.WriteLine($"{index} : {value}");
    }

    //--- コンストラクタ / デストラクタもこの通り
    public Program(string text) => Console.WriteLine(text);
    ~Program() => Console.WriteLine("Destructor");

    static void Main()
    {
        var x = new Program("Constructor");
        x.Property = "Setter Property";
        Console.WriteLine(x.Property);
        x[123] = "Setter Indexer";
        Console.WriteLine(x[123]);
    }
}

/*
Constructor
Setter Property
Getter Property
123 : Setter Indexer
123 : Getter Indexer
Destructor
*/

逆コンパイル

先の例を逆コンパイルしてみると以下のようになります。お分かりの通り、これまでの書き方をより簡易にするための糖衣構文でしかありません。

class Program
{
    public string Property
    {
        get { return "Getter Property"; }
        set { Console.WriteLine(value); }
    }

    public string this[int index]
    {
        get { return string.Format("{0} : Getter Indexer", index); }
        set { Console.WriteLine(string.Format("{0} : {1}", index, value)); }
    }

    public Program(string text)
    {
        Console.WriteLine(text);
    }

    ~Program()
    {
        Console.WriteLine("Destructor");
    }

    private static void Main()
    {
        Program x = new Program("Constructor");
        x.Property = "Setter Property";
        Console.WriteLine(x.Property);
        x[123] = "Setter Indexer";
        Console.WriteLine(x[123]);
    }
}

[おまけ] イベントへの対応

プロパティの get/set ができるのなら、(滅多に使われない) イベントの add/remove もできるのでは?と思った方もいらっしゃるかもしれません。ただ、残念ながら執筆時点 (2016/09/27) では csc.exe がクラッシュして死にます。Roslyn の実装を見ると add/remove にも対応してそうな雰囲気を感じるところもあるのですが...。

private EventHandler click;
public event EventHandler Click
{
    //--- 残念ながらコンパイラがクラッシュする
    add => this.click += value;
    remove => this.click -= value;
}

イベントビューアで確認するとスタックトレースはこんな感じで吐かれていました。

アプリケーション:csc.exe
フレームワークのバージョン:v4.0.30319
説明: ハンドルされない例外のため、プロセスが中止されました。
例外情報:System.InvalidOperationException
   場所 Microsoft.Cci.ReferenceIndexer.ProcessMethodBody(Microsoft.Cci.IMethodDefinition)
   場所 Microsoft.Cci.MetadataVisitor.Visit(Microsoft.Cci.ITypeDefinitionMember)
   場所 Microsoft.Cci.MetadataVisitor.Visit(System.Collections.Generic.IEnumerable`1<Microsoft.Cci.IMethodDefinition>)
   場所 Microsoft.Cci.ReferenceIndexerBase.Visit(Microsoft.Cci.ITypeDefinition)
   場所 Microsoft.Cci.MetadataVisitor.Visit(System.Collections.Generic.IEnumerable`1<Microsoft.Cci.INamedTypeDefinition>)
   場所 Microsoft.Cci.ReferenceIndexer.Visit(Microsoft.CodeAnalysis.Emit.CommonPEModuleBuilder)
   場所 Microsoft.Cci.MetadataWriter.CreateIndices()
   場所 Microsoft.Cci.MetadataWriter.BuildMetadataAndIL(Microsoft.Cci.PdbWriter, System.Reflection.Metadata.BlobBuilder, System.Reflection.Metadata.BlobBuilder, System.Reflection.Metadata.BlobBuilder, System.Reflection.Metadata.Blob ByRef, System.Reflection.Metadata.Blob ByRef)
   場所 Microsoft.Cci.PeWriter.WritePeToStream(Microsoft.CodeAnalysis.Emit.EmitContext, Microsoft.CodeAnalysis.CommonMessageProvider, System.Func`1<System.IO.Stream>, System.Func`1<System.IO.Stream>, Microsoft.Cci.PdbWriter, System.String, Boolean, Boolean, System.Threading.CancellationToken)
   場所 Microsoft.CodeAnalysis.Compilation.SerializeToPeStream(Microsoft.CodeAnalysis.Emit.CommonPEModuleBuilder, EmitStreamProvider, EmitStreamProvider, System.Func`1<System.Object>, Microsoft.CodeAnalysis.DiagnosticBag, Boolean, System.Threading.CancellationToken)
   場所 Microsoft.CodeAnalysis.CommonCompiler.RunCore(System.IO.TextWriter, Microsoft.CodeAnalysis.ErrorLogger, System.Threading.CancellationToken)
   場所 Microsoft.CodeAnalysis.CommonCompiler.Run(System.IO.TextWriter, System.Threading.CancellationToken)
   場所 Microsoft.CodeAnalysis.CSharp.CommandLine.Csc+<>c__DisplayClass1_0.<Run>b__0(System.IO.TextWriter)
   場所 Microsoft.CodeAnalysis.CommandLine.ConsoleUtil.RunWithUtf8Output[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]](System.Func`2<System.IO.TextWriter,Int32>)
   場所 Microsoft.CodeAnalysis.CSharp.CommandLine.Csc.Run(System.String[], Microsoft.CodeAnalysis.CommandLine.BuildPaths, System.IO.TextWriter, Microsoft.CodeAnalysis.IAnalyzerAssemblyLoader)
   場所 Microsoft.CodeAnalysis.CommandLine.DesktopBuildClient.RunLocalCompilation(System.String[], Microsoft.CodeAnalysis.CommandLine.BuildPaths, System.IO.TextWriter)
   場所 Microsoft.CodeAnalysis.CommandLine.BuildClient.RunCompilation(System.Collections.Generic.IEnumerable`1<System.String>, Microsoft.CodeAnalysis.CommandLine.BuildPaths, System.IO.TextWriter)
   場所 Microsoft.CodeAnalysis.CommandLine.DesktopBuildClient.Run(System.Collections.Generic.IEnumerable`1<System.String>, System.Collections.Generic.IEnumerable`1<System.String>, Microsoft.CodeAnalysis.CommandLine.RequestLanguage, Microsoft.CodeAnalysis.CommandLine.CompileFunc, Microsoft.CodeAnalysis.IAnalyzerAssemblyLoader)
   場所 Microsoft.CodeAnalysis.CSharp.CommandLine.Program.Main(System.String[], System.String[])
   場所 Microsoft.CodeAnalysis.CSharp.CommandLine.Program.Main(System.String[])

Expression-Bodied なイベントへの対応も提案としては出ているので、今後 (どういった書き味になるかはさておき) 対応が入るかもしれませんね。

throw 式

2016/09/21 (水) の朝方、throw 式に関する Pull-Request が master にマージされました!これにより、これまでステートメント (文) としてのみ提供されていた throw を式として記述できるようになります。

f:id:xin9le:20160924225231p:plain

throw 式が使えるところ

パッと確認した範囲では、以下の箇所で使えました。

  • 条件演算子 (三項演算子)
  • null 結合演算子
  • ラムダ形式のメンバー

条件演算子 (三項演算子)

条件演算子で throw 式を使ってみると以下のような感じになります。

//--- C# 7 ではこう書ける
static int IntParse(string value)
    => int.TryParse(value, out var result)
    ?  result
    :  throw new ArgumentException(nameof(value));

//--- C# 6 までの書き方
static int IntParse(string value)
{
    int result;
    if (int.TryParse(value, out result))
        return result;

    throw new ArgumentException(nameof(value));
}

C# 7 のコードを逆コンパイルしてみると以下のようになります。C# 6 までのコードと同様の展開がされることが分かりますね。

//--- 逆コンパイル結果
private static int IntParse(string value)
{
    int result;
    if (!int.TryParse(value, out result))
    {
        throw new ArgumentException("value");
    }
    return result;
}

null 結合演算子

null 結合演算子に対しても同じように使うことができます。

//--- C# 7
static object Assert(object x)
    => x ?? throw new ArgumentNullException(nameof(x));

//--- 逆コンパイル結果
private static object Assert(object x)
{
    if (x == null)
    {
        throw new ArgumentNullException("x");
    }
    return x;
}

ラムダ形式のメンバー

C# 6 で導入された => によるプロパティやメソッドの簡易記法 (= ラムダ形式メンバー) でも使えます。例えば下記のように、未実装時の記述がシンプルで楽になりますね。

//--- C# 7
public string Name => throw new NotImplementedException();
public string SayHello() => throw new NotImplementedException();

//--- 逆コンパイル結果
public string Name{ get { throw new NotImplementedException(); }}
public string SayHello(){ throw new NotImplementedException(); }

[おまけ] throw null;

みなさん、nullthrow するとどうなるかご存知ですか?実は NullReferenceException が飛びます。

try
{
    //--- 以下のふたつは等価
    throw null;
    throw new NullReferenceException();
}
catch
{}

違う型の例外でも NullRefereneceException になります。

try
{
    NotSupportedException ex = null;
    string a = null;
    var b = a ?? throw ex;  //--- NotSupportedException は飛ばない
}
catch (Exception ex2)
{
    //--- System.NullReferenceException
    Console.WriteLine(ex2.GetType());
}

throw 式の結合優先度

岩永先生 (@ufcpp) が throw 式の結合優先度を調べて教えてくださいました。まとめてくださった Gist をそのまま拝借するとこんな感じらしいです。

最初どうしてこうなるのか全然わからなかった以下のような例も、結合優先度のおかげで理屈が分かって納得しました。

try
{
    //--- throw null されると思ってたのに、されないで ArgumentNullException が飛んだ
    NotSupportedException ex = null;
    string a = null;
    var b = a ?? throw ex ?? throw new ArgumentNullException();

    //--- けど ↑↑ は結合優先度の問題なので、実はこれと一緒
  //var b = a ?? throw (ex ?? throw new ArgumentNullException());
}
catch (Exception ex2)
{
    //--- System.ArgumentNullException
    Console.WriteLine(ex2.GetType());
}

上記を逆コンパイルすると以下のように展開されます。

try
{
    NotSupportedException ex = null;
    string a = null;
    if (a == null)
    {
        NotSupportedException expr_0A = ex;
        if (expr_0A == null)
        {
            throw new ArgumentNullException();
        }
        throw expr_0A;
    }
    var b = a;
}
catch (Exception ex2)
{
    Console.WriteLine(ex2.GetType());
}

throw 式の型

例えば条件演算子の場合、コンパイラはその戻り値の型を決定しなければなりません。これまでは以下のようになっていました。

//--- こんな継承関係があるとする
class Base {}
class A : Base {}
class B : Base {}

//--- A は Base になり得るので x は Base 型
var x = true ? new A() : new Base();

//--- コンパイルエラー : 継承元を探してくれたりはしない
var x = true ? new A() : new B();

throw 式は条件演算子に書くことができると先に書きました。この場合 throw 式の型はどうなるのかというと、答えは「どんな型にでもなれる」です。こんな型を「bottom 型 (= すべての型から派生している型)」と言います。.NET Framework において双対するのが object 型で、これを「top 型 (= すべての型の基底となる型)」と言うそうです。

//--- x の型は A と推論される
var x = true ? new A() : throw new Exception();

//--- x の型は B と推論される
var x = true ? throw new Exception() : new B();

つまり条件演算子は throw 式 ではない方の型で推論される動きになるのですが、両方を throw 式にしたらどうなるか。これは (当然) コンパイルエラーになります。

//--- 型推論できないのでエラー
//--- Type of conditional expression cannot be determined because there is no implicit conversion between '<throw>' and '<throw>'
var x = true ? throw new NotSupportedException() : new ArgumentNullException();

Roslyn の開発進捗が更新されました - 2016/09/21

2016/09/21 (水) の朝方、とある Pull-Request のマージにより Roslyn の開発状況/進捗 (Language Feature Status) が更新されました。でも実際に変更を加えたのは 2016/08/30 (火) っぽいので、約 1 か月前から決まっていた内容です。

Language Feature Status

以下はその引用です。

C# 7 / Visual Basic 15 に搭載予定

Feature Branch State
Binary Literals master Finishing
Digit Separators master Finishing
Local Functions master Finishing
Type switch master Finishing
Ref Returns master Finishing
Tuples master Finishing
Out var master Finishing
ValueTask master Finishing
Throw Expr features/throwexpr Prototyping
Expression-Bodied Everything features/exprbody Prototyping

C# 8 / Visual Basic 16 以降で搭載予定

Feature Branch State
Address of Static none Feature Specification
Async Main none Feature Specification
Source Generation master Prototyping
private protected features/privateProtected Prototyping
Non-null Ref Types features/NullableReferenceTypes Prototyping
Bestest Betterness none Feature Specification
Records features/records Feature Specification
With Exprs features/records Feature Specification
Pattern Matching features/patterns Prototyping

C# 7 で搭載見込みの機能は実装完了しているものすべてを記事にしていますので、ご興味があればご覧ください。変更点なども順次追記/修正しています。

変更点

diff から見る前回との差分はザッと以下のような感じです。

機能 概要 変更点
Throw Expr 例外を式でも投げられるように C# 7 での搭載に再昇格
Expression-Bodied Everything コンストラクタなども => で書けるように 新規に C# 7 で搭載する方向
Address of Static 搭載見送り
Bestest Betterness より優れたオーバーロード解決 Better Betterness から名称変更

開発も終盤に差し掛かっているのかなぁと思っていたりしたのですが、一度は見送られた throw 式が再度搭載する方向になるなどの方針変更が入っています。まだまだアクティブな開発が続いている感じで目が離せませんね!最近は Visual Studio 15 Preview 5 のリリース準備も着々と進んでいるように見受けられますし、そこまでにどれだけ開発が進むのか楽しみです。

ちなみにここに書いてあることは記事執筆時点 (2016/09/24) での状況であって最終的な確定事項ではないのでご注意ください。C# 7 の fix はいつになるのか...w

Spy++ が見つからないときの対処方法

Windows のデスクトップアプリ (今だとクラシックデスクトップアプリって言うんでしたっけ?) 開発者にとっては必需品と言っても過言ではないレベルの神ツールである Spy++。ウィンドウハンドルやウィンドウメッセージを拾う楽しさを知ってしまうと、別プロセスに対して悪さ (?) するのもお手の物ですよね!

f:id:xin9le:20160914020702p:plain

以前は Visual Studio をインストールすれば勝手に付いてきたのですが、Visual Studio 2015 になってからは標準インストールしても入っていないことに気付きました。そんなときの対処メモ。

対処方法

とっても簡単で、Visual Studio 2015 のセットアップで以下の機能をインストールすれば OK!もちろん次の Visual Studio でどうなるかはわかりませんが...

  • [Programming Languages] - [Visual C++] - [Common Tools for Visual C++ 2015]

f:id:xin9le:20160914021421p:plain

セットアップが完了すれば、以下のフォルダに配置されているはずです。

  • C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\spyxx.exe

Roslyn の開発進捗が更新されました - 2016/08/04

2016/08/04 (木) の朝方、Gafter 先生の Pull-Request が master にマージされ、それにより Roslyn の開発状況/進捗 (Language Feature Status) が更新されました。

以下はその引用です。

C# 7 / Visual Basic 15 に搭載予定

Feature Branch State
Address of Static none Feature Specification
Binary Literals master Finishing
Digit Separators master Finishing
Local Functions master Finishing
Type switch master Finishing
Ref Returns master Finishing
Tuples master Finishing
Out var master Finishing
ValueTask master Finishing

C# 8 / Visual Basic 16 以降で搭載予定

Feature Branch State
Async Main none Feature Specification
Source Generation master Prototyping
Throw Expr features/patterns Prototyping
private protected features/privateProtected Prototyping
Non-null Ref Types features/NullableReferenceTypes Prototyping
Better Betterness none Feature Specification
Records features/records Feature Specification
With Exprs features/records Feature Specification
Pattern Matching features/patterns Prototyping

C# 7 で搭載見込みの機能は実装完了しているものすべてを記事にしていますので、ご興味があればご覧ください。

変更点

diff から見るに、これまでとの差分はザッと以下のような感じです。

機能 概要 変更点
Out var out 引数に渡す変数を宣言と同時に生成 プロトタイプ完了
ValueTask 非同期メソッドの戻り値を任意型に プロトタイプ完了
Async Main Main 関数で async/await を許可 搭載見送り
Source Generation コード生成による処理の差し込み 搭載見送り
Throw Expr 例外を式でも投げられるように 搭載見送り

個人的にかなり残念なのは Source Generation の搭載が見送り項目になったことです。実はこの機能、6 月中は master ブランチに入っていたのですが 2016/07/01 のコミットfeatures/source-generators に逃がされました。

数日かけて実際に動きも (ほぼほぼ全部) 調べてブログ記事を書き終えたのが 2016/06/30 の深夜。社内@neuecc 先生に「こんな機能が来そうじゃモン!」と夜な夜な話して「いざ公開じゃー!」と思ったところで機能削除されて意気消沈していました...(´;ω;`)

ちなみにここに書いてあることは記事執筆時点 (2016/08/05) での状況であって最終的な確定事項ではないのでご注意ください。とは言え最近は Visual Studio 15 Preview 4 のリリース準備も着々と進んでいるようですし、C# 7 の fix がだいぶ近づいているのかなぁという気がしないでもないです。