クラスライブラリなどを提供する場合、同時に使い方や詳細な仕様をまとめたドキュメント(APIリファレンス)も提供する場合があります。 これらを個別に作成・メンテナンスすることも出来ますが、バージョンアップを重ねる度に実装とドキュメントの内容の同期を取るのは手間のかかる作業です。

これを解決する一つの方法として、ソースコード中にコメントとして直接APIのリファレンスを記述することで、実装とドキュメントの同期をしやすくするというやり方があります。 この場合、コメントからドキュメントを作成する方法が問題となってきます。 Doxygenなどこの様な機能を提供する専用のツールは存在しますが、C#およびVBコンパイラではこの様な要求を満たすXMLドキュメントコメントという機能がコンパイラの機能として用意されています。 XMLドキュメントコメントは、Javadocと同様の機能を提供するものです。

ここではXMLドキュメントコメントの記述方法と、XMLドキュメントコメントを使ったドキュメントの生成方法について解説します。

§1 XMLドキュメントコメントとは

XMLドキュメントコメントとは、ソースコードにクラスやメソッドなどの動作・引数・戻り値などの説明をコメント文の形式で記述するためのものです。 しかし、単なるコメント文ではなく、XMLフラグメント(不完全なXMLドキュメント)の形式で記述します。 XMLドキュメントコメントを記述することで、クラスやメソッドの説明をIntelliSenseに表示したり、ツールを使ってクラスライブラリのリファレンスドキュメントを作成したりすることが出来ます。

XMLドキュメントコメントを使って記述した一つの例を見てみます。 次のコードでは、通常のコメントとXMLドキュメントコメントの二種類を使ってコメント文を記述しています。

XMLドキュメントコメントを記述する例
using System;

/// <summary>
/// XMLドキュメントコメントを使ったサンプルです。
/// </summary>
class Sample {
  /// <summary>
  /// アプリケーションのメイン エントリ ポイントです。
  /// </summary>
  static void Main()
  {
    // メッセージを表示します
    Console.WriteLine("Hello, world");
  }
}

2つの//(スラッシュ)で始まる行は通常のコメント、3つの///で始まる行がXMLドキュメントコメントです。

次のようにJavadocのような「/** 〜 */」の形式のコメントもXMLドキュメントコメントとして扱われます。 当然、「/* 〜 */」は通常のコメントとして扱われます。

XMLドキュメントコメントを記述する例
using System;

/**
<summary>XMLドキュメントコメントを使ったサンプルです。</summary>
*/
class Sample {
  /**
  <summary>アプリケーションのメイン エントリ ポイントです。</summary>
  */
  static void Main()
  {
    /* メッセージを表示します */
    Console.WriteLine("Hello, world");
  }
}

XMLドキュメントコメントは、通常のコメント文と同様にコメントとして扱われるためコンパイル結果には一切影響を及ぼしません。 ですが、C#コンパイラでは1.0、VBコンパイラでは8.0からXMLドキュメントコメントに対応していて、通常のコメント文とは異なる扱いをします。 コンパイル結果に影響を及ぼさない点は変わりませんが、ソースコード上に記述されているXMLドキュメントコメントを読み取り、単一のXMLドキュメントを生成できるようになっています。

例えば、上記のコードからは次のようなXMLドキュメントが生成されます(XMLドキュメントの生成方法については後述します)。

ソースコードから生成されるXMLドキュメント
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>xmlcomment</name>
    </assembly>
    <members>
        <member name="T:Sample">
            <summary>
            XMLドキュメントコメントを使ったサンプルです。
            </summary>
        </member>
        <member name="M:Sample.Main">
            <summary>
            アプリケーションのメイン エントリ ポイントです。
            </summary>
        </member>
    </members>
</doc>

XMLドキュメントコメントではいくつかの要素(タグ)を使うことができ、これによって動作や引数・戻り値などの説明を記述することができます。 コンパイル時にこれらのXMLフラグメントが一つのXMLドキュメントとしてまとめられ、生成されるXMLドキュメントを加工することによってドキュメントの作成などを行うことが出来るようになっています。



§2 XMLドキュメントコメントの記述

XMLドキュメントコメントはXMLフラグメントとして記述するため、XMLの規則にならって記述する必要があります。 XML形式であれば基本的にどのような要素(タグ)でも記述することは出来ますが、使用するXML要素として推奨されているものがいくつか存在します。 ここではXMLドキュメントコメントの記述方法について見ていきます。

§2.1 XMLドキュメントコメントの要素

XMLドキュメントコメントを記述するにあたって、コメントに使用するXML要素(タグ)として推奨されるものがいくつか定められています。 例えば説明を記述するためのsummary要素や戻り値を説明するためのreturns要素などです。 これらの要素を使ってXMLドキュメントコメントを記述することにより、オブジェクトブラウザや入力候補に表示させたり、ツールを使ってリファレンスを作成したりすることが出来ます。 また、これらの要素は、言語によらずC#・VBなどで同じ要素を使うことが出来ます。

推奨される要素を使ってXMLドキュメントコメントを記述する例
using System;

namespace Smdn.Utils {
  /// <summary>文字列操作に関するユーティリティメソッドを提供するクラスです。</summary>
  /// <remarks>このクラスは静的クラスです。 インスタンスを作成することは出来ません。</remarks>
  public static class StringUtils {
    /// <summary>文字列の並びを逆転した結果を返します。</summary>
    /// <remarks><paramref name="str"/>の長さが0の場合、長さ0の<see cref="System.String"/>を返します。</remarks>
    /// <param name="str">並びを逆転する<see cref="System.String"/>を指定します。</param>
    /// <returns>並びを逆転した<see cref="System.String"/>を返します。</returns>
    /// <exception cref="ArgumentNullException"><paramref name="str"/>がnullの場合にスローされます。</exception>
    /// <example>
    /// 次のコードでは、変数<c>foo</c>に文字列<c>"oof"</c>が代入されます。
    /// <code>
    /// string foo = StringUtils.Reverse("foo");
    /// </code>
    /// </example>
    public static string Reverse(string str)
    {
      if (str == null) throw new ArgumentNullException("str");

      char[] chars = str.ToCharArray();

      Array.Reverse(chars);

      return new string(chars);
    }

    /// <summary>文字列を全角および半角の空白で区切った結果を<see cref="System.String"/>の配列で返します。</summary>
    /// <remarks><paramref name="str"/>の長さが0の場合、長さ0の<see cref="System.String"/>配列を返します。</remarks>
    /// <param name="str">分割する<see cref="System.String"/>を指定します。</param>
    /// <returns>文字列を全角および半角の空白で区切った<see cref="System.String"/>の配列を返します。</returns>
    /// <exception cref="ArgumentNullException"><paramref name="str"/>がnullの場合にスローされます。</exception>
    public static string[] SplitBySpace(string str)
    {
      if (str == null) throw new ArgumentNullException("str");

      return str.Split(new char[] {' ', ' '}, StringSplitOptions.None);
    }
  }
}

ここで使用した要素、およびよく使われる要素とその用途・意味を以下の表にまとめておきます。

XMLドキュメントコメントの要素
要素 形式 用途と意味
説明文のセクションを構成する要素 summary要素 <summary>...</summary> 型やメンバに関する説明・概要を記述します。

この要素は他の要素を含むこともでき、他の型やメンバへのリンクとなるsee要素や段落を表すpara要素などを使ってマークアップすることが出来ます。
この要素で記述された内容はIntelliSenseで表示されるようになるため、この要素では型やメンバについての簡単な(1行程度の)説明にとどめ、より詳細な動作や使用上の注意点などはremarks要素で記述するようにします。
remarks要素 <remarks>...</remarks> 型やメンバに関する補足的な追加の情報を記述します。

この要素でsummary要素の記述を補足します。 summary要素と同様、see要素やpara要素を使ってマークアップする事が出来ます。
param要素 <param name="name">...</param> 指定された引数nameに関する説明を記述します。

この要素でメソッドの引数に指定できる値や、値域についての説明を記述します。 ArgumentExceptionなど引数の例外について記述するにはexception要素を使います。
typeparam要素 <typeparam name="name">...</typeparam> ジェネリック型やジェネリックメソッドにおいて、指定された型パラメータnameに関する説明を記述します。
returns要素 <returns>...</returns> メソッドの戻り値に関する説明を記述します。
exception要素 <exception cref="member">...</exception> メンバがスローする例外に関する説明を記述します。

cref属性でスローする例外の型を、要素のテキストでその例外がスローされる理由・状況についての説明を記述します。
example要素 <example>...</example> 型やメンバの使用例について記述します。

see要素やpara要素の他に、インラインのコードはc要素、複数行にまたがるコードはcode要素を使って記述することが出来ます。
value要素 <value>...</value> プロパティが表す値について記述します。
seealso要素 <seealso cref="member"/> 指定されたメンバmemberへのリンクを「参照」セクションの関連項目に追加します。

seealso要素を使うことで、親クラスやオーバーロードされた他のメソッド、類似するメンバなどのリンクの一覧を記述することができます。 cref属性で指定された型・メンバへのリンクが「参照」のセクションに追加されます。
seealso要素は他の要素とは異なり説明文を記述することはできません。 また、seealso要素は説明文中でメンバへのリンクを記述するsee要素とは異なります。
リンクを作成するための要素 see要素 <see cref="member"/> cref属性で指定されたメンバmemberへのリンクを指定します。 see要素は説明文中で型やメンバのリンクを作成するために使用します(seealso要素とは異なります)。
paramref要素 <paramref name="name"/> 指定された引数nameの説明へのリンクを指定します。
typeparamref要素 <typeparamref name="name"/> 指定された型パラメータnameの説明へのリンクを指定します。
説明文内でのマークアップを行う要素 para要素 <para>...</para> 説明文の段落を定義します。 これはXHTML/HTMLのp要素やdiv要素に相当する要素です。
code要素 <code>...</code> 1行以上の複数行にわたるコードを記述します。 説明文中でサンプルコードを提示する場合に使います。
c要素 <c>...</c> インラインで表示されるコードを記述します。 説明文中の変数名やメソッド名などをマークアップする場合に使います。

summaryなど説明文のセクションを構成する要素は常に最上位の要素でなければならず、互いに他の要素の子要素として入れ子にしたりすることはできません。 例えば、remarks要素の中にexample要素を含めたりすることはできません。

それ以外の要素はsummary要素やexample要素に含めて記述することができます。 例えば、remarks要素やexample要素の説明文中でサンプルコードを記述したい場合にはcode要素を使います。

上記以外にも、メンバのアクセス許可に関する説明を記述するpermission要素、リスト構造を作成するlist要素、他のドキュメントを読み込むinclude要素などがあります。 詳細は以下のドキュメントを参照してください。

上記のドキュメントにおいて推奨されるXMLタグでは、Obsolete属性によって廃止予定・非推奨とマークされた型・メンバであることを表す<obsolete><deprecated>のような要素は定義されていません。 したがって、廃止予定・非推奨であることをドキュメントに記載する場合は、<summary>要素あるいは<remarks>要素を用いて記述する必要があります。 これに関しては機能の廃止・旧式化 §.Obsolete属性とXMLドキュメントコメントも参照してください。

このようにして記述したXMLドキュメントコメントは、前述したとおりIDEの入力候補等でも表示されるようになります。

なお、先に挙げたのコードをコンパイルすると、以下のようなXMLドキュメントが生成されます。

コンパイラが出力するXMLドキュメント
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Smdn.Utils</name>
    </assembly>
    <members>
        <member name="T:Smdn.Utils.StringUtils">
            <summary>文字列操作に関するユーティリティメソッドを提供するクラスです。</summary>
            <remarks>このクラスは静的クラスです。 インスタンスを作成することは出来ません。</remarks>
        </member>
        <member name="M:Smdn.Utils.StringUtils.Reverse(System.String)">
            <summary>文字列の並びを逆転した結果を返します。</summary>
            <remarks><paramref name="str"/>の長さが0の場合、長さ0<see cref="T:System.String"/>を返します。</remarks>
            <param name="str">並びを逆転する<see cref="T:System.String"/>を指定します。</param>
            <returns>並びを逆転した<see cref="T:System.String"/>を返します。</returns>
            <exception cref="T:System.ArgumentNullException"><paramref name="str"/>がnullの場合にスローされます。</exception>
            <example>
            次のコードでは、変数<c>foo</c>に文字列<c>"oof"</c>が代入されます。
            <code>
            string foo = StringUtils.Reverse("foo");
            </code>
            </example>
        </member>
        <member name="M:Smdn.Utils.StringUtils.SplitBySpace(System.String)">
            <summary>文字列を全角および半角の空白で区切った結果を<see cref="T:System.String"/>の配列で返します。</summary>
            <remarks><paramref name="str"/>の長さが0の場合、長さ0<see cref="T:System.String"/>配列を返します。</remarks>
            <param name="str">分割する<see cref="T:System.String"/>を指定します。</param>
            <returns>文字列を全角および半角の空白で区切った<see cref="T:System.String"/>の配列を返します。</returns>
            <exception cref="T:System.ArgumentNullException"><paramref name="str"/>がnullの場合にスローされます。</exception>
        </member>
    </members>
</doc>

§2.2 cref属性

exception要素・seealso要素・see要素では、cref属性に記述された型やメンバを参照します。 cref属性に型名・メンバ名を記述する際の要点として、次の事項が挙げられます。

  • cref属性では、コード中から参照する場合と同じように参照する型やメンバのスコープが適用される
    • 同一クラス内のメンバを参照する場合はクラス名を省略できる
    • 異なるクラスのメンバを参照する場合はクラス名から記述する必要がある
    • using句・Importステートメントでインポートされていない名前空間の型を参照する場合は、名前空間を付加した完全限定名で型名を記述する必要がある
  • オーバーロードされたメソッドでは、参照するメソッドを限定するために引数リストを記述する必要がある
  • List<T>などのジェネリック型を参照する場合はXML中に山カッコを記述することはできないので、文字参照を使ってList&lt;T&gt;と記述するか、波カッコを使ってList{T}と記述する必要がある

これらの点を踏まえてcref属性を記述すると次のようになります。

XMLドキュメントコメントにおけるcref属性の指定例
using System;
using System.Collections.Generic;

namespace SampleNamespace {
  struct Structure1 {
    int Field;
  }

  class Class1 {
    /// <example>
    /// 同一型内のメンバの場合は、クラス名を省略して<see cref="Field"/>や<see cref="Method"/>のように記述できる
    /// 異なる型のメンバの場合は、<see cref="Structure1.Field"/>のように型名から記述する必要がある
    /// </example>
    public int Field = 0;
    public void Method() {}

    /// <example>
    /// オーバーロードされたメソッドを限定する場合は、<see cref="Method1()"/>、
    /// <see cref="Method1(int)"/>、<see cref="Method1(int,int)"/>のように引数リストを記述する必要がある
    /// </example>
    public void Method1() {}
    public void Method1(int x) {}
    public void Method1(int x, int y) {}

    /// <example>
    /// 引数が配列やref/outパラメータの場合は、
    /// <see cref="Method1(int[,])"/>、<see cref="Method1(ref int)"/>のように記述する
    /// </example>
    public void Method1(int[,] x) {}
    public void Method1(ref int x) {}

    /// <example>
    /// コンストラクタを参照する場合は、<see cref="Class1()"/>、<see cref="Class1(int)"/>のように
    /// 「型名(引数リスト)」と記述する
    /// </example>
    public Class1() {}
    public Class1(int x) {}
  }

  /// <example>
  /// ジェネリック型・ジェネリックメソッドを参照する場合、
  /// 山カッコを文字参照に置き換えて<see cref="List&lt;T&gt;.Add"/>のように記述するか、
  /// 山カッコを波カッコに置き換えて<see cref="List{T}.Add"/>のように記述する必要がある
  /// <see cref="List{THoge}.Add"/>のように型パラメータ名が異なっていても、型パラメータの数が一致すれば適切に参照される
  /// </example>
  class Class2 {
    /// <exception cref="ArgumentException">インポートされた名前空間にある型は名前空間を記述しなくてもよい</exception>
    /// <exception cref="System.ComponentModel.InvalidEnumArgumentException">
    /// インポートされていない名前空間にある型は、完全限定名を記述する必要がある
    /// </exception>
    /// <remarks>実際にはスローされない例外がcref属性に記述されていてもエラーとはならない</remarks>
    public void ThrowsExceptionMethod()
    {
    }
  }

  /// <example>
  /// インポートされていない場合に完全限定名を記述するのは、型だけでなくメンバの場合も同様
  /// <see cref="OtherNamespace.Class5.Field5"/>
  /// </example>
  class Class3 {
    protected void Method3() {}
  }

  /// <example>
  /// 基底クラスのメンバを参照する場合は、<see cref="Class3.Method3()"/>のように型名から記述する必要がある
  /// </example>
  class Class4 : Class3 {
  }
}

namespace OtherNamespace {
  class Class5 {
    public static readonly int Field5 = 16;
  }
}

このほか、cref属性ではメンバのIDを指定することもできます。

§2.2.1 メンバのID

see要素などで指定されたcref属性の値は、コンパイル時に後述する形式のIDに変換されます。 例えば、次のようなXMLドキュメントが記述されているコードがあったとします。

cref属性を記述したXMLドキュメント
/// <seealso cref="String.IndexOf(string)"/>
class Class1 {}

このコードをコンパイルすると、cref属性は次のように変換されます。

出力されるXMLドキュメントの抜粋
<member name="T:Class1">
    <seealso cref="M:System.String.IndexOf(System.String)" />
</member>

このIDは、「メンバの種類:完全限定名」の形式で定義されています。 メンバの種類は一文字で表され、型を表すTやメソッドを表すMなどを指定します。 例えば、型であれば「T:名前空間.型名」となり、メソッドであれば「M:名前空間.型名.メソッド名(引数リスト)」のようになります。 このように変換されるメンバのIDは、cref属性を指定する際にも使用することができます。

IDの規則といくつかの変換例を表にまとめると次のようになります。

IDの規則
IDの例
種類 変換例 IDが表すメンバ
N (名前空間) N:System.Collections System.Collections名前空間
T (型)
クラス/インターフェイス/構造体/列挙体/デリゲート
T:System.String Stringクラス
T:System.ArgumentException ArgumentException例外クラス
T:System.StringComparison StringComparison列挙体
T:System.IDisposable IDisposableインターフェイス
T:System.EventHandler EventHandlerデリゲート
T:System.Collections.Generic.List`1 List<T>ジェネリッククラス
T:System.Action`1 Action<T>ジェネリックデリゲート
T:System.Action`2 Action<T1, T2>ジェネリックデリゲート
M (メソッド)
メソッド/コンストラクタ/オーバーロードされた演算子
M:System.String.ToUpper String.ToUpperメソッド
M:System.String.IndexOf(System.String) String.IndexOf(string)メソッド
M:System.String.IndexOf(System.String,System.Int32) String.IndexOf(string, int)メソッド
M:System.String.#ctor(System.Char[]) String(char[])コンストラクタ
M:System.String.#ctor(System.Char,System.Int32) String(char, int)コンストラクタ
M:System.String.op_Equality(System.String,System.String) Stringクラスの等価演算子==
P (プロパティ)
プロパティ/インデクサ
P:System.String.Length String.Lengthプロパティ
P:System.String.Chars(String.Int32) String.Charsプロパティ (インデクサ)
P:System.Collections.Generic.List`1.Count List<T>.Countプロパティ
F (フィールド) F:System.IO.Path.DirectorySeparatorChar Path.DirectorySeparatorCharフィールド
E (イベント) E:System.Windows.Form.Control.Click Control.Clickイベント

その他、メンバIDの規則の詳細については以下のドキュメントにまとめられています。

§2.2.2 ジェネリック型・ジェネリックメソッドのID

ジェネリック型の型名を表記する場合、コード中で使用される山カッコ < > ではなくバッククォート ` と型パラメータのを使って表記します。 例えば、List<T>は型パラメータの数が1なのでIDは次のようになります。

System.Collections.Generic.List<T>
              ↓
T:System.Collections.Generic.List`1

同様に、Dictionary<TKey, TValue>.Countプロパティでは型パラメータの数が2なのでIDは次のようになります。

System.Collections.Generic.Dictionary<TKey, TValue>.Count
              ↓
P:System.Collections.Generic.Dictionary`2.Count

ジェネリックメソッドの場合は、バッククォート2つに続けて型パラメータの数を指定します。 また、引数で型パラメータを参照するにはバッククォート2つに続けて0から始まる型パラメータのインデックスを指定します。 例えばSystem.Linq.Enumerable.SelectメソッドのIDは次のようになります。

System.Linq.Enumerable.Select<TSource, TResult>(IEnumerable<TSource>, Func<TSource, TResult>)
              ↓
M:System.Linq.Enumerable.Select``2(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1})

§2.2.3 メソッドの引数リストとID

メソッドのrefパラメータやoutパラメータは、ともに型名の後ろにアットマーク @ を付けます。 ポインタ型の場合は型名の後ろにアスタリスク * を付けます

FooNamespace.BarClass.BazMethod(ref int, out int, int*)
              ↓
M:FooNamespace.BarClass.BazMethod(System.Int32@,System.Int32@,System.Int32*)

配列(ジャグ配列や多次元配列)の場合は次のようになります。 多次元配列では各次元でのインデックスの下限を表す 0: を記述する必要があります。

FooNamespace.BarClass.BazMethod(int[], int[][], int[,])
              ↓
M:FooNamespace.BarClass.BazMethod(System.Int32[],System.Int32[][],System.Int32[0:,0:])

§2.3 CDATAを使ったサンプルコードの記述

XMLドキュメントの要素中では不等号<,>を直接記述することができません。 かわりに文字参照(&lt;, &gt;)に置き換えて説明文を記述することもできますが、比較演算子やジェネリック型を多用するサンプルコードを記述する場合には不便です。

CDATAセクションを使うと、次の例のように文字参照への置き換えなしでコードをそのまま記述することができます。

CDATAセクションを使ってサンプルコードを記述する例
using System;
using System.Collections.Generic;

/// <example>
///  CDATAセクションを使った<see cref="List{T}"/>のサンプルコード記述例です。
///  <code><![CDATA[
///    var list = new List<string>();
///
///    list.Add("foo");
///    list.Add("bar");
///
///    if (0 < list.Count)
///      list.Add("baz");
///  ]]></code>
///  文字参照を使った<see cref="List{T}"/>のサンプルコード記述例です。
///  <code>
///    var list = new List&lt;string&gt;();
///
///    list.Add("foo");
///    list.Add("bar");
///
///    if (0 &lt; list.Count)
///      list.Add("baz");
///  </code>
/// </example>
class Class1 {}
コンパイルして得られるXMLドキュメント
<member name="T:Class1">
     <example>
      CDATAセクションを使った<see cref="T:System.Collections.Generic.List`1"/>のサンプルコード記述例です。
      <code><![CDATA[
        var list = new List<string>();
    
        list.Add("foo");
        list.Add("bar");
    
        if (0 < list.Count)
          list.Add("baz");
      ]]></code>
      文字参照を使った<see cref="T:System.Collections.Generic.List`1"/>のサンプルコード記述例です。
      <code>
        var list = new List&lt;string&gt;();
    
        list.Add("foo");
        list.Add("bar");
    
        if (0 &lt; list.Count)
          list.Add("baz");
      </code>
     </example>
</member>

CDATAセクションはcode要素以外でも使うことができます。

§2.4 外部ドキュメントのインクルード (include要素)

XMLドキュメントコメントでは、include要素を使うことで外部のXMLファイルからドキュメントをインクルードすることができます。 include要素を使うことでドキュメントをソースコード上ではなく別のファイルで管理したり、ソース上の複数箇所で同じ説明文が記述されることがないよう定型文を外部ファイル化したりすることができます。

include要素は<include file="filename" path="xpath"/>のように記述します。 ドキュメントの生成時に、XMLファイルfilenameからXPath式xpathで表される要素の内容が読み込まれ、include要素が記述された位置にインクルードされます。

include要素を使った例を見てみます。 まず、次のようなメソッド郡と、XMLドキュメントコメントを記述します。 ソースコード中には、引数の型だけが異なり、機能的には同一のメソッドが記述されています。 このメソッドではsummary要素やparam要素の内容は共通なので、これらを外部のファイルで記述することにします。 ここでは、ファイルMathUtils.xmlに記述した内容をインクルードするようにします。

引数の型だけが異なるメソッド郡を含むソースコード
using System;

public static class MathUtils {
  /// <include file="MathUtils.xml" path="MathUtils/Max/*"/>
  /// <returns>引数で与えられた3つの<see cref="T:System.Int32"/>のうち、最大のものを返します。</returns>
  public static int Max(int x, int y, int z)
  {
    return Math.Max(Math.Max(x, y), z);
  }

  /// <include file="MathUtils.xml" path="MathUtils/Max/*"/>
  /// <returns>引数で与えられた3つの<see cref="T:System.Int64"/>のうち、最大のものを返します。</returns>
  public static long Max(long x, long y, long z)
  {
    return Math.Max(Math.Max(x, y), z);
  }

  /// <include file="MathUtils.xml" path="MathUtils/Max/*"/>
  /// <returns>引数で与えられた3つの<see cref="T:System.Double"/>のうち、最大のものを返します。</returns>
  public static double Max(double x, double y, double z)
  {
    return Math.Max(Math.Max(x, y), z);
  }
}

インクルードするMathUtils.xmlの内容は次のようにします。 include要素のpath属性ではXPath式MathUtils/Max/*を指定しているため、このファイルに記述されている「MathUtils要素配下の(MathUtils/)、Max要素配下にある(Max/)、すべての要素(*)」がインクルードされます。

共通する内容を記述したファイル
<?xml version="1.0"?>
<MathUtils>
  <Max>
    <summary>引数で与えられた3つの値のうち、最大のものを返します。</summary>
    <param name="x">比較する3つの値のうち、1つ目の値。</param>
    <param name="y">比較する3つの値のうち、2つ目の値。</param>
    <param name="z">比較する3つの値のうち、3つ目の値。</param>
  </Max>
</MathUtils>

このように記述されたソースコードからは、次のようなXMLドキュメントが生成されます。

生成されるXMLドキュメント
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>test</name>
    </assembly>
    <members>
        <member name="M:MathUtils.Max(System.Int32,System.Int32,System.Int32)">
            <summary>引数で与えられた3つの値のうち、最大のものを返します。</summary>
            <param name="x">比較する3つの値のうち、1つ目の値。</param>
            <param name="y">比較する3つの値のうち、2つ目の値。</param>
            <param name="z">比較する3つの値のうち、3つ目の値。</param>
            <returns>引数で与えられた3つの<see cref="T:System.Int32" />のうち、最大のものを返します。</returns>
        </member>
        <member name="M:MathUtils.Max(System.Int64,System.Int64,System.Int64)">
            <summary>引数で与えられた3つの値のうち、最大のものを返します。</summary>
            <param name="x">比較する3つの値のうち、1つ目の値。</param>
            <param name="y">比較する3つの値のうち、2つ目の値。</param>
            <param name="z">比較する3つの値のうち、3つ目の値。</param>
            <returns>引数で与えられた3つの<see cref="T:System.Int64" />のうち、最大のものを返します。</returns>
        </member>
        <member name="M:MathUtils.Max(System.Double,System.Double,System.Double)">
            <summary>引数で与えられた3つの値のうち、最大のものを返します。</summary>
            <param name="x">比較する3つの値のうち、1つ目の値。</param>
            <param name="y">比較する3つの値のうち、2つ目の値。</param>
            <param name="z">比較する3つの値のうち、3つ目の値。</param>
            <returns>引数で与えられた3つの<see cref="T:System.Double" />のうち、最大のものを返します。</returns>
        </member>
    </members>
</doc>

§2.5 独自の要素・属性とXMLドキュメントの拡張

XMLドキュメントコメントでは、妥当なXMLである限り上記以外の要素や属性を使って記述することが出来ます。 例えば、以下のようにXML名前空間を使って独自の属性を埋め込んだり、XHTMLの要素を使ってドキュメントを記述するといったことが出来ます。

独自の要素・属性を使ってマークアップしたドキュメントの例
/// <summary xml:lang="ja">1つめ<see cref="T:System.String"/></summary>
/// <summary>2つめ<see cref="T:System.String" x:version="netfx2.0" xmlns:x="http://example.com/ns/extensions"/></summary>
/// <summary xml:lang="en">3rd<see cref="T:System.String"/></summary>
/// <remarks>
/// XHTMLの要素を使って説明を記述する例です。
/// <div xmlns="http://www.w3.org/1999/xhtml">
///   <h1>メソッドの動作</h1>
///   <p>メソッドの動作をフローチャートで表すと次のようになります。
///     <img src="flowchart.png" alt="フローチャート" />
///   </p>
/// </div>
/// </remarks>
class XmlDocumentExtension {}

上記のコードからは次のようなXMLドキュメントが生成されます。

生成されるXMLドキュメント
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>XmlDocumentExtension</name>
    </assembly>
    <members>
        <member name="T:XmlDocumentExtension">
            <summary xml:lang="ja">1つめ<see cref="T:System.String"/></summary>
            <summary>2つめ<see cref="T:System.String" x:version="netfx2.0" xmlns:x="http://example.com/ns/extensions"/></summary>
            <summary xml:lang="en">3rd<see cref="T:System.String"/></summary>
            <remarks>
            XHTMLの要素を使って説明を記述する例です。
            <div xmlns="http://www.w3.org/1999/xhtml">
              <h1>メソッドの動作</h1>
              <p>メソッドの動作をフローチャートで表すと次のようになります。
                <img src="flowchart.png" alt="フローチャート" />
              </p>
            </div>
            </remarks>
        </member>
    </members>
</doc>

独自の要素を含むXMLドキュメントを生成することが出来ても、整形・表示する側がサポートしていない場合は当然無視されるかエラーとなります。 整形・表示には対応しているツールを使用したり専用のツールを作成する必要があります。

§2.6 XMLドキュメントと入力候補

Visual Studio 2008では、XMLドキュメントコメントを記述するだけで入力候補に表示されるようになります。 必ずしもXMLドキュメントを生成するように設定する必要はありません。

また、オブジェクトブラウザで閲覧する場合にも、記述したXMLドキュメントコメントの内容が表示されるようになります。

MonoDevelop 2.4でも同様に、XMLドキュメントを生成していなくても入力候補に表示されるようになります。

§3 XMLドキュメントの生成

XMLドキュメントコメントを記述するだけでもIDEの入力候補に表示されたりするようになりますが、コンパイラにXMLドキュメントを生成させるようにすることで、記述したXMLドキュメントコメントを他のツールを使って整形したりより見やすい形式に変換したりすることが出来るようになります。 ここでは、コマンドラインとIDE上でXMLドキュメントを生成するように設定する手順について見ていきます。

§3.1 コマンドラインでの作成

コマンドラインでXMLドキュメントを生成するために最低限必要なオプションは以下のとおりです。 出力するターゲットが実行可能ファイルでもライブラリでもXMLドキュメントを生成できます。 いずれもcsc・vbcおよびMono C#コンパイラであるmcs(gmcs)に共通です。

/doc:(ファイル名)
XMLドキュメントを生成し、指定したファイル名で保存する
(入力ファイル)
アセンブリのソースファイルを指定する

以下は先のコードを使ってコンパイルとXMLドキュメントの生成を行う例です。

cscでのXMLドキュメントの生成
csc /doc:sample.xml xmlcomment.cs

なお、2013年08月現在のvbncではXMLドキュメントの生成はサポートされていません。

§3.2 IDEでの設定

ここではXMLドキュメントを生成するためのIDEでの設定方法について見ていきます。

§3.2.1 Visual Studio 2008での例

XMLドキュメントを生成させたいプロジェクトのコンテキストメニューから[プロパティ]を選択します。

[ビルド]タブを選択し、最下部にある[出力]の[XML ドキュメント ファイル]にチェックを入れます。 必要に応じて、出力するXMLドキュメントのフォルダとファイル名を指定します。

以上の手順でビルド時にXMLドキュメントが生成されるようになります。

§3.2.2 MonoDevelop 2.4での例

XMLドキュメントを生成させたいプロジェクトのコンテキストメニューから[オプション]を選択します。

[ビルド]→[コンパイラ]の項目を選択し、[一般オプション]の[xml ドキュメンテーションを生成する]にチェックを入れます。

以上の手順でビルド時にXMLドキュメントが生成されるようになります。 XMLドキュメントは、プロジェクトの出力フォルダに「アセンブリ名.xml」のファイル名で出力されるようになります。 なお、生成されるXMLドキュメントのファイル名を指定することは出来ません。

§4 XMLドキュメントの整形とHTMLへの変換

XMLドキュメントを整形したり他の形式に変換するツールにはSandcastleNDocなどいくつか種類がありますが、ここでは一つの例としてコマンドラインでのドキュメント生成が行えるMonodocを使ってHTMLに変換する例を紹介します。 Monodocはドキュメントの編集と閲覧を行うためのツールですが、コンパイラが生成するXMLドキュメントをインポートしてHTML形式に整形して出力させることも出来るようになっています。

ではMonodocを使ってXMLドキュメントからHTMLを作成する手順について見ていきます。 ここではクラスライブラリの作成で作成したライブラリSmdn.Utils.dllを使ってHTMLを作成します。 ビルドが完了した時点でのディレクトリ構成は次のようになっています。

$ tree 
.
|-- AssemblyInfo.cs
|-- OrderByLengthComparer.cs
|-- Smdn.Utils.csproj
|-- Smdn.Utils.pidb
|-- StringUtils.cs
`-- bin
    `-- Debug
        |-- Smdn.Utils.dll
        |-- Smdn.Utils.dll.mdb
        `-- Smdn.Utils.xml

2 directories, 8 files

HTMLを生成するにはまずXMLドキュメントをインポートしてMonodoc形式に変換する必要があります。 mdoc updateコマンドでXMLドキュメントと元になるアセンブリを指定してインポートします。 ここでは、変換したMonodoc形式ファイルの出力先ディレクトリ名をmdocとしました。

mdocを使ってXMLドキュメントからMonodoc形式に変換する
$ mdoc update -i ./bin/Debug/Smdn.Utils.xml -o ./mdoc/ ./bin/Debug/Smdn.Utils.dll
New Type: Smdn.Utils.StringUtils
Member Added: public static string Reverse (string str);
Member Added: public static string[] SplitBySpace (string str);
Namespace Directory Created: Smdn.Utils
New Namespace File: Smdn.Utils
New Type: Smdn.Utils.OrderByLengthComparer
Member Added: public OrderByLengthComparer ();
Member Added: public override int Compare (string x, string y);
Members Added: 4, Members Deleted: 0

$ tree ./mdoc/
./mdoc/
|-- Smdn.Utils
|   |-- OrderByLengthComparer.xml
|   `-- StringUtils.xml
|-- index.xml
`-- ns-Smdn.Utils.xml

1 directory, 4 files

これでMonodoc形式に変換されました。 Monodocでは専用のエディタを使ってこのファイルを編集することも出来ますが、ここでは編集を省略してHTMLへの変換に進みます。

mdoc export-htmlコマンドでMonodoc形式からHTML形式に変換します。 出力先のディレクトリ名はhtmlとしました。

mdocを使ってMonodoc形式からHTML形式に変換する
$ mdoc export-html -o ./html/ ./mdoc/
Smdn.Utils.OrderByLengthComparer
Smdn.Utils.StringUtils

$ tree ./html/
./html/
|-- Smdn.Utils
|   |-- OrderByLengthComparer.html
|   |-- StringUtils.html
|   `-- index.html
`-- index.html

1 directory, 4 files

これで変換されたHTMLファイルが出力されます。 生成されたindex.htmlをブラウザで閲覧すると、ドキュメントを参照することが出来ます。 なお、生成したファイルはこちらのページでも閲覧できるようにしてありますのでご覧ください。

§5 XMLドキュメントと翻訳

コンパイラやオブジェクトブラウザが認識するXMLドキュメントの要素にはドキュメントの言語に関する規定がなく、日本語や英語など複数の言語でドキュメントを記述する方法については特にこれといった方法が用意されていません。 そのため、複数の言語でドキュメントを記述したり、ローカライズされたドキュメントを生成するには何らかの追加的な手順をとる必要があります。

§5.1 条件付きコンパイルを使う方法

#ifディレクティブによる条件付きコンパイルは、XMLドキュメントコメント部にも適用されます。 そこで、言語別のドキュメントを生成するシンボルを定義し、かつ定義したシンボルを使って各言語毎にXMLドキュメントコメントを分割して記述することにより、出力されるXMLドキュメントの言語を切り替えるようにすることができます。

次の例では、シンボルDOCLANG_JAが定義されている場合は日本語、それ以外はデフォルトとして英語のドキュメントが生成されるようにXMLドキュメントコメントを記述しています。

条件付きコンパイルで言語別のXMLドキュメントを生成する例
using System;

namespace Smdn.Utils {
#if DOCLANG_JA
  /// <summary>文字列操作に関するユーティリティメソッドを提供するクラスです。</summary>
#else
  /// <summary>The StringUtils class provides string-related utility methods.</summary>
#endif
  public static class StringUtils {}
}

このように記述されたソースコードを、シンボルの定義なしでコンパイルすれば英語のドキュメントが生成されます。

シンボルを定義しないでコンパイルする
csc StringUtils.cs /out:Smdn.Utils.dll /target:library /doc:Smdn.Utils.xml
生成されるXMLドキュメント
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Smdn.Utils</name>
    </assembly>
    <members>
        <member name="T:Smdn.Utils.StringUtils">
            <summary>The StringUtils class provides string-related utility methods.</summary>
        </member>
    </members>
</doc>

そして、/defineスイッチでシンボルDOCLANG_JAを定義してコンパイルすれば日本語のドキュメントが生成されます。

シンボルDOCLANG_JAを定義してコンパイルする
csc StringUtils.cs /out:Smdn.Utils.dll /target:library /define:DOCLANG_JA /doc:Smdn.Utils.xml
生成されるXMLドキュメント
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Smdn.Utils</name>
    </assembly>
    <members>
        <member name="T:Smdn.Utils.StringUtils">
            <summary>文字列操作に関するユーティリティメソッドを提供するクラスです。</summary>
        </member>
    </members>
</doc>

この方法ではシンボルの定義だけで言語別のドキュメントを生成できるという利点がありますが、記述しなければならない言語の数が増えれば#ifディレクティブによる分岐が煩雑になり、また言語の数だけコンパイルを行う必要があるという欠点があります。

§5.2 独自の要素・属性を使う方法

XMLドキュメントコメントでは独自の要素や属性を使うことができるため、これを使って言語別にマークアップを行うことができます。 ここでは例として、使用が推奨される要素にxml:lang属性を付加して言語別にドキュメントコメントを記述することにします。 なお、xml:lang属性がないものはドキュメントにおけるデフォルトの言語(未翻訳のドキュメント)であるとします。

xml:lang属性を付加して言語別のXMLドキュメントコメントを記述する例
using System;

namespace Smdn.Utils {
  /// <summary xml:lang="ja">文字列操作に関するユーティリティメソッドを提供するクラスです。</summary>
  /// <summary>The StringUtils class provides string-related utility methods.</summary>
  public static class StringUtils {
    /// <summary>The Method1</summary>
    /// <summary xml:lang="ja">メソッド1</summary>
    public static void Method1()
    {
    }

    /// <summary>The Method2</summary>
    public static void Method2()
    {
    }
  }
}

このように記述されたソースコードをコンパイルすると次のようなドキュメントが生成されます。

生成されるドキュメント
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Smdn.Utils</name>
    </assembly>
    <members>
        <member name="T:Smdn.Utils.StringUtils">
            <summary xml:lang="ja">文字列操作に関するユーティリティメソッドを提供するクラスです。</summary>
            <summary>The StringUtils class provides string-related utility methods.</summary>
        </member>
        <member name="M:Smdn.Utils.StringUtils.Method1">
            <summary>The Method1</summary>
            <summary xml:lang="ja">メソッド1</summary>
        </member>
        <member name="M:Smdn.Utils.StringUtils.Method2">
            <summary>The Method2</summary>
        </member>
    </members>
</doc>

このままでは複数の言語での記述が混在したままとなるので、xml:lang属性に指定されている値を使って目的の言語だけの記述となるようにフィルタリングします。 ここでは次のような簡易ツールを使ってフィルタリングを行います。

xml:lang属性の値に従ってXMLドキュメントの要素をフィルタリングする簡易ツール
using System;
using System.Collections.Generic;
using System.Xml;

public class XmlLangFilter {
  static void Main(string[] args)
  {
    // 第一引数で与えられたXMLファイルを読み込む
    var document = new XmlDocument();

    document.Load(args[0]);

    // 第二引数で与えられるxml:langの値にしたがって要素をフィルタリングする
    var filterLang = 2 <= args.Length ? args[1] : string.Empty;

    // 全ての/doc/members/member要素について、引数で与えられたxml:langの値を持つ要素だけをフィルタリングする
    foreach (XmlElement memberElement in document.SelectNodes("/doc/members/member")) {
      // member下の要素名と要素のXmlElementを格納するDictionary
      var langElement = new Dictionary<string, XmlElement>();
      // member下の全てのXmlElementを格納するList
      var elements = new List<XmlElement>();

      // menber要素のすべての子要素を列挙
      foreach (XmlElement element in memberElement.ChildNodes) {
        elements.Add(element);

        // 要素名を取得
        var name = element.Name;
        // xml:lang属性の値を取得
        var lang = element.GetAttribute("xml:lang");

        // xml:lang属性を削除する
        element.RemoveAttribute("xml:lang");

        if (langElement.ContainsKey(name)) {
          // すでにDictionaryに同名の要素が格納されている場合、
          // xml:langが目的の値を持つ場合に限り上書きする
          if (lang == filterLang)
            langElement[name] = element;
        }
        else {
          langElement[name] = element;
        }
      }

      // 一旦member要素のすべての子要素を削除する
      foreach (var element in elements) {
        memberElement.RemoveChild(element);
      }

      // フィルタリングした要素だけを追加する
      foreach (var element in langElement.Values) {
        memberElement.AppendChild(element);
      }
    }

    // フィルタリングしたXMLドキュメントを標準出力に書き出す
    var writerSettings = new XmlWriterSettings();

    writerSettings.Indent = true;
    writerSettings.IndentChars = "    ";

    var writer = XmlWriter.Create(Console.OpenStandardOutput(), writerSettings);

    document.Save(writer);
  }
}

このツールを使ってフィルタリングすることにより、次のように言語別のドキュメントを得ることができます。

xml:lang属性の指定されていない要素だけをフィルタリングする
XmlLangFilter.exe input.xml
xml:lang属性の指定されていない要素だけをフィルタリングした結果
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Smdn.Utils</name>
    </assembly>
    <members>
        <member name="T:Smdn.Utils.StringUtils">
            <summary>The StringUtils class provides string-related utility methods.</summary>
        </member>
        <member name="M:Smdn.Utils.StringUtils.Method1">
            <summary>The Method1</summary>
        </member>
        <member name="M:Smdn.Utils.StringUtils.Method2">
            <summary>The Method2</summary>
        </member>
    </members>
</doc>
xml:lang属性にjaが指定されている要素だけをフィルタリングする
XmlLangFilter.exe input.xml ja
xml:lang属性にjaが指定されている要素だけをフィルタリングした結果
<?xml version="1.0"?>
<doc>
    <assembly>
        <name>Smdn.Utils</name>
    </assembly>
    <members>
        <member name="T:Smdn.Utils.StringUtils">
            <summary>文字列操作に関するユーティリティメソッドを提供するクラスです。</summary>
        </member>
        <member name="M:Smdn.Utils.StringUtils.Method1">
            <summary>メソッド1</summary>
        </member>
        <member name="M:Smdn.Utils.StringUtils.Method2">
            <summary>The Method2</summary>
        </member>
    </members>
</doc>

この方法では属性を付け加えるだけで言語別のドキュメントを記述でき、また記述されている言語の数に関わらずドキュメントの生成には一度だけのコンパイルで済むという利点がありますが、生成したドキュメントに対して加工を行うツールが必要になり、ドキュメントの加工を行う手順が増えるという欠点があります。