更新日: 2010 年 4 月 2 日

執筆者: 株式会社ピーデー 川俣 晶

この記事は、「MSDN プログラミング シリーズ」として発行している技術書籍「プロフェッショナルマスター Visual C# 2010 ~最新テクニックをマスターする 35 のテーマ」(日経 BP 社刊) を基にワンポイント テクニックを紹介しています。


センセーション 「おほん、教師のセンセーションである」
ジョシュア 「生徒のジョシュアです。ところで、コンパイル時でなく、後から開発して挿入するモジュールを呼び出す必要があるのですが、どういう手段が良いでしょうか?」
センセーション 「一般的には別プロセスで走らせて通信を行うのが最も簡単じゃのう。分離されていれば安全性も高まる。無論、ちょっとは遅くなってしまうが . . .」
ジョシュア 「性能重視なので遅いと困るんです。もっと良い方法はないですか?」
センセーション 「ならばアセンブリを動的にロードして呼び出してしまうべきじゃな。同じアプリケーション ドメインに相手がいれば、通信のオーバーヘッドは無くせるのじゃ」

目次

  1. ライブラリの動的読み込み
  2. 実行ファイルも動的読み込み
  3. プラグインの書き方
  4. 対戦ゲーム
  5. 結末

1. ライブラリの動的読み込み

使用するライブラリを実行時に動的に解決するというのは、.NET では、実は、それほど難しくありません。アプリケーション ドメインに新しいライブラリを動的に読み込むメソッドがもともと存在しています。

読み込んだアセンブリに含まれるクラスやメソッドを動的に探し出す処理はやや面倒になる可能性がありますが、C# 4.0 で追加された dynamic 型があれば、探し出した型を活用することもさらに簡単になります。コンパイル時に未知の型もこれで容易に扱えるからです。(この dynamic の活用例については、前回の最後にも簡単にご紹介しました。)

以下はそのサンプル コードです。

呼び出し側: (コンソール アプリケーション)

C#
using System;
using System.Reflection;

class Program
{
    static void Main(string[] args)
    {
        Assembly m = Assembly.LoadFrom(args[0]);
        dynamic instance1 = 
          Activator.CreateInstance(m.GetType("ClassLibrary1.Class1"));
        instance1.SayWelcome();
    }
}
 

呼ばれる側: (クラス ライブラリ)

C#
using System;

namespace ClassLibrary1
{
    public class Class1
    {
        public void SayWelcome()
        {
            Console.WriteLine("Welcome C# World!");
        }
    }
}
 

実行結果:

ただし、コンソール アプリケーション コマンドラインで、引数にクラス ライブラリの .dll ファイルを指定します

Welcome C# World!

このプログラムは、コマンドラインの引数で指定されたクラス ライブラリの DLL (.dll ファイル) を読み込み、すべての型を調べます。その結果、クラスを発見した場合はそれをインスタンス化し、SayWelcome メソッドを呼び出します。

ポイントは、SayWelcome というメソッドの存在を呼び出し側はまったく知らないにも関わらず、上記が実行できるということです。つまり、"実行時に" 動的にメソッドを調べて呼び出しているわけです。

ちなみに、型の名前が分かっている場合には、すべての型を調べなくても、Assembly クラスに含まれる GetType メソッドで効率よく型を取得することができます。(これについては、前回の記事を参考にしてください。)

ページのトップへ


2. 実行ファイルも動的読み込み

上記のサンプルで、「拡張子が .dll であれば動的に読み込める」と思われた方も居られるかもしれませんが、それは正解ではありません。実は、拡張子が .exe の実行ファイル本体も動的に読み込むことができます。

例えば、以下のようなコンソール アプリケーションのソース コードをコンパイルして実行ファイル (.exe ファイル) を得たとしましょう。この実行ファイルも、上述した方法で、クラス ライブラリ (.dll ファイル) の代わりにファイル名を指定して実行できます。

C#
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("not called");
        }
    }
}

namespace ClassLibrary1
{
    public class Class1
    {
        public void SayWelcome()
        {
            Console.WriteLine("Welcome C# World!");
        }
    }
}
 

実行結果:

コマンドライン引数に実行ファイルを指定した場合 (例えば、前述した呼び出し側の実行ファイルが a.exe で、上記が b.exe の場合、a.exe b.exe と実行します)

Welcome C# World!

この場合、上記のソース コードのアセンブリ (.exe) の Main メソッドは使用されていません。しかし、目的の名前を持つクラスが公開されていれば発見が可能で、インスタス化と実行ができます。

ページのトップへ


3. プラグインの書き方

この手法を用いると、後から別に開発されたプログラムで機能を拡張する、いわゆるプラグインと類似の機能を容易に作成できます。

そのために必要なことは 2 つしかありません。

  • プラグインのファイルの在処 (ありか) を指定する手段をきちんと決めておく
  • プラグインを呼び出す手順を明確に決めておく

ちなみに、老婆心から言えば、ダミーでも良いのでテスト用のプラグインを作成し、呼び出せることを確認しておくことも必要でしょう。「呼び出せるはず」と言うのが素人、「テスト用プラグインを呼び出せました」と言うのがプロフェッショナルです。

それでは、この 2 つはどう実装したら良いのでしょうか ?

結論は簡単です。ある決まった方法 (たった 1 つの方法) はありません。特定のフォルダに入れておいたり、決められたレジストリの値にパスを書き込んでおく方法でも良いでしょう。また、呼び出す方法も特定の名前のクラスやメソッドを用意するという方法でも良いし、ある決まったクラスを継承したクラスを作っておいたり、前回紹介したように特定の属性 (Attribute) を使用するということでも良いでしょう。また、特定のインターフェイスの定義を提供してそれを実装することを "約束" しても良いし、何も定義しないで特定の名前やシグネチャのメソッドを必ず用意すると決めておいても構いません。

どのような方法を使っても、たいていの場合、C# のリフレクションや dynamic 型を使えば対応できます。ですから、プラグインの書き方に唯一の正解はありません。プログラムを作成する皆さん自身が、その場に応じて最善の設計 (デザイン) を行うことが求められます。知恵と勇気と経験で素晴らしいデザインを実現しましょう!

Note: なお、ここでは説明しませんが、Managed Extensibility Framework (MEF) と呼ばれる拡張可能なアプリケーションを構築するためのフレームワークを使用すると、C# を使用して、プラグイン可能な高度なアプリケーションを効率的に作成することができます。

ページのトップへ


4. 対戦ゲーム

では、動的にコードを読み込む簡単なサンプルとして、以下のような例を紹介しましょう。

  • これは、数当てゲームです
  • ジャッジ (判定) では、1 から 10 までの数字を 1 つ決めます
  • ジャッジは、2 つのアセンブリを読み込みます
  • 2 つのアセンブリは、それぞれ単独で正解を推測して、Player クラスの Think メソッドを使って推測した値を返します
  • ジャッジは、参加している Player クラスの答えを調べて、正解なら、その旨を伝えて終了します (どちらかが正解するまで数当てゲームは続きます)

このプログラムはプレイヤーをプラグイン可能で、新しいプレイヤーを開発して試すために、毎回プレイヤーを組み込んで再コンパイルする必要はないわけです。ジャッジや乗り越えるべきライバルは、ソースコード非公開のままでも構いません。それでも対戦できます。

実際に、上記のジャッジと、2 種類のプレイヤーのソース コードを見てみましょう。

ジャッジのアプリケーション:

C#
using System;
using System.Reflection;

class Program
{
    // アセンブリの中を調べて、「Play」の名前を持つ型を検索する
    private static Type seekPlayer(Assembly assem)
    {
        foreach (var t in assem.GetTypes())
        {
            if (t.Name == "Player") return t;
        }
        return null;
    }

    static void Main(string[] args)
    {
        Assembly a1 = Assembly.LoadFrom(args[0]);
        Assembly a2 = Assembly.LoadFrom(args[1]);
        dynamic instance1 = Activator.CreateInstance(seekPlayer(a1));
        dynamic instance2 = Activator.CreateInstance(seekPlayer(a2));
        Console.WriteLine("ゲーム開始");
        int number = new Random().Next(10) + 1;
        for (; ; )
        {
            int answer1 = instance1.Think();
            int answer2 = instance2.Think();
            Console.WriteLine("プレイヤー1は{0}、プレイヤー2は{1}と答えました。", 
                answer1, answer2);
            if (number == answer1 && number == answer2)
            {
                Console.WriteLine("両者正解で引き分け");
                break;
            }
            if (number == answer1)
            {
                Console.WriteLine("Player1の勝ち");
                break;
            }
            if (number == answer2)
            {
                Console.WriteLine("Player2の勝ち");
                break;
            }
        }
    }
}
 

プレイヤー 1: 行き当たりばったり法

C#
using System;

namespace ClassLibrary1
{
    public class Player
    {
        private Random random = new Random();
        public int Think()
        {
            return 10 - random.Next(10);
        }
    }
}
 

プレイヤー 2: 総当たり法

C#
using System;

namespace ClassLibrary2
{
    public class Player
    {
        private int candidate = 0;
        public int Think()
        {
            return candidate++ % 10 + 1;
        }
    }
}
 

実行結果:

以下は、実行するごとに結果は異なります。また、3 つのソースコードをそれぞれ a.exe, b.dll. c.dll として、a.exe b.dll c.dll で実行したと仮定します。

ゲーム開始
プレイヤー 1 は 6、プレイヤー 2 は 1 と答えました。
プレイヤー 1 は 1、プレイヤー 2 は 2 と答えました。
プレイヤー 1 は 10、プレイヤー 2 は 3 と答えました。
プレイヤー 1 は 10、プレイヤー 2 は 4 と答えました。
プレイヤー 1 は 3、プレイヤー 2 は 5 と答えました。
Player 2 の勝ち

ちなみに、このアーキテクチャではプレイヤー 1 と 2 を入れ替えることも、新しく開発した自前のプレイヤーと対戦させることも容易です。

ページのトップへ


5. 結末

ジョシュア 「分かりました。後からアセンブリを読み込んで随時拡張できるプログラムを C# で書くこともできるわけですね!」
センセーション 「うむ。ある程度の定義さえきちんと打ち合わせておけば、後から未知のアセンブリを読み込んで呼び出すこともできるのじゃ。これぞ、無限の可能性じゃ」
ジョシュア 「ありがとうございます。これで疑問も解消です!」

Code Recipe Code Recipe

ページのトップへ