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

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

昨日、リアクティブエクステンションが流行というのと、
私なりに問題があるんじゃないかと思ってそれについて少し書きましたが、
実装の段階で自分の間違いに気づきました。
くれぐれも、昨日の記事を参考にして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 型を継承しないのと同じことです。



……さて、コードの修正に入ります。
PR
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を使うことができませんが…。
多くのプログラマを悩ませるnullに関する問題。
.NETだと、値型にはnull許容型が導入されたものの、参照型にはnull非許容型が導入されませんでした。
今から変更すると互換性等の問題が大きそうなので仕方ないでしょうが…。

……というわけで、疑似的にnull非許容の参照型を作ってみました。

public struct Safe<T> where T : class
{
    readonly T value;
    private Safe(T value)
    {
        this.value = value;
    }
    public static implicit operator Safe<T>(T value)
    {
        if (value == null) throw new ArgumentNullException("value");
        return new Safe<T>(value);
    }
    public static implicit operator T(Safe<T> safe)
    {
        if (safe.value == null) throw new InvalidOperationException("Safe<T>構造体に値が格納されていません。");
        return safe.value;
    }
}


いや、単純に構造体でラップしただけなのですが…。
元の型との間に暗黙の変換があり、変換時にnullをはじき出す、という単純な実装です。
メソッドの引数にstringじゃなくてSafe<string>を指定すれば、構造体がnullを許容しないから
nullを渡そうとした場合にIDEに指摘されるはず!!

……と思ったら指摘されませんでした。

ちゃんと暗黙の変換が働いたようです。スバラシイネー(白目)
というわけで、どちらにせよ実行時に例外が起きることでしか発見できない残念な仕様に。
しかもこれ、裏技を使うことでnullを例外を起こさずに代入できてしまうんです。

var safeStr = default(Safe<string>);

たったこれだけのコードで(このために、値を取り出す変換時もnullチェックが入ってます)。
なんだよ、意味ないじゃん…。

ただ、このSafe構造体が全くの無意味かというとそうでもないんですよね。
何よりも、引数の型として使うことでnull非許容であることを使用者にアピールできます。
また、(前述の裏技を除いて)引数として渡された時点でnullでないことが確定しているため、
ArgumentNullExceptionを毎回書く必要がなくなります。

で、ふいに思ったんですが、
この構造体にちょっとCLIのサポートがあれば完全にnull非許容な参照型が表せるんじゃないでしょうか?
具体的には、nullからSafe<T>への暗黙の変換を禁止したり、default(Safe<T>)を禁止したり…。
あ、ジェネリックを考えるとdefaultの禁止は難しいかな……。

そりゃ言語開発チームが難しいって言ってるんだから私が解決できるわけないですよね。

ただ、引数のnull禁止に関しては属性を使えばなんとかなると思うんです。
毎回書くのは面倒でしょうから、糖衣構文なんかを導入して。

void MyMethod(string! str) { ... }

こんな感じで型名の後に「!」をつけてnull非許容を表すことぐらいならできるんじゃないでしょうか。
どうにかならないですか、言語開発チームさん!!
ネットサーフィンしていると、Java使い向けのC#講座はあっても、逆は極端に少ないことが分かります。
C#のほうが後発なのだから当然と言えば当然なのですが、C#をメインに扱う私としてはもう少し時代の逆行にも対応してほしいなと思うところが…。

というわけで、一部の方々が熱いC#とJavaの比較でもしてみようかな、と思います。
※以下の文章は全て私の主観です。ご注意ください。

●オブジェクト依存指向(Java) vs 肥大化する言語仕様(C#)
お互いがお互いを否定する理由じゃないかと一番思うところがこれです。
批判的な言い方をしましたが、ようするにJavaが純粋なオブジェクト指向を基盤に置いているのに対し、C#はオブジェクト指向も道具の一部としてマルチパラダイムになっているという大きな差があるわけです。
ネットで見かけるC# / Java 批判はこれを理解してないんじゃないかと思うことさえあります。
これはJava使いから見れば「C#はごちゃごちゃしたいらない機能を追加しすぎ」であり、
C#使いから見れば「Javaはいつまでもオブジェクト指向に固執しすぎ」じゃないかと。


●継承とインターフェイスに対する価値観の差
Javaは純粋なオブジェクト指向ですから、継承とインターフェイスを湯水のごとく使います。
むしろ、これらが使いこなせないと真価を発揮できません。
そのためJavaには匿名クラスなどの構文があり、メソッドは既定でオーバーライド可能です。

それに対し、C#にとって継承は「非常に強力な機能」であり、小規模な用途ではあまり使われません。
"振る舞い"を渡したり、差し替えたりといった用途にはデリゲートを使えばいいんです。
そのためC#には継承関連の糖衣構文がなく、メソッドは既定でオーバーライド不可能です。


●どちらのほうが簡単か
これについては本当に個人差だと思います。
単純に「言語の仕様を覚えたい」というなら間違いなくJavaのほうが簡単です。
言語仕様の大きさはJavaのほうが圧倒的に小さく単純だからです。
また、初級者的なプログラムを作りたいならどちらを使っても大差ありません。
標準ライブラリのクラス名・メソッド名を除いて全く同じコードになることなることすらあります。
中規模~大規模なプログラムを作る場合でも、難易度としては大差ないと思います。
何か新しいことをしようとした場合、C#なら言語の機能を覚える必要がありますし、
Javaなら(広い意味での)デザインパターンを覚える必要があります。

両方を同程度に使いこなした場合、コードはC#のほうが短くなります。
ただし、初心者が上級者のコードを読むために必要な労力はJavaのほうが少なくて済みます。
これは言語仕様の大きさを比べれば当然のことです。


=======================================================
私自身はC#使いですが、どちらかを贔屓しないように書いたつもりです。
この文章に対する批判や意見は大いに受け付けます(ないと思いますが)。
忍者ブログ [PR]