Cutting Edge

C# 4.0 の dynamic キーワードと COM

Dino Esposito

Dino Esposito私は C/C++ の開発者として経験を積んできましたが、特に Microsoft .NET Framework が存在していなかったころは、Visual Basic でプログラミングを行っていた同僚に、C や C++ のように型指定が厳密ではない言語を使用することについて不平をもらしていたものです。

型が静的に決まり、厳密に型指定するプログラミングが、ソフトウェア業界を成功へ導いた時代がありました。しかし、世の中は変わり、今日、C# の開発者のコミュニティ (かつての C/C++ の開発者のほぼすべてが集結したものと考えてよいでしょう) は、さらに動的なプログラミング モデルの必要性をはっきりと感じることが多くなりました。先月、マイクロソフトが C# 4.0 と Visual Studio 2010 を通じて利用可能にした動的プログラミング機能の一部を紹介しました。今月は、これに関連するシナリオをいくつか深く掘り下げて説明しましょう。まずは、C# 4.0 を使用せざるを得ない理由の 1 つ、つまり、C# 4.0 を使用すれば、.NET Framework の COM オブジェクトを使用して簡単にプログラミングできるという点から紹介します。

COM オブジェクトへの容易なアクセス

あるオブジェクトの構造と動作を、コンパイラが完全に認識している静的に定義された型では十分に記述できない場合、このオブジェクトは動的であると言えます。正直なところ、この文脈での動的という用語は、やや総称的に聞こえます。そこで、簡単な例を見てみることにしましょう。VBScript などのスクリプト言語では、次のコードは問題なく実行されます。

Set word = CreateObject("Word.Application")

CreateObject 関数は、引数として渡される文字列が、登録済み COM オブジェクトの ProgID であると想定します。この関数はコンポーネントのインスタンスを作成して、その IDispatch オートメーション インターフェイスを返します。IDispatch インターフェイスの詳細を、スクリプト言語レベルで見ることはありません。重要なのは、次のようなコードを記述できることです。

Set word = CreateObject("Word.Application")
word.Visible = True
Set doc = word.Documents.Add()
Set selection = word.Selection
selection.TypeText "Hello, world"
selection.TypeParagraph()

doc.SaveAs(fileName)

このコードでは、まず、基になる Microsoft Office Word アプリケーションの動作をオートメーションで起動するコンポーネントへの参照を作成しています。次に、Word のメイン ウィンドウを表示状態にして、新しい文書を追加し、なんらかのテキストを書き込んで、この文書をどこかに保存しています。コードは明確で、簡単に読み取ることができ、そしてさらに重要なことですが、正しく動作します。

このように機能するのは、VBScript が提供する特定の機能、つまり遅延バインディングのおかげです。遅延バインディングとは、渡されたオブジェクトの型を、実行の流れの中で使用することになるまで認識しないことを意味します。オブジェクトを使用することになると、ランタイム環境は、まず、呼び出されたメンバーが実際にオブジェクトに存在することを確認してから、そのメンバーを呼び出します。コードが実際に実行される前に、あらかじめ確認されることは一切ありません。

ご存知のように、VBScript などのスクリプト言語にはコンパイラがありません。ただし、Visual Basic (CLR バージョンを含む) には、以前から類似機能がありました。正直に言うと、Visual Basic には COM オブジェクトを簡単に使用できる機能が備わっていたため、Visual Basic 担当の同僚をうらやましく思っていたものです。COM オブジェクトは、Office など、相互運用する必要があるアプリケーションの重要な構成要素であることが多かったためです。実際のところ、私のチームでは、アプリケーション全体を C# で記述していても、相互運用に関連するコード部分は Visual Basic で記述することがありました。意外ですか。多言語プログラミングとは、いつかはたどり着くことになる、新しい領域ではないでしょうか。

Visual Basic には、(重要な) 互換性の理由から、CreateObject 関数が存在します。大切なのは、.NET Framework ベースの言語が、事前バインディングを考慮して設計されたということです。COM 相互運用は .NET Framework で対応できるシナリオでしたが、C# 4.0 が開発されるまでは、キーワードや機能として、言語で明確にサポートされたことはありませんでした。

C# 4.0 (および Visual Basic) には、動的なルックアップ機能が用意されており、現在では遅延バインディングが .NET Framework 開発者にも使用できる処理となっています。動的なルックアップを使用すると、実行時に解決される静的型チェックを行わずに、コードからメソッド、プロパティ、インデクサー プロパティ、およびフィールドにアクセスできます。

また、C# 4.0 では、メンバー宣言で既定値を認識することで、パラメーターの省略を可能にします。つまり、省略可能なパラメーターを持つメンバーが呼び出されるときに、省略可能な引数であれば指定しなくてもかまいません。また、引数は名前でも位置でも渡すことができます。結局、C# 4.0 で COM のバインドが強化されたことで、単純に、スクリプト言語の一般的な機能の一部が、静的かつ厳密な型指定が必要な言語でもサポートされるようになります。新しい dynamic キーワードを使用して、COM オブジェクトをシームレスに操作する方法を紹介する前に、型の動的ルックアップの内部動作について少し詳しく説明しましょう。

動的言語ランタイム

Visual Studio 2010 で変数を dynamic として宣言するときは、既定の構成に IntelliSense を含めないようにします。興味深いことに、ReSharper 5.0 (jetbrains.com/resharper、英語) などの追加のツールをインストールすると、IntelliSense を通じて、動的オブジェクトに関する情報を一部取得できます。図 1 に、ReSharper をインストールした場合としなかった場合のコード エディターを示しています。このツールでは、dynamic 型で定義されているメンバーが一覧されるだけです。少なくとも、dynemic オブジェクトは System.Object のインスタンスです。

image: IntelliSense for a Dynamic Object in Visual Studio 2010, with and Without ReSharper
図 1 Visual Studio 2010 における動的オブジェクトの IntelliSense (ReSharper をインストールした場合としなかった場合)

コンパイラが次のようなコードを検出するとどうなるか見てみましょう (実装の詳細を簡単に理解できるように、コードは意図的に単純にしています)。

class Program
{
  static void Main(string[] args) 
  { 
    dynamic x = 1;
    Console.WriteLine(x);
  }
}

2 行目で、コンパイラは WriteLine のシンボルの解決を試みないため、従来の静的型チェックのように、警告やエラーはスローされません。dynamic キーワードが関係している限り、C# はインタープリター言語のように処理されます。そのため、コンパイラでは、dynamic 変数または dynamic 引数が含まれる式を解釈する、アドホック コードを出力します。インタープリターは、.NET Framework メカニズムの最新のコンポーネントである、動的言語ランタイム (DLR) に基づいています。より明確に表現すると、コンパイラは、DLR によってサポートされる抽象構文を使用して式ツリーを生成し、この式ツリーを DLR ライブラリに渡して処理しなければなりません。DLR 内では、コンパイラによって作成された式が、動的に更新されるサイト オブジェクトにカプセル化されます。サイト オブジェクトの役割は、実行時にメソッドをオブジェクトにバインドすることです。図 2 には、先ほど説明した簡単なプログラムに対して出力される実際のコードを、大幅に省略して示します。

図 2 のコードは、読みやすいように編集して簡単にしていますが、処理の要点は示されています。dynamic 変数が System.Object インスタンスにマップされ、DLR のプログラム向けにサイト オブジェクトが作成されます。サイト オブジェクトは、パラメーターを受け取る WriteLine メソッドとターゲット オブジェクトのバインドを管理します。このバインドは、Program 型のコンテキスト内で保持されます。dynamic 変数の Console.WriteLine メソッドを呼び出すには、サイト オブジェクトを呼び出し、ターゲット オブジェクト (この場合は Console 型) とそのパラメーター (この場合は dynamic 変数) を渡します。内部では、x 変数に現在格納されているオブジェクトを、パラメーターとして受け取ることができる WriteLine メンバーが、ターゲット オブジェクトに実際に含まれているかどうかを、サイト オブジェクトが確認します。問題が発生すると、C# ランタイムから RuntimeBinderException がスローされます。

図 2 dynamic 変数の実際の実装

internal class Program
{
  private static void Main(string[] args)
  {
    object x = 1;

    if (MainSiteContainer.site1 == null)
    {
      MainSiteContainer.site1 = CallSite<
        Action<CallSite, Type, object>>
        .Create(Binder.InvokeMember(
          "WriteLine", 
          null, 
          typeof(Program), 
          new CSharpArgumentInfo[] { 
            CSharpArgumentInfo.Create(...) 
          }));
    }
    MainSiteContainer.site1.Target.Invoke(
      site1, typeof(Console), x);
  }

  private static class MainSiteContainer
  {
    public static CallSite<Action<CallSite, Type, object>> site1;
  }
}

COM オブジェクトの操作

.NET Framework ベースのアプリケーション内から COM オブジェクトを操作する C# 4.0 の新機能は、最近では非常に簡単なものになっています。C# で Word 文書を作成する方法を紹介して、.NET 3.5 と .NET 4 で必要なコードを比較してみましょう。サンプル アプリケーションでは、指定のテンプレートに基づいて新しい Word 文書を作成し、文書に書き込んでから、決まった場所に保存します。テンプレートには、共通の情報対するブックマークがいくつか含まれています。.NET Framework 3.5 または .NET Framework 4 のいずれを対象にしているかにかかわらず、Word 文書をプログラムから作成するには、まず、Microsoft Word オブジェクト ライブラリを追加します (図 3 参照)。

image: Referencing the Word Object Library
図 3 Word オブジェクト ライブラリの参照

Visual Studio 2010 や .NET Framework 4 が開発される前は、Word 文書を作成する際、図 4 のようなコードが必要でした。

図 4 C# 3.0 による新しい Word 文書の作成

public static class WordDocument
{
  public const String TemplateName = @"Sample.dotx";
  public const String CurrentDateBookmark = "CurrentDate";
  public const String SignatureBookmark = "Signature";

  public static void Create(String file, DateTime now, String author)
  {
    // Must be an Object because it is passed as a ref
    Object missingValue = Missing.Value;

    // Run Word and make it visible for demo purposes
    var wordApp = new Application { Visible = true };

    // Create a new document
    Object template = TemplateName;
    var doc = wordApp.Documents.Add(ref template,
      ref missingValue, ref missingValue, ref missingValue);
    doc.Activate();

    // Fill up placeholders in the document
    Object bookmark_CurrentDate = CurrentDateBookmark;
    Object bookmark_Signature = SignatureBookmark;
    doc.Bookmarks.get_Item(ref bookmark_CurrentDate).Range.Select();
    wordApp.Selection.TypeText(current.ToString());
    doc.Bookmarks.get_Item(ref bookmark_Signature).Range.Select();
    wordApp.Selection.TypeText(author);

    // Save the document 
    Object documentName = file;
    doc.SaveAs(ref documentName,
      ref missingValue, ref missingValue, ref missingValue, 
      ref missingValue, ref missingValue, ref missingValue, 
      ref missingValue, ref missingValue, ref missingValue, 
      ref missingValue, ref missingValue, ref missingValue,
      ref missingValue, ref missingValue, ref missingValue);

    doc.Close(ref missingValue, 
      ref missingValue, ref missingValue);
    wordApp.Quit(ref missingValue, 
      ref missingValue, ref missingValue);
  }
}

COM オートメーション インターフェイスを操作するには、多くの場合、バリアント型を使用する必要があります。.NET Framework ベースのアプリケーションで、COM オートメーション オブジェクトを操作する際、バリアント型をプレーンなオブジェクトとして表します。実質的な効果としては、バリアント型パラメーターは参照によって渡す必要があるため、Word 文書の基になるテンプレート ファイルの名前を指定する際に、文字列は使用できません。次に示すように、代わりに Object を利用する必要があります。

Object template = TemplateName;
var doc = wordApp.Documents.Add(ref template,
  ref missingValue, ref missingValue, ref missingValue);

続いて検討する必要があるのは、Visual Basic とスクリプト言語は、C# 3.0 に比べて非常に制限が少ない点です。たとえば、COM オブジェクトのメソッドで宣言するパラメーターをすべて指定するように強制されることはありません。一方、Documents コレクションの Add メソッドには 4 つの引数が必要で、使用している言語で省略可能なパラメーターをサポートしていない限りは、引数を無視することはできません。

既に説明したように、C# 4.0 では省略可能なパラメーターをサポートしています。そのため、C# 4.0 を使用して、図 4 のコードを再コンパイルするだけで機能し、不明値だのみを渡している ref パラメーターをすべて省略するように書き直すことができます。コードは次のとおりです。

Object template = TemplateName;
var doc = wordApp.Documents.Add(template);

C# 4.0 の "ref を省略する" という新しいサポートを使用すると、図 4 のコードはより単純になります。さらに重要なのは、コードが読みやすくなり、スクリプト コードと構文的に似てくるということです。図 5 には、C# 4.0 を使用してコンパイルされ、図 4 のコードと同じ効果が得られるように編集したコードを示します。

図 5 C# 4.0 による新しい Word 文書の作成

public static class WordDocument
{
  public const String TemplateName = @"Sample.dotx";
  public const String CurrentDateBookmark = "CurrentDate";
  public const String SignatureBookmark = "Signature";

  public static void Create(string file, DateTime now, String author)
  {
    // Run Word and make it visible for demo purposes
    dynamic wordApp = new Application { Visible = true };
            
    // Create a new document
    var doc = wordApp.Documents.Add(TemplateName);
    templatedDocument.Activate();

    // Fill the bookmarks in the document
    doc.Bookmarks[CurrentDateBookmark].Range.Select();
    wordApp.Selection.TypeText(current.ToString());
    doc.Bookmarks[SignatureBookmark].Range.Select();
    wordApp.Selection.TypeText(author);

    // Save the document 
    doc.SaveAs(fileName);

    // Clean up
    templatedDocument.Close();
    wordApp.Quit();
  }
}

図 5 のコードにより、.NET Framework のプレーン型を使用して、COM オブジェクトを呼び出すことができます。また、省略可能なパラメーターを使用することで、さらに簡単になります。

C# 4.0 で導入された dynamic キーワードと他の COM 相互運用機能を使用しても、必ずしもコードを迅速に作成できるわけではありませんが、C# コードをスクリプトのように作成することができます。COM オブジェクトにとって、スクリプトのように作成できることは、パフォーマンスが向上することと同じくらい、重要なことかもしれません。

PIA を使用しない配置

.NET Framework が初めて開発されて以来、COM オブジェクトはマネージ クラスにラップして、.NET ベースのアプリケーションから使用することができました。これを行うには、COM オブジェクトのベンダーから提供される、プライマリ相互運用機能アセンブリ (PIA) を使用する必要があります。PIA は必要不可欠で、クライアント アプリケーションと合わせて配置する必要があります。ただし、多くの場合、PIA はあまりに大規模で、COM API 全体が含まれてしまうため、セットアップに PIA を格納するのは望ましくないかもしれません。

Visual Studio 2010 には、No-PIA (PIA を使用しない) というオプションが用意されています。No-PIA とは、現在のアセンブリに、PIA から取得した必要な定義を埋め込む、コンパイラの機能のことです。この機能により、本当に必要な定義のみが最終アセンブリに含まれるため、ベンダーの PIA をセットアップに組み込む必要がありません。図 6 には、Visual Studio 2010 で No-PIA を有効にする、[プロパティ] ボックスのオプションを示します。

image: Enabling the No-PIA Option in Visual Studio 2010
図 6 Visual Studio 2010 で No-PIA を有効にするオプション

No-PIA は、型の同値化として知られている C# 4.0 の機能が基になっています。簡単に説明すると、型の同値化とは、明確に異なる 2 つの型を実行時には同等と見なし、同じ意味で使用できるということです。型の同値化の一般的な例としては、異なるアセンブリで定義された同じ名前の 2 つのインターフェイスが挙げられます。これらのインターフェイスの型は異なりますが、同じメソッドが存在する限り、同じ意味で使用できます。

以上のことをまとめると、COM オブジェクトを使用する負荷は高いままですが、C# 4.0 の COM 相互運用サポートを利用すると、さらに簡単なコードを作成できます。.NET Framework ベースのアプリケーションから COM オブジェクトを処理すると、本来ならほとんど管理することのなかった、レガシ アプリケーションや重要なビジネス シナリオにかかわることになります。COM は .NET Framework の必要悪ですが、dynamic キーワードを使用することで負担を少し軽減することができます。

Dino Esposito は、『Programming ASP.NET MVC』 (Microsoft Press) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』 (Microsoft Press、2008 年) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) です。

この記事のレビューに協力してくれた技術スタッフの Alex Turner に心より感謝いたします。