忍者ブログ
おそらく活動状況とか、他愛もないこととか書きます。 μ崎みのりの気ままで気まぐれなブログです。 作曲とか、ゲーム製作なんかしてます。
[1]  [2]  [3]  [4]  [5]  [6
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

公式の仕様書に載ってる面白い記述について。

C#5.0で追加されたasync / await はコンテキストキーワードなので、
特定の部分以外では普通に識別子として使えます。
で、このうちasyncのほうはメソッド宣言の直前でのみキーワードになるため、
タイトルのようなメソッドを宣言してもエラーにならないというお話。

using async = System.Threading.Tasks.Task;
using await = System.Threading.Tasks.Task;

class Async
{
     async async async(async async) => await async;
     async await await(await await) => await @await;
}


不思議なことに(?)このコードは何のエラーもなくコンパイルできます。
色分けで大体どう解釈されてるか分かりますよね。
5つのasyncは左からキーワード、型、メソッド名、型、引数名と解釈されています。
残念ながらawaitを識別子に使う場合はメソッド内で引数を参照する際に@awaitと書く必要がありますが、それでも同じ単語が4種類の意味で使われているのは圧巻(?)ですね。

ちなみにasync / await の他だと、var・yieldや全てのクエリキーワード等がコンテキストキーワードなので、
yield return yield;
とか、
var from = from var in yield select var;
といった(無意味に読みにくい)記述が可能です。

ただし、これらの単語がコンテキストキーワードなのは飽くまでも互換性のためなので、
新しくコードを書く場合は識別子に使わないのが普通でしょうね。
PR
私は結構若い頃(小学生時代)からC#に触れていて、
言語がバージョンアップする度にその情報を追いかけて見ています。
そのため、恐らく普通の人よりはC#の言語仕様に詳しいと自負しています。

……自負しているはずなんですが。
ちょっとした省略の仕様とかで最近知らなかったものを見つけたのでメモ。

①配列初期化子の簡略化
一番古いタイプの省略記法を知りませんでした。
C#3.0で追加された型推論タイプは知ってたのに……。

// 普通の配列初期化子
int[] array1 = new int[] { 0, 1, 2, 3, 4 };
// 暗黙に型指定された配列初期化子(C#3.0以降)
int[] array2 = new[] { 0, 1, 2, 3, 4 };
// 知らなかったシンタックス。配列型変数の宣言と同時に初期化する場合のみ有効。
int[] array3 = { 0, 1, 2, 3, 4 };


②オブジェクト / コレクション初期化子の簡略化
ちょっとしたことなんですが、コンストラクタに引数がなければ括弧を省略できるんですね…。

var list = new List<int> { 0, 1, 2, 3, 4 };
var point = new Point { X = 10, Y = 20 };


コンストラクタなのに括弧がないと少し気持ち悪く感じます。
ただ、オブジェクト初期化子を使って初期化する時ってコンストラクタに引数がない場合が
多いので、括弧を省略することで「オブジェクト初期化子使ってますよ!」のアピールにはなるんじゃないかなーとは思います。
使えるものは使うスタンスなので、気付いたからには積極的に省略しますよ。
個人的に、C#は冗長に書くより短く書いた方が吉な言語だと思ってますので。
昨日、リアクティブエクステンションが流行というのと、
私なりに問題があるんじゃないかと思ってそれについて少し書きましたが、
実装の段階で自分の間違いに気づきました。
くれぐれも、昨日の記事を参考にしてIObserver<T>の類似インターフェイスを作らないで下さい。
それは一時の気の迷いです。
以下、反省点と未来の自分へのメモ。

【Awaitable と Observable の違い】
●Awaitable (待機できる)
.NETがasync / await を使用するための条件としている実装パターン。
何らかのプロセスの完了を待って、結果(戻り値)を取り出すためにあります(※戻り値が無い場合もありますが、完了を待って「無」を取り出すと考えます)。
.NET でインターフェイスにしなかったのは恐らくTaskより後発だからで、パターンが先にあれば
IAwaitable<T> / IAwaiter<T> インターフェイスみたいなのが用意されていたと思います。

●Observable (観察できる)
Rxの根幹となるパターン。
時間と共に変化する状態に対して、リアルタイムに最新情報を共有するイメージ。
.NET ではインターフェイス IObservable<T> / IObserver<T> が用意されています。
このインターフェイスは GoF の Observer パターンよりちょっと高機能。

●で、どこが違うのか
Awaitable も Observable も時間に関わるパターンなわけですが、
Awaitable が単一の値を通知するのに対して Observable は複数の値を通知する点が違います。
ここが最も重要な違いです。

【Observable と Enumerable の関連性】
「複数の値」といえば .NET のIEnumerable<T> / IEnumerator<T> が思い浮かびます。
GoF の Iterator パターンです。
この2つの違いは、「複数の値」を空間上に並べるか時間上に並べるかの差です。
「時間か空間か」という点を除いて、この2つは完全に一致します。
というか、同じように扱うために「一致するように実装します」。

【なら、Awaitable が対応するものは?】
ここまで考えて、↓の表にたどり着きました。

時間空間
単一の値Awaitable(値)
複数の値ObservableEnumerable

Observable と Enumerableという「複数同士」が対応するなら、
Awaitableに対応する「ひとつ」って何かと考えると、それは単なる変数などの値です。
ここまで分かれば結論まであと少し。

【IObserver<T>のOnCompletedメソッドに引数がない理由】
配列などの列挙可能なものは「0個」である場合があります。
同じように、時間軸に並んだ観察可能なものも、通知すべき値がない、つまり「0個」である場合を表現できるべきです。
この「0個」の表現のためにも、OnCompletedメソッドに引数があってはなりません。
引数がある場合、最低でも1個の値が必要になってしまいます。

【Observable ≠ Awaitable】
昨日の私が勘違いしていた点。
それぞれのインターフェイスを定義する場合でも、IObservable<T> と IAwaitable<T>、
IObserver<T> と IAwaiter<T> に継承関係があってはいけません(私は継承関係を持たせてしまい、実装していくうちに詰まりました)。
これは、int[ ] 型が int 型を継承しないのと同じことです。



……さて、コードの修正に入ります。
Unityを先日使い始めた私ですが、どうやらUniRxってのを使うと便利になるようでして。
調べていくとLINQのひとつらしいことが分かりました。

……いいですね、LINQ。

Rxがクエリする対象は時間軸に乗ってやってくるメッセージ。
値がずらーっと空間上に並んでるイメージのコレクション(IEnumerable<T>)と同じように、
値が時間軸上に並んでいるイメージ(IObservable<T>)でしょうか。

で、詳しい説明は省くとして、Rxにひどく感銘を受けたので似たようなものを自作ゲームフレームワークに実装することにしました。
ちなみに既存のインターフェイスは使わず、ゲームフレームワークとうまくマッチするような実装にしています。


現状で私が感じるRxの問題点は、「既に終わった処理から値を取り出せないこと」です。
(System.IObserver<T>に定義されているメソッドではどうやっても実現不可能)
Rxにとっては完了後のことはあまり重要じゃないと思うので、これはこれでいいんでしょうけど。
私が使いたい機能とはマッチしないんです←ここ重要。

それで似たようなものを実装することにしたわけですね。
既存のRxとの違いは、OnError(Exception) と OnCompleted() がひとつのメソッド
OnEnded(Result<T, Exception>)に統合されてることです。
この引数には正常終了した場合は戻り値、エラーの場合は例外が渡されるしくみで、
既存のRxでできなかった「既に終わった処理から値を取り出す」ことが可能です。
これが await 演算子と相性がいいのですが、本物に勝てるとは思っていません。

ただ拡張メソッドが大量すぎて実装が大変なので、少し時間はかかりそうです。
Visual Studio 2015 RC版がいつの間にか日本語化できるようになっていたので、
さっそく導入してみました。
今さらですがProfessional相当の機能を個人開発者等に無償提供って大丈夫なのか、Microsoft。
(と言っていますが、Visual C# Express 2010 からの乗り換えです。実に5年越し)

理由としてはさすがにasync / awaitが使いたくなったのと、
C#6.0で便利な糖衣構文が色々追加されたから。

まず起動して…ソリューションエクスプローラでクラスやメソッド定義が見えるようになっていることに感動(もしかしたら以前のバージョンで既に追加されていたのかもしれませんが)。

詳細は公式に任せるとして(「++C++; // 未確認飛行 C」さんのこのページのほうが分かりやすいです)、新しい糖衣構文使ってみた印象をレビューします。

【式形式のメソッド / プロパティ / インデクサ】
コーディングに一番影響しそうなのがこれです。
プロパティやメソッドの実装が1ステートメントだけな場合、ラムダ式とほぼ同じ記法で書けるようになりました。
特に読み取り専用プロパティ / インデクサでは get ブロックそのものを省けるため、効果は絶大。
ただメソッドに対しては乱用に注意しないといけないなーと思いました。
理由は return を省けるため、パッと見で戻り値があるのかないのか一瞬迷うこと。
ようするに、beforeではreturnの有無でパッと分かっていたのが、afterではそれが省略されたせいで戻り値の型を見ないと戻り値の有無が分かりません。

before:
int MyProperty { get { return x; } }
int MyMethod1() { return OtherMethod(); }
void MyMethod2() { OtherMethod(); }


after:
int MyProperty => x;
int MyMethod1() => OtherMethod();
void MyMethod2() => OtherMethod();


…というわけで私は、戻り値がない場合にはこの構文は使わないことに決めました(たしか公式も「副作用のない式」の場合に使うことを想定していたはず)。
文字数も空白が1文字分少なくなるだけですしね。

μ崎スタイル(voidの時は式形式を使わない):
int MyProperty => x;
int MyMethod1() => OtherMethod();
void MyMethod2() { OtherMethod(); }


【自動実装プロパティの強化 (getのみの自動実装プロパティ / プロパティ初期化子)】
getのみのほうは私が待ちに待っていた機能です。
C#が関数指向に近づくにつれ、書く頻度が高まっていたであろう不変オブジェクトがこれで書きやすくなります。
対してプロパティ初期化子は、プライマリーコンストラクタがC#6.0 から省かれたこともあって使用頻度はやや低め。
不要なコンストラクタを書かなくて良くなりますね。

【nameof演算子】
Visual Studioがあってこその機能ですね。
頻出するArgumentNullExceptionで主にお世話になります。
WPF(本質的にはMVVM)だとINotifyPropertyChangedの実装には[CallerMemberName]属性が使えるので今さら感がありますが、依存関係プロパティも恩恵を受けるのでなかなか効果的。

【null条件演算子 (?. 演算子 ・ ?[ ] 演算子)】
これも多くの開発者に望まれていたであろう機能。
null合体演算子 ?? と並んで、うまくnullと付き合っていくためにとても都合がいいものです。
私もイベントの呼び出し等で早速使いました。

【文字列挿入】
string.Formatメソッドに対応する糖衣構文。
string.Formatって打つのが面倒で文字列を + 演算子で結合していた場面でも効果を発揮しています。
無駄なバグも減るいい機能ですが、作成するプログラムによっては全く使わなかったり…。

【using static】
まだ使ったことはありません。
拡張メソッドが省略できる対象に含まれないあたりは良いと思います。
System.Linq.EnumerableのRange・Repeatあたりが使いやすくなりそう。

【オブジェクト初期化子の強化 (インデックス初期化子 / Add拡張メソッドへの対応)】
主にDictionaryで効果を発揮しますね。
同じことができる2引数のAddメソッドと比べると、波カッコが減って見やすくなります。
拡張メソッドのほうは、Stackとかで初期化子を使いたい時に便利そう。

【例外フィルター】
これもまだ使ったことはありません。高度な例外処理では必要になるんでしょうか。
個人的には、キーワードがwhenなのがちょっと疑問。F#との兼ね合いですかねぇ。
英語として多少おかしくても、意味が分かりやすいifにしてほしかったなーと。

【catch・finallyブロック内でawait】
そもそもこのバージョンからasync / awaitを使い始めた私にはレビューしようがありません…。

【おまけ・async / await】
そもそもawait演算子はAwaitableパターンを実装していれば(一応)どんなクラスにも対応可能です。
これを応用して、ゲームプログラミングでフレームをまたぐ処理を書きやすくすることができます。
UnityだとIEnumerableを用途外に使用して強引にコルーチンを実装していましたが、awaitのパターンを使えばもうちょっとそれっぽく書けるようになります。
もっとも、Unitiyではasync / awaitを使うことができませんが…。
忍者ブログ [PR]