はじめに
ディレクトリのような階層構造をC#で簡潔に表現できないかと構文を最大限悪用して試行錯誤した結果です。
作例
var result = await "C:\\aaa" with { _ = new _ { await "bbb" with { _ = new _ { "ddd" } }, await "ccc" with { _ = new _ { "eee", "fff" } } } }; Show(result);
上記のような何となく木構造にも見えなくもないコードを書くと以下のような結果を得られます。
C:\aaa C:\aaa\bbb C:\aaa\bbb\ddd C:\aaa\ccc C:\aaa\ccc\eee C:\aaa\ccc\fff
それにしてもstringをawaitしたりwith式を呼び出し始めたりして意味わかんないですよね。
ここからはどうやってこのコードを合法としているのかを解説します。
await出来るstring
知っている人にとっては常識かもしれませんが、async/await構文はTaskやTask<T>専用ではありません。
一定の条件を満たすクラスや構造体であれば何でもawait出来ます。
- インスタンスメソッドや拡張メソッドで
GetAwaiterを実装している GetAwaiterから返される型が以下の要件を満たしてるpublic bool IsCompleted { get; }を実装しているpublic ??? GetResult()を実装しているINotifyCompletion経由でpublic void OnCompleted(Action continuation)を実装している
というわけで、まずは拡張メソッド経由でstringにGetAwaiterを生やします。
internal static class StringExtension { internal static StringPathAwaiter GetAwaiter(this string @this) { return new StringPathAwaiter(@this); } }
そうしたらStringPathAwaiterを上記の条件を満たすように実装します。
internal class StringPathAwaiter : INotifyCompletion { private readonly string _path; internal StringPathAwaiter(string path) { _path = path; } public bool IsCompleted { get { return false; } } public void OnCompleted(Action continuation) { continuation(); } public Path GetResult() { return new Path(_path); } }
こうすることでstringをawaitすることで最終的にStringPathAwaiter経由でPathクラスを生成しています。
謎のコレクションクラス_
それぞれのPathには複数の子Pathを持たせたいので、先にコレクションクラスをでっちあげます。
C#ではIEnumerableを実装して1つの引数を持つメソッドAddが実装されていればコレクション初期化子を使用することができます。
この辺を悪用して極限まで目立たないクラス名のコレクションクラスをでっちあげます。
internal class _ : IEnumerable<Path> { private List<Path> _backing = new List<Path>(); public void Add(Path path) { _backing.Add(path); } public IEnumerator<Path> GetEnumerator() { return _backing.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public static implicit operator List<Path>(_ x) { return x._backing; } }
Pathレコードの実装
C#ではレコード型として実装すればwith式で指定されたプロパティを変更できます。*1
また、withの優先順位はawaitよりも低いので、見た目が悪くなるかっこが不要になりよりスタイリッシュさを演出できます。
子Pathを格納するプロパティ名を_にして、コード上で最大限目立たなくします。
また、stringからの暗黙の型変換を定義することで、型推論が使える場面ではawaitを経由しないで直接変換出来るようにします。
internal record Path { public string Name { get; } internal Path(string value) { Name = value; } public override string ToString() { return Name; } public static Path operator /(Path x, string y) { return new Path(IO::Path.Combine(x.Name, y)); } public IEnumerable<Path> _ { set; get; } = Enumerable.Empty<Path>(); public static implicit operator Path(string path) { return new Path(path); } }
まとめ
上記のようなコードを書けば、まともに生きていればお目にかかることのないC#を書くことができます。
プロダクションコードに投入すればいろんな意味で楽しいと思うで、ぜひとも真似してみてください。