「C#縛りでLT大会」に参加しました

クロスワープの大鷲です。

去る 9月13日、Forkwell さん主催のイベント「C#縛りでLT大会 & Meetup」で喋ってきました。

forkwell.connpass.com

当日の資料はこちらです。
docs.com

ここでは当日話せなかったことについて補足します。

LT のテーマが「C#」だったので、ASP.NET 等のプラットフォームについてではなく、C# という言語自体について話したいと思っていました。
最近であれば C# 7 などが熱いテーマではありますが、当日は岩永さんも登壇されるということで、「これはうかつなことは言えない…」と思い、肩の力を抜いて聞ける内容にすることにしました。

結果的に、当日の LT の中で最も実用性に乏しいセッションになってしまいました。*1

言語機能 → キーワード

テーマは「C# で最も使われていない言語機能」にしました。
誰でも使うような言語機能であれば、今更取り上げるまでもなく知られているでしょうし、解説も溢れています。
逆に使われていない機能という集計は、おそらく誰もやっていないだろうと思いました。

しかし「言語機能」とは何か、どうやって集計するのかというのはなかなか難しい問題です。
そこで今回は、やや安易ではありますが、キーワードの数を集計することにしました。
もちろん「言語機能=キーワード」ではありません。
class や for は言語機能ですが、int や string は言語機能とは言えません。
また、継承のように、キーワードを使わない言語機能もあります。
ですが「ユルい発表だし、まぁいいか」と思いました。

少ない方に注目する意味

少ない方に注目した理由はもう一つあります。

例えば、「namespace」と「int」では、コード中にはどちらが多く出てくるでしょうか?
数えてみるまでもなく、int の方が圧倒的に多いのは明白です。
しかし、そのことをもって、「namespace より int の方がよく使われている」と言ってしまってよいものでしょうか?

namespace はおそらく、ほぼすべての C# ソース ファイルに、1 ファイル当たり 1 回だけ書かれるでしょう。それが namespace の上限です。
一方、int は何度も登場するかもしれませんし、場合によっては 1 回も登場しないファイルもあるでしょう。
粒度が違うので、単純に回数を比較するのはフェアではありません。
ちゃんと比較するなら、出現回数に応じて重みづけをするといった工夫が必要になります。
そうした方法を考えるのは面倒で、時間もあまりありませんでした。

一方、少ない方に注目した場合はどうでしょうか。
使用回数が namespace よりも少ないようなキーワードであれば、単純に数を比較するだけで、それなりに意味がある集計になるのではないでしょうか。*2

集計方法の試行錯誤

集計対象を GitHub の注目リポジトリにするという方針は最初に決めました。わかりやすく妥当な対象だと思います。
そこから集計ツールを作るのには試行錯誤しました。

対象のリポジトリは GitHub が提供している API で取得することができます。

最初は、ソース コードを入手するのにも API を使おうと思っていました(ver.1)。
API を叩くのには GitHub 自身が提供している公式クライアントライブラリである octokit.net を使うことにしたのですが、実はこのライブラリにはバグがあり、C# のコードを検索できませんでした。

github.com

翻訳サイトと格闘しながら、なんとか Issue を報告しました。

さて、バグを報告しつつも手元では応急処置を施して検索できるようにしました。
しかし、ソースコードは全部で 14 万以上もあり、あっという間にレート リミットに引っかかってしまいました。
また、レートに注意したとしても、API の仕様として 1,000 ファイルまでしか検索できないということが分かり、この方法は諦めました。

そこで、ソース コードは git.exe を叩いてクローンすることにしました。
最初はそれもプログラムで制御して、

  1. リポジトリの列挙
  2. git.exe によるクローン
  3. ソース コードをパースして集計

という一連の流れを、すべて一本のアプリの中で行おうとしていました(ver.2)。

しかし、これは別のプログラムに分けて、バッチ ファイルでやった方が楽だということに気が付きました。
そこで最終的に、

  • リポジトリの URL を出力するアプリ
  • git.exe を使ってクローンするバッチ ファイル
  • ソース コードをパースして集計するアプリ

という 3 つの部分に分割しました(ver.3)。

集計ツールは隠しているわけではありませんが、本当に間に合わせツールで恥ずかしいのでリンクはしないでおきます。
ちなみに集計部分のコードはこんな感じです。

var ast = CSharpSyntaxTree.ParseText(code);
var root = await ast.GetRootAsync();

var counts = root.DescendantTokens()
  .Where(x => x.IsKeyword())
  .GroupBy(x => x.Kind())
  .Select(x => new
  {
    Keyword = x.Key,
    Count = x.Count()
  })
  .OrderByDescending(x => x.Count)
  .ToArray();

キーワードではないやつら

ツールを一晩回して、なんとか集計はできました。

そこで、ふと不安になりました。
もし、一度も使われていないキーワードがあった場合は、それは集計に含まれていません。
しかしそれは、趣旨から言えば一位です。

そこで Roslyn のソースコードと突き合わせて、すべてのキーワードが登場しているか確かめました。
するといたのです。集計に含まれていないキーワードが。nameof でした。
「こんな便利な機能が、まさか!?」と思いましたが、登場回数 0 回ということで、一時、一位にランクインさせていました。
しかし、後で調べ直したところ、Roslyn は nameof をキーワードとして認識していないということがわかりました。
この理由については、当日、岩永さんから

nameof というメソッドがあった場合はメソッド名として解釈されるので、キーワード扱いすることができない

と教えて頂きました。

その他にも、var や dynamic などがキーワード扱いされていませんでした。*3

集計を終えて

上位にランクインしたものを見てみると、いくつかのグループに分けられます。

  • __arglist など、言語仕様書にも載っていない隠しキーワード
  • LINQ のクエリ式で使うキーワード
  • 属性の適用対象を指定するキーワード
  • その他

です。

隠しキーワードが少ないのは当然ですね。
クエリ式のキーワードが少ないのは、そもそも LINQ のクエリ式に人気がないのではないかと思っています。*4
属性の適用対象を指定するキーワードは、そのほとんどが「一応書けるようになっているけど、書いても書かなくても同じ」つまり「明示的に書く意味がない」ものばかりです。
その他に含まれるのは

です。

確かにどれも使う局面は少なそうです。
explicit は明示的に変換メソッドを設けた方が分かりやすいでしょう。
stackalloc は unsafe コンテキストでしか使えないので乱用するものではありません。
when は C# 6 から登場した新しい機能なので、まだ利用が浸透していないのだと思います。*5
alias も、無くても何とかなりそうです。

質問にお答えして

そう言えば、当日、岩永さんからこんな質問を頂いていました。

属性につける field は、自動プロパティのバッキング フィールドに適用されるのか?

つまりこういうことです。

class Foo
{
  // こう書いたら、NonSerialized 属性は Bar プロパティのバッキング フィールドに適用されるのか?
  [field: NonSerialized]
  public int Bar { get; set; }
}

その場ではわからなかったので「後で試して報告します」とお答えしました。

結論から言いますと、適用されませんでした。
コンパイル エラーにはなりませんが警告が出ますし、リフレクションで見ても適用されていませんでした。

おわりに

今回調べてみて、自分でも知らなかったキーワードがいくつかありました。*6
……今後も使うことはありそうにありません。

あと岩永さんのサイトすごい。何でも載ってる。

*1:笑いは結構とれたと思うのですが…

*2:というのは後付けで考えた理由なのですが。

*3:他にも漏れがあったら教えてください。

*4:が、メソッド形式との使用頻度の比較はしていませんので、単なる思い付きです。

*5:最初 where と混同していました

*6:alias とか typevar とか