雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

Console.WriteLine(Nothing) を紐解いてみる

VB のコードを書いたのが 1 年半ぶりな人が Visual Basic Advent Calendar 20 日目をお送り致します。
さて、先日 C# のメソッドオーバーロードの解決についてまとめたわけですが、 VB には Nothing と云う(メソッドオーバーロードの解決に於いて) dynamic より厄介な存在がある事をちゅきさんの記事を見て思い出しました。
……思い出してしまったので、何故 Console.WriteLine(Nothing) がコンパイルエラーとなるのか紐解いてみたいと思います。

Visual Basic 言語仕様書

VB のメソッドオーバーロードの解決に必要なドキュメントは、やっぱり言語仕様書です。
言語仕様書は MSDN にありますが、例によって英語なので(以下略。

C# との違い

ところで、そもそも C#VB も同じ .NET 言語ですがメソッドオーバーロードの解決はそれぞれの言語で独立的に規定されています。
今回調べるまで同じ仕様だと思っていたのですが、細部での解決ステップに結構違いがあったりして、新鮮な驚きがありました。
やはり言語仕様は楽しいですね。
……と云う前ふりを挟んでようやく Console.WriteLine(Nothing) のオーバーロード解決を始めます。

オーバーロードの解決

VB のメソッドオーバーロードの解決をは言語仕様書 11.8.1 に規定されています。
まず、今回の Console.WriteLine のケースではオーバーロードの対象となる「メソッドグループ」には以下が含まれます。

  • [1] WriteLine()
  • [2] WriteLine(Boolean)
  • [3] WriteLine(Char)
  • [4] WriteLine(Char())
  • [5] WriteLine(Char(), Integer, Integer)
  • [6] WriteLine(Decimal)
  • [7] WriteLine(Double)
  • [8] WriteLine(Single)
  • [9] WriteLine(Integer)
  • [10] WriteLine(UInteger)
  • [11] WriteLine(ULong)
  • [12] WriteLine(Object)
  • [13] WriteLine(String)
  • [14] WriteLine(String, Object)
  • [15] WriteLine(String, Object, Object)
  • [16] WriteLine(String, Object, Object, Object)
  • [17] WriteLine(String, Object())

……多いなぁ。
と、ここで C# と大きく違うところが。
C# では最初の時点でパラメータの個数を確認して引数と一致しないメソッドを対象から外します。
その後は基本的に適用出来るメソッドを探して候補リストに加えていく、と云うアプローチをしますが、 VB の場合は逆で、拡張メソッドを含めたオーバーロード一式をリストアップしたうえで、適用出来ないものをリストから外していくアプローチが取られます。
VB の言語特性によるところが大きい様ですが、これはとても興味深いです。
さて。
それでは 11.8.1 の規定に従ってリストから不適切な候補を外していきましょう。

11.8.1 (1)

今回はジェネリックメソッドではないので型の推論は対象外ですね。

11.8.1 (2)

次に、アクセスできないメンバーおよび引数リストに適用できないメンバーをすべてセットから削除します。

「アクセスできないメンバー」つまりは protected や private と云ったものを指していると思われます(つまりメソッドグループにはこう云ったものも含まれるわけで。。。)が、今回の Console.WriteLine にはアクセス出来ないメソッドがないので対象外です。
「引数リストに適用できないメンバー」については、 11.8.2 に規定があるのでそちらを参照します。

11.8.2 (1)

まず、それぞれの位置指定引数をメソッド パラメーターのリストと順番に照合します。位置指定引数の方がパラメーターよりも多く、最後のパラメーターが ParamArray でない場合、メソッドは適用できません。それ以外の場合は、位置指定引数の数と一致させるために、ParamArray パラメーターが ParamArray 要素型のパラメーターで拡張されます。位置指定引数が省略されている場合、メソッドは適用できません。

いろいろ書いてありますが、要は、

  • パラメータの数より引数の数が多いのにパラメータの最後にオプション引数が指定されていなかったら適用出来ない
  • 位置指定引数(パラメータ名が指定されていないパラメータ)に一致する引数が指定されていないパラメータがひとつでもあったら適用出来ない

と云う事です。
これによって [1] が候補から外れます。

11.8.2 (2)

今回のメソッドグループには名前付き引数を持ったオーバーロードがないので対象外です。

11.8.2 (3)

次に、一致していないパラメーターがあり、そのパラメーターが省略可能ではない場合、メソッドは適用できません。省略可能なパラメーターが残っている場合は、省略可能なパラメーター宣言で指定された既定値が、そのパラメーターと照合されます。 Object パラメーターで既定値が指定されていない場合は、次のようになります。
(後略)

ここでパラメータ数が一致していない [5], [14], [15], [16], [17], [18] が除外されます。

11.8.2 (4)

今回はジェネリックメソッドではないので対象外です。
さて、 11.8.2 の規定が一通り確認出来たので 11.8.1 の確認に戻りましょう。

11.8.1 (3)

次に、引数リストに適用できるようにするために縮小変換を必要とするすべてのメンバーをセットから削除します。ただし、引数式の型が Object である場合は除きます。

今回の引数は Nothing ですが、 8.8 の以下の規定により Nothing は任意の型への拡大変換が出来る事がわかります。

拡大変換には、次の変換があります。

恒等変換/既定の変換

従ってここで除外される候補はありません。

11.8.1 (4)

11.8.1 (3) と同様です。

11.8.1 (5)

今回の例は Shared メソッドなので対象外です。
# しかし、この位置まで拡張メソッドが候補として残っていると云う事に驚きを隠せません。。。 ^^;;

11.8.1 (6)

次に、セット内の 2 つのメンバーを M および N としたとき、引数リストに対して M が N よりも適切である場合は、セットから N を削除します。

ここからはメソッド同士の適用性を確認する事になります。
適用性については 11.8.1.1 で規定されているのでこちらを確認します。

11.8.1.1

まずは一文目。

メンバー M と N のシグネチャが同じである場合、または M の各パラメーター型が N の対応するパラメーター型と同じである場合、両者は “同等に適用可能” であると見なされます。

今回はシグネチャが等しいメソッドはないため、 “同等に適用可能” と云えるオーバーロードはありません。
次。

メンバー M と N のシグネチャが異なり、 M の少なくとも 1 つのパラメーター型が N のパラメーター型よりも適用可能であり、 N に M のパラメーター型よりも適用可能なパラメーター型が存在しない場合、 M は N よりも “適用性が高い” と見なされます。引数 Aj に対応するパラメーター Mj および Nj のペアがあるとき、次の条件のいずれかが満たされると、Mj の型は Nj の型よりも適用性が高いと見なされます。
(以下ひとつずつ確認)

1. Mj および Nj の型が同一である。

今回は引数が 1 つなので前述のチェックで同一の型がない事は確認済です。

2. Mj の型から Nj の型への拡大変換が存在する。

ここで、 8.8 の数値変換規則及び文字列変換規則に沿って各パラメータの拡大変換を確認します。

  • Integer から Long 、 Decimal 、 Single 、または Double への変換。
  • ULong から Decimal 、 Single 、または Double への変換。
  • Decimal から Single または Double への変換。
  • Char から String への変換。
  • Char() から String への変換。

これにより、各変換先をパラメータに持つメソッドより変換元をパラメータに持つメソッドの方が適用性が高い事は確認出来ます。
しかし、依然として複数の候補が残る事に変わりはありません。

3. Aj がリテラル 0 であり、Mj が数値型であり、Nj が列挙型である。

今回の引数はリテラル 0 ではないため対象外です。

4. Mj が Byte であり、Nj が SByte である。

メソッドグループに Byte をパラメータに持つオーバーロードメソッドがないため対象外です。

5. Mj が Short であり、Nj が UShort である。

メソッドグループに Short をパラメータに持つオーバーロードメソッドがないため対象外です。

6. Mj が Integer であり、Nj が UInteger である。

これにより [10] より [9] がより適切である事は言えます。

7. Mj が Long であり、Nj が ULong である。

メソッドグループに Long をパラメータに持つオーバーロードメソッドがないため対象外です。

8. Mj および Nj がデリゲート関数型であり、(以下略

メソッドグループに デリゲート関数型をパラメータに持つオーバーロードメソッドがないため対象外です。

11.8.1 (6) つづき

さて、 11.8.1.1 によって適切性の確認を一通り終えましたが、残念ながらまだひとつに絞り切れていません。そこで 11.8.1 (6) の続きの文を見ます。

セットに複数のメンバーが残り、引数リストに対する残りのメンバーの適切さが同等でない場合は、コンパイル時のエラーが発生します。

はい。残念ながらここで候補の絞り込みに失敗したと判断されコンパイルエラーになりました。

まとめる事がないまとめ

……と云う事で、 Console.WriteLine を例にして、引数に Nothing を渡した際のメソッドオーバーロード解決手順を追いかけてみました。
いやぁ、言語仕様って本当に楽しいですね(はぁと